Agent Mail
Space MCP tools in the Agent Mail scope (agent_mail). Reach these with a wks_live_ key over space-mcp or space-api.
Tools in the agent_mail scope, callable over space-mcp (tools/call) or space-api (REST) with a wks_live_* key that grants agent_mail.
add_campaign_step
Scope: agent_mail:write
Append ONE email to the end of a drip / cold-email sequence — the way to add a follow-up. Works on a LIVE campaign and touches nothing else: existing steps, their per-recipient drafts, and the sent history all survive (unlike update_mail_campaign's steps, which replaces the sequence). Returns the new step_id. Appending does NOT send anything: people who already finished the sequence stay finished until you queue_campaign_follow_up them onto this step. Set personalization_enabled to write one email per recipient (write_campaign_draft).
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
is_welcome | boolean | newsletter: this is its welcome email (one per newsletter), sent once to each new subscriber when welcome_on. | |
subject | string | Merge tags allowed ({{first_name}}...). On a personalized step this is the fallback for anyone without a draft. | |
body_text | string | ||
body_html | string | ||
template_id | string | ||
delay_minutes | number | Wait after the previous step. Default 1440 (one day). | |
from_address_override | string,null | Force this one email to a specific mailbox, for EVERY recipient. Leave null to keep each person's own sender — the mailbox that wrote to them first. | |
personalization_enabled | boolean | When true, each recipient gets a unique AI-written email for this step. The step's subject/body become the base template the AI rewrites per contact. | |
personalization_prompt | string | What you should know about THIS email — both how to write it and how to handle someone who answers it (e.g. 'first touch; lead with the finding for their building. If they ask for the one-pager, send it. If they ask price, hand it to the owner.'). Sent to you with every reply to this email, and where it differs from your usual way of working, it wins. Applies whether or not personalization_enabled is set. |
add_people_to_campaign
Scope: agent_mail:run
Put people into an email campaign: new people start at email 1 (or start_at = the email number), queued behind what is already scheduled at the campaign's pace — or on start_on (a date) to bring someone back later ("write again in spring"). Someone who finished, was stopped, replied or is paused is PUT BACK IN at that email; someone who unsubscribed or bounced is refused by name. On a draft campaign they wait for Start. Name people by contact_ids or emails (contacts of this space).
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_ids | string[] | ||
emails | string[] | Email addresses of contacts in this space. | |
start_at | number | The email number to begin at (1 = the first). Default 1. | |
start_on | string | A date/time to send their first email (running campaigns). Default: next in line. |
approve_campaign_drafts
Scope: agent_mail:write
Approve personalized draft(s) so they're allowed to send. Pass draft_id for one, or campaign_id to approve ALL generated/edited drafts at once. NOTE: you (the agent) can only approve when the campaign's personalization_review='any' — if it's 'owner', a human must approve in the app.
| Parameter | Type | Required | Description |
|---|---|---|---|
draft_id | string | ||
campaign_id | string | Approve all reviewable drafts on this campaign. |
control_mail_campaign
Scope: agent_mail:run
Pause, resume, or cancel an email campaign. Broadcasts: pause/cancel while scheduled, resume re-schedules. Drips: pause/resume while active/paused, cancel from draft/active/paused. A campaign already fanning out ('sending') can no longer be stopped.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
action | pause | resume | cancel | yes |
create_designed_mail_template
Scope: agent_mail:write
Create a DESIGNER-GRADE email template by emitting a block tree — the SAME pipeline the in-UI AI Mail designer uses. The blocks are compiled by the shared email-safe compiler, so the result automatically gets table layout, Outlook fallbacks, dark-mode flip, and mobile reflow (prefer this over create_mail_template + raw body_html, which skips all that). Returns a normal template (usable in campaigns by template_id) whose blocks also open in the visual editor. blocks: ordered array. Each = { block_type, props?, design?, slots? }. BLOCK TYPES + key props: hero { title, subtitle?, cta_label?, cta_url?, image_url?, alignment?:'left'|'center', text_overlay?:bool (image becomes full-bleed bg w/ white text), overlay_opacity?:0..0.95, background_link_url? }; text { content_md } (markdown: # ## ### , bold, italic, links, >quote, - lists); button { label, url, variant?:'solid'|'outline', font_size?:10-28, color?:hex, text_color?:hex }; image { url, alt?, link_url? }; image_with_caption { image_url, caption?, credit? }; product { image_url?, title, price?, original_price?, description?, cta_label?, cta_url?, cta_color?, cta_font_size?, layout?:'card'|'inline' }; pull_quote { quote_text, attribution?, variant?:'default'|'highlight' }; section_header { eyebrow?, title?, show_rule? }; feature_row { columns?:2|3|4, layout?:'grid'|'list', features:[{icon,title,body?}] }; social_row { links:[{platform:'facebook'|'x'|'instagram'|'youtube'|'linkedin'|'tiktok'|'website'|'email', url}] }; callout { title, body?, cta_label?, cta_url?, cta_color?, cta_variant?, button_position?:'beside'|'below' }; footer { address, show_unsubscribe? }; divider; spacer { height }; custom_html { html }; columns { column_count:2 }; flex_columns { widths:[40,60] }. CTA styling on hero/callout/product uses cta_color, cta_text_color, cta_variant, cta_size, cta_font_size. design (any block): background_color, background_image_url (+ background_image_size 'cover'|'tile'), background_overlay_opacity (0..0.95 darken scrim), background_overlay_color (+ background_overlay_angle) for gradients, text_color, border_color, padding/padding_x/padding_y/margin_top/margin_bottom (none/xs/sm/md/lg/xl/2xl), border, radius, text_align, font_size_scale (xs..4xl). NESTING: columns/flex_columns hold children via slots:[[...left blocks], [...right blocks]] (slot 0=left, 1=right). Only those two types accept slots. theme (optional, whole email): { palette?, accent_color?:hex, font?, spacing_rhythm?:'compact'|'normal'|'generous', dark_mode_variant?:'auto', page_background_color?:hex, page_background_image_url? }. Supports {{merge_tags}} like {{first_name}} in any text/subject.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
subject | string | ||
description | string | ||
category | string | ||
theme | object | Whole-email theme (see description). | |
blocks | object[] | yes | Ordered block tree. Each item { block_type, props?, design?, slots? } — see description for the full vocabulary. |
create_mail_campaign
Scope: agent_mail:write
Create a DRAFT email campaign. Pick its KIND first — it decides how it sends: kind 'newsletter' = the same emails for everyone subscribed (the audience); each email (a step) is sent on its own with send_newsletter_email to everyone subscribed AT THAT MOMENT, nobody walks a series, someone who subscribes later gets the next email (and the welcome email when welcome_on — pass welcome). kind 'custom' = each person gets emails written for them, as a sequence; with enrollment_mode 'continuous' someone added to the audience later is HELD (nothing sent) until someone picks the email they start at (add_people_to_campaign with start_at) — newcomers 'first_email' starts them at email 1 instead. Omit kind only for the old broadcast/drip behaviour.
A connected sender address is NOT required to draft — only to send. For drips pass steps in order. Use list_mail_addresses / list_audiences to discover ids.
A campaign has THREE parts. (1) WHAT IT IS FOR: description, a few sentences — who it is for, the offer, the goal. (2) WHO IT GOES TO and how it paces: audience, sender(s), daily_send_cap, send window, min_send_gap_minutes ('one every N minutes' — say it directly rather than working backwards from a cap), warmup_*, sender_rotation_ids. (3) THE EMAILS: each step has its content plus personalization_prompt — what YOU should know about that email, covering both how to write it and how to answer someone who replies to it. The RULES (what may be promised, which template answers which question, what goes straight to the owner) are not the campaign's: they are the running agent's own instructions, which it carries everywhere. A step's own note wins where it differs.
WHO RUNS IT: call whoami for your own agent_id, set agent_bot_id to it, and the campaign is yours. It can be reassigned; the new agent reads the description and each email's note and works by its own instructions. For a personalized step you write each recipient's email with write_campaign_draft (free, on your own model), or have the platform write them with generate_campaign_drafts (billed to the space wallet) — your choice. personalization_review decides who approves them before sending ('none' | 'owner' | 'any').
WHEN SOMEONE REPLIES: it lands in the inbox and, with stop_on_reply, their follow-ups stop; the next email never goes out on top of an unanswered reply. To answer replies yourself, LISTEN: listen_to_events { event_types: ['email_campaign_replied'], filters: { email_campaign_replied: { campaign_id } } }. Each reply then reaches your chat with message_id and email_address_id — answer in the thread with send_agent_mail { from_address_id: email_address_id, reply_to_message_id: message_id }. Whether you ask first before sending is your own ask-first setting.
COLD EMAIL: is_outreach=true on a drip presets stop_on_reply, a 50/day cap and a 9-17 Mon-Fri window, each overridable.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
kind | newsletter | custom | newsletter = each email goes to everyone subscribed when it is sent; custom = emails written per person. | |
newcomers | hold | first_email | custom + continuous: someone added later is held until someone picks where they start (default) or starts at email 1. | |
welcome_on | boolean | newsletter: send the welcome email to each new subscriber, once. | |
welcome | object | newsletter: the welcome email each new subscriber gets. | |
campaign_type | broadcast | drip | Old behaviour, only when kind is omitted. | |
subject | string | ||
body_text | string | ||
body_html | string | ||
from_address_id | string | Optional — campaign stays draft-only until set. | |
audience_id | string | ||
template_id | string | ||
enrollment_mode | one_shot | continuous | ||
trigger_audience_id | string | Continuous drips: audience to keep enrolling from. | |
steps | object[] | Drip steps in order. | |
is_outreach | boolean | Cold-email preset marker (drips): defaults stop_on_reply=true, daily_send_cap=50, 9-17 Mon-Fri send window unless overridden. | |
daily_send_cap | number,null | Max sends/day for this campaign. null = unlimited. | |
send_window_start_hour | number,null | Send window start hour 0-23 (campaign timezone). | |
send_window_end_hour | number,null | Send window end hour 1-24. | |
send_window_days | array,null | Allowed weekdays 0=Sun…6=Sat. null/[] = every day. | |
send_window_timezone | string,null | IANA timezone for the send window (defaults to the space timezone). | |
frequency_cap_hours | number | Min hours between any two emails to the same contact, across ALL campaigns. 0 = off. NOT the spacing of this campaign's own sends — that is min_send_gap_minutes (between people) and each step's delay (between emails to one person). | |
min_send_gap_minutes | number,null | Minimum minutes between two sends of THIS campaign — 'one every 15 minutes'. null = spacing is whatever daily_send_cap works out to across the send window. Works with no window too. When both apply, the tighter of the gap and the cap wins. | |
stop_on_reply | boolean | Stop the campaign for a contact once they reply. Every campaign comes with it ON: it is the automation "Someone replies → stop it for them" (made for the campaign, visible in Automations). false switches that automation off. | |
stop_on_click | boolean | Stop the campaign for a contact once they click a link — an automation made for the campaign, like stop_on_reply. | |
description | string,null | What this campaign is for, in a few sentences (who it is for, the offer, the goal — e.g. 'Offers our LL97 audit to NYC building owners; every reply should lead to a 20-minute call.'). Read by whoever writes its emails or answers its replies. The RULES — what may be promised, what goes to the owner, which template answers what — belong in the running agent's own instructions, not here; a single email's specifics go in that step's personalization_prompt. Changeable while the campaign runs. | |
sender_rotation_ids | array,null | Pool of sending-address ids rotated per send. Each send goes to whichever mailbox has used the least of its OWN daily allowance, so a small or newly-warming address takes its share instead of waiting for a bigger one to drain; when nothing has a stated limit it simply round-robins. Set the ceilings with set_mail_address_daily_limit. Overrides from_address_id for drip sends, and a step's from_address_override overrides this. | |
warmup_enabled | boolean | Gradually ramp the daily cap for new senders. | |
warmup_start_cap | number | Warmup day-1 daily cap (default 10). | |
warmup_daily_increment | number | How much the warmup cap grows per day (default 5). | |
personalization_review | none | owner | any | For AI-personalized steps, who APPROVES each recipient's draft: 'none' = auto-send (hands-off), 'owner' = a human must approve in the app, 'any' = you (the agent) may approve too. Default 'owner'. | |
agent_bot_id | string,null | Which MACHINE AGENT in this space runs this campaign — writes each person's email when a step is personalized. Any engine (hermes, claude_code, codex, grok, opencode, dsh). Call whoami for your own agent_id, or list_agents for the other agents in this space (use their agent_id, NOT assigned_to_workspace_id). Changeable at any time: the new agent reads the description and each email's note, and works by its own instructions. NULL = none picked, which resolves to the space's single active agent and refuses when there are several. |
create_mail_template
Scope: agent_mail:write
Create a reusable email template. kind 'html' (full designed HTML email you author) or 'plain_text'. Supports {{merge_tags}} like {{first_name}}.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
kind | html | plain_text | ||
subject | string | ||
body_html | string | Required for kind 'html'. | |
body_text | string | Required for kind 'plain_text'. | |
description | string | ||
category | string | ||
variables | object[] |
create_mailbox
Scope: agent_mail:write
Ask to make a new mailbox (email address) on one of this space's ready mail domains — e.g. sales@acme.com for a cold-email campaign. The owner approves it in this chat before anything is made; once approved it is in this space, usable as a campaign sender or with send_email. A mailbox new to cold email should get a low daily_send_limit. Check list_mail_domains first: the domain must be listed and ready, and the address not taken.
| Parameter | Type | Required | Description |
|---|---|---|---|
email_address | string | The whole address, e.g. sales@acme.com. Or give name + domain. | |
name | string | The part before the @ (when email_address is not given). | |
domain | string | The domain (when email_address is not given), one from list_mail_domains. | |
display_name | string | The name people see beside the address, e.g. "Acme Sales". | |
daily_send_limit | number,null | Max emails/day from this address. null = unlimited. |
create_message_template
Scope: agent_mail:write
Create a reusable message template. channel: email | sms (subject is email-only). variables: [{key,label,required?,default?}] for {{merge}} tags.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
channel | sms | email | yes | |
subject | string | Email subject (email channel only). | |
body | string | ||
variables | object[] | [{key,label,required?,default?}] |
create_signature_template
Scope: agent_mail:write
Author a signature template: an inline-styled HTML fragment (a block, NOT a full document) containing {{tokens}} that are filled from whichever mailbox uses it. Tokens: {{full_name}} {{title}} {{company}} {{phone}} {{phone_link}} {{email}} {{website}} {{website_url}} {{address}} {{image_url}}. Use {{phone_link}} inside href="tel:…" and {{website_url}} inside href, since those carry the machine-readable form. Elements left holding only a separator after substitution are dropped, so a mailbox with no phone will not show a dangling divider. A template with no tokens is rejected — that is a fixed signature, and create_mail_signature is the tool for those.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Shown in the picker, e.g. 'Narrow — logo left'. |
html | string | yes | Inline-styled HTML fragment with {{tokens}}. |
description | string | Optional note, e.g. 'reads well on phones'. |
delete_campaign_step
Scope: agent_mail:write
Take ONE email out of a campaign. People who have not reached it yet skip straight to the next one. A custom campaign must not be running (pause it first); a newsletter email already sent is history and stays.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | ||
step_id | string | yes |
delete_mail_campaign
Scope: agent_mail:write
Delete an email campaign that never started (a draft) — its emails and settings go with it. A campaign that sent anything is history: stop it with control_mail_campaign cancel instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes |
delete_message_template
Scope: agent_mail:write
Delete a space message template.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes |
delete_signature_template
Scope: agent_mail:write
Delete a signature template. Mailboxes using it fall back to their built-in layout; their details are untouched, so nobody is left without a signature.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes |
generate_campaign_drafts
Scope: agent_mail:write
Write per-recipient AI-personalized emails for a drip campaign's personalized steps (steps with personalization_enabled). Generates up to limit (step × contact) pairs per call and returns { generated, failed, remaining } — CALL AGAIN while remaining>0 to cover everyone. Bills the space wallet. If personalization_review='none' drafts auto-send; otherwise review/approve them before (or after) starting. Safe to run before Start — drafts are keyed to contacts and reused when the campaign begins. Pass contact_ids or regenerate=true to redo specific ones.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_id | string | Only generate for this step (default: all personalized steps). | |
contact_ids | string[] | Only (re)generate for these contacts. | |
regenerate | boolean | Also redo existing non-approved drafts (generated/edited/failed). | |
limit | number | Max pairs this call (default 15, max 50). |
get_brand_footer
Scope: agent_mail:read
Get the space's legal email footer — mailing address, fine print and unsubscribe setting. On a campaign or drip send these are appended automatically if the email does not already carry them (CAN-SPAM safety net; plain 1:1 emails are untouched). address, social_links and logo_url are read through from the space brand record — see get_space_brand, which also carries the phone, contact email, website and legal name.
get_campaign_results
Scope: agent_mail:read
How an email campaign is doing, in one read — the same numbers as its Results tab, counted in PEOPLE: people, reached, emails sent, delivered, opened, clicked, replied, unsubscribed, didn't arrive (with rates), each email's own numbers (plus how many go out today and how many are waiting for it), where everyone is now (not reached yet, waiting for the next email, replied, finished, paused, stopped, unsubscribed, failed) and what each mailbox sent. Use it to answer 'how did the campaign do?'; then list_campaign_people with a metric (and email_number) to see WHO is behind a number.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | From list_mail_campaigns. |
get_mail_campaign
Scope: agent_mail:read
Full campaign detail: settings, drip steps, whether this space requires human approval before sending — plus pacing (the send window, daily cap and minimum gap MULTIPLIED OUT into how many this campaign can actually send per day, and which limit binds) and queue (who is in line right now, grouped by day with first and last send time). Read the pacing before setting any send time: a 50/day cap with a 15-minute gap inside a 9–5 window is 32 a day, not 50.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes |
get_mail_template
Scope: agent_mail:read
Get one email template including its body.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes |
get_mail_template_blocks
Scope: agent_mail:read
Get a template's editable block tree (block_type, props, design, parent_id, parent_slot, order_index) plus its theme — so you can edit precisely and send the modified tree back via update_designed_mail_template.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes |
get_mail_thread
Scope: agent_mail:read
Get one inbound mail thread with its messages (oldest first), including each message id (use as reply_to_message_id in send_agent_mail to reply in-thread).
| Parameter | Type | Required | Description |
|---|---|---|---|
thread_id | string | yes | |
message_limit | number |
get_mailbox_signature
Scope: agent_mail:read
Read one sending address's signature: its details (name, title, company, phone, email, website, postal address), which layout or template renders them, whether it is switched on, and the currently rendered HTML. Use list_mail_addresses for the address id.
| Parameter | Type | Required | Description |
|---|---|---|---|
address_id | string | yes |
get_message_template
Scope: agent_mail:read
Get one space message template including its body and variables.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes |
get_newsletter_subscriber
Scope: agent_mail:read
One newsletter subscriber's story: every email of the newsletter they received (delivered, each open, each click with its link), their replies to each with any answer, the emails still to come, how they joined, and whether they unsubscribed or bounced. Read one sent email's full text with list_mail_messages / get_mail_thread by its thread_id.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | yes |
list_campaign_drafts
Scope: agent_mail:read
List the per-recipient personalized drafts for a campaign: contact, subject, body_html, status (generated/edited/approved/skipped/failed) + a status rollup (counts). Filter by step_id or status. Use to review what the AI wrote before approving.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_id | string | ||
status | generated | edited | approved | skipped | failed | ||
limit | number |
list_campaign_people
Scope: agent_mail:read
WHO is behind any number of an email campaign — the same list its Results page opens: who got it, who opened (email 2), who clicked, who replied (and to which email), who unsubscribed, whose email didn't arrive. Each person comes with every email's state (sent · delivered · opened · clicked · did_not_arrive) and their place in the campaign. Counted in people, like get_campaign_results. Then act on them: tag_campaign_people, add_campaign_people_to_list, or one person with stop/pause/resume_campaign_for_person.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | From list_mail_campaigns. |
metric | all | sent | delivered | opened | clicked | replied | unsubscribed | not_arrived | Which number: all (everyone in it) · sent · delivered · opened · clicked · replied · unsubscribed · not_arrived (bounced or refused). Default all. | |
email_number | number | One email of the campaign (1 = the first). Leave out for any of them — e.g. metric opened + email_number 2 = who opened email 2. | |
query | string | Narrow by name, email or company. | |
offset | number | ||
limit | number | Up to 200. Default 50. |
list_campaign_recipients
Scope: agent_mail:read
Per-recipient state of a mail campaign or drip: who's enrolled, current step, next send, unsubscribed/failed, each recipient's last-message tracking (delivered/opened/clicked), whether they REPLIED, and sent_from — the mailbox that wrote to them, which is the one their follow-up will come from. Use to answer 'who opened the campaign?' or 'who should I follow up with?' (replied:false). The replied flag is read from the mail itself, not from enrollment status: a one-shot that already finished never updates that column, so filtering on status alone would follow up with people who already answered.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | From list_mail_campaigns. |
status | active | completed | unsubscribed | failed | paused | stopped | replied | Enrollment state. 'completed' = finished the sequence, which on a one-shot is everyone who was emailed. 'stopped' = taken out by a person, an agent, an automation or a rule (each row says who and why); 'paused' rows say until when. Prefer the replied filter for who wrote back. | |
replied | boolean | true = only people who wrote back. false = only those who never did — the follow-up list. | |
step_id | string | Also return each person's draft_status for this step, so you can see who still needs one written. | |
limit | number | Max rows (default 50, cap 200). | |
offset | number |
list_campaign_replies
Scope: agent_mail:read
List everyone who REPLIED to a drip / cold-email campaign: contact, when they replied, the mail thread_id, and a snippet of their reply. Pair with get_mail_thread to read the full reply and send_agent_mail (reply_to_message_id) to answer. To have YOU hear replies as they arrive instead, listen_to_events { event_types: ['email_campaign_replied'], filters: { email_campaign_replied: { campaign_id } } } — each one then reaches your chat with message_id and email_address_id for send_agent_mail.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes |
list_mail_addresses
Scope: agent_mail:read
List the email addresses assigned to this space (usable as campaign senders).
list_mail_campaigns
Scope: agent_mail:read
List email campaigns (broadcasts + drip sequences) in the space.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Filter: draft, scheduled, sending, sent, failed, canceled, active, paused, completed. | |
limit | number |
list_mail_domains
Scope: agent_mail:read
List this space's own mail domains (the ones it can have mailboxes on), whether each is ready to send, and the mailboxes already on each. Call this before asking for a new mailbox with create_mailbox.
list_mail_messages
Scope: agent_mail:read
Read individual emails with per-message tracking: engagement (delivered/opened/clicked/bounced), timestamps, bounce reason. Filter by thread_id (read a conversation's messages), campaign_id, direction ('inbound' = the agent's received mail), since (ISO), or search (subject/address). Set include_body:true for body text. THIS is how you answer 'did they open my email?' for 1:1 sends.
| Parameter | Type | Required | Description |
|---|---|---|---|
thread_id | string | Restrict to one mail thread (from list_mail_threads). | |
campaign_id | string | Restrict to one campaign's messages. | |
direction | inbound | outbound | ||
since | string | ISO timestamp — only messages created after this. | |
search | string | Match subject / to / from. | |
include_body | boolean | Include body_text (truncated to 4000 chars). | |
limit | number | Max rows (default 50, cap 200). | |
offset | number |
list_mail_suppressions
Scope: agent_mail:read
List the space-wide do-not-send list: addresses auto-suppressed after a hard bounce or spam complaint, plus manual adds. Every drip step and broadcast send skips these. Use for deliverability audits and pre-import list cleaning.
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | Filter by email substring. | |
limit | number |
list_mail_templates
Scope: agent_mail:read
List reusable email templates in the space.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number |
list_mail_threads
Scope: agent_mail:read
List inbound agent-mail conversations (the inbox) for the space, newest first. Use to triage incoming email; reply with send_agent_mail (reply_to_message_id from get_mail_thread).
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Filter by thread status; omit to exclude archived. | |
search | string | Match subject or sender. | |
limit | number |
list_mail_unsubscribes
Scope: agent_mail:read
Everyone who left the space's email, two ways: unsubscribes = stopped everything from a sending address (Stop all emails, a spam report — with the reason link_click · manual · spam_complaint and which sender), and campaign_unsubscribes = left ONE campaign or newsletter (the link in its email, the unsubscribe page, or taken off on request — with which campaign and how) and still get the others. Nobody can put either back; only the person can, from the unsubscribe page or by signing up again.
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | Filter by email substring. | |
limit | number |
list_message_templates
Scope: agent_mail:read
List the space's reusable message templates (email/SMS bodies from the Templates feature). Optionally filter by channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | sms | email |
list_my_campaigns
Scope: agent_mail:read
The email campaigns YOU are the agent for: what each is for (about) and whether you hear its replies (hears_replies — a listener of yours on email_campaign_replied). Start here when you want to know what you are responsible for — you are identified by your own key, so nothing needs passing in. Use list_mail_campaigns instead to see every campaign in the space.
list_newsletter_subscribers
Scope: agent_mail:read
A newsletter's people: tab 'subscribed' (with how they joined and since when), 'unsubscribed' (when, from which email, how — the link, a spam report, removed) or 'bounced'. Each row says how many of its emails they received and opened. counts gives all three totals.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
tab | subscribed | unsubscribed | bounced | ||
q | string | Find by name or email. | |
offset | number | ||
limit | number | Default 50, at most 200. |
list_signature_templates
Scope: agent_mail:read
List this space's signature TEMPLATES — arrangements you (or anyone) authored that render a mailbox's own details rather than replacing them. Also returns the token vocabulary a template may use. Prefer these over create_mail_signature when the owner wants "design my signature": a template follows their details, so changing a phone number updates every mailbox using it.
pause_campaign_for_person
Scope: agent_mail:run
Pause ONE person in an email campaign, until a date (they come back on their own and continue at the email they were waiting for) or until resume_campaign_for_person. Use for "not now, try me in March".
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | ||
email | string | ||
until | string | A future date, e.g. 2027-03-01. Leave out to pause until resumed. | |
reason | string |
queue_campaign_follow_up
Scope: agent_mail:run
Put recipients back in line for a step they have not received — this is what actually makes a follow-up send. A one-shot drip marks everyone 'completed' the moment its last email goes out, and the scheduler only looks at active enrollments, so a step added afterwards sends to NOBODY until this is called. Default audience 'not_replied' skips anyone who has written back (read from the actual mail, not from enrollment status, which a finished campaign never updates) and marks them replied when the campaign stops on reply. Nothing is bypassed: suppression, unsubscribes, send windows, daily caps and the reply-stop are all re-checked per person at send time, and a personalized step holds each recipient until their draft is approved. Use dry_run first to see the counts.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_id | string | yes | The step to queue them onto — usually the one add_campaign_step just returned. |
audience | not_replied | opened | clicked | all | not_replied (default) = everyone who never wrote back. opened / clicked = only those who engaged. 'all' still refuses to re-open anyone the campaign stopped for on reply. | |
contact_ids | string[] | Narrow it to these people (still filtered by audience). | |
delay_minutes | number | Minutes from now until the first one sends. Defaults to the step's own delay. | |
dry_run | boolean | Count them without changing anything. |
reorder_campaign_steps
Scope: agent_mail:write
Put a campaign's emails in a new order: step_ids = EVERY email of the campaign, first to last (ids from get_mail_campaign). Each email keeps its words, wait and per-person drafts. A custom campaign must not be running (pause it first); a sent newsletter email keeps its place. To add an email in the middle: add_campaign_step, then reorder.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_ids | string[] | yes |
reschedule_campaign_queue
Scope: agent_mail:run
Re-space everyone already queued on a drip using the campaign's CURRENT pacing (send window, daily cap, minimum gap, warm-up). Changing those settings does NOT re-time a queue that already exists — the new pace applies only to sends timed after the change, so without this the People tab keeps showing the old spacing and the scheduler drags the sends apart hours later. Order is preserved (first in line stays first) and the batch keeps its own start time, never pulled earlier than now, so this cannot accidentally send everything today. Pass start_at to move the whole batch. dry_run reports what would change. Returns the pacing multiplied out plus the resulting per-day spread.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
start_at | string | ISO timestamp — move the whole queue to start here instead of where it starts now. Never applied earlier than the present moment. | |
dry_run | boolean | Report the new spread without writing it. |
resume_campaign_for_person
Scope: agent_mail:run
Resume ONE paused person in an email campaign at the email they were waiting for, at the campaign's pace.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | ||
email | string |
run_mail_preflight
Scope: agent_mail:read
Run the deterministic pre-flight QA check (the SAME linter behind the UI's Pre-flight button — no LLM, no cost) on a template or a whole campaign. Catches dead/empty links on buttons and CTAs, missing image alt text, undeclared {{merge_tags}}, spammy/over-long subjects, Gmail-clip size, and a missing unsubscribe on bulk sends. campaign_id checks exactly what would send (every drip step, template or inline body, as bulk). Returns per-check score 0-100, status passed/warnings/failed, and a fixable issue list. For a campaign it also answers personal_fields: for each {{token}} that fills from people's custom fields (e.g. {{reason_line}}), how many of the campaign's people don't have it — they are taken out of the campaign at that email rather than sent a blank, so fill them in first (import_contacts updates by email). ALWAYS run this before send_mail_campaign.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | Check one template. | |
campaign_id | string | Check a campaign's full sendable content (all drip steps). | |
is_bulk | boolean | template_id only: lint as a bulk/campaign send (unsubscribe becomes required). Default false. |
send_agent_mail
Scope: agent_mail:write
Send an email from one of the space's agent-mail addresses.
| Parameter | Type | Required | Description |
|---|---|---|---|
from_address_id | string | yes | |
to | string,array | yes | |
cc | string,array | ||
bcc | string,array | ||
subject | string | yes | |
body_html | string | ||
body_text | string | ||
reply_to_message_id | string |
send_mail_campaign
Scope: agent_mail:run
Consequential: send/schedule a broadcast (scheduled_at defaults to now) or start a drip sequence. Requires a sender address, an audience, and content on the campaign. Best practice: run_mail_preflight and send_test_mail first. If get_mail_campaign shows approval_required=true, the campaign must be approved by a human first (submit_mail_campaign_for_review).
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
scheduled_at | string | ISO timestamp; omit to send ASAP. |
send_newsletter_email
Scope: agent_mail:run
Consequential: send ONE email of a NEWSLETTER (kind 'newsletter') to everyone subscribed right now — its audience minus anyone who unsubscribed or bounced — at the campaign's pace. Pass at to schedule it instead (ISO time), or unschedule: true to take a scheduled time back. Someone who subscribes after it goes gets the next email, not this one. A sent email is history: write the next one with add_campaign_step. Refused while the previous email is still going out.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_id | string | yes | The email (a step of the newsletter) to send. |
at | string | Schedule it for this ISO time instead of sending now. | |
unschedule | boolean | Take its scheduled time back. |
send_test_mail
Scope: agent_mail:run
Send a REAL test copy of an email to ONE address (subject prefixed '[Test]') so a human can eyeball it in a live inbox before the campaign goes out. Content source: template_id, campaign_id (+ step_position for a drip step, default 0), or inline body_html/body_text. Sender defaults to the campaign's sender, else the space's first assigned address. Merge tags are NOT rendered in tests.
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | yes | Single recipient email address for the test. |
template_id | string | ||
campaign_id | string | ||
step_position | number | Drip campaigns: which step to test (default 0). | |
subject | string | Override the resolved subject. | |
body_html | string | Inline content instead of template/campaign. | |
body_text | string | ||
from_address_id | string |
set_mail_address_daily_limit
Scope: agent_mail:write
Set or clear the per-address daily send ceiling (deliverability guard shared across ALL campaigns sending from that address; enforced by the scheduler and the rotation picker). Pass daily_send_limit=null to remove. Use list_mail_addresses for ids + current limits.
| Parameter | Type | Required | Description |
|---|---|---|---|
address_id | string | yes | |
daily_send_limit | number,null | Max emails/day from this address. null = unlimited. |
skip_campaign_drafts
Scope: agent_mail:write
Mark personalized draft(s) 'skipped' — those contacts get the plain step template instead of an AI-written email. Pass draft_id for one, or campaign_id (optionally step_id) to skip many.
| Parameter | Type | Required | Description |
|---|---|---|---|
draft_id | string | ||
campaign_id | string | ||
step_id | string |
stop_campaign_for_person
Scope: agent_mail:run
Stop ONE person in an email campaign: no more emails from it for them (everyone else carries on). Use when you took the conversation over, they said no, or they bought. Put them back later with add_people_to_campaign.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | ||
email | string | ||
reason | string | One line the campaign's People view shows, e.g. "booked a call". |
submit_mail_campaign_for_review
Scope: agent_mail:write
Submit a campaign for human approval (approval_status='pending'), or withdraw it back to unreviewed. Use when get_mail_campaign shows approval_required=true. Approving/rejecting is owner/admin-only in the UI — an agent can only submit.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
action | submit | withdraw |
suppress_mail_address
Scope: agent_mail:write
Manually add an email address to the space-wide do-not-send list (reason 'manual'). Use to pre-clean an imported lead list — known-bad addresses never get sent to, protecting sender reputation.
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | yes | |
detail | string | Optional note on why (shown in audits). |
unsubscribe_from_campaign
Scope: agent_mail:run
Take ONE person off an email campaign (any kind) because they ASKED — "stop emailing me" in a reply, a call, a message. Unlike stop_campaign_for_person this is their choice, kept like the link in the email: nobody can put them back in; only they can, from the unsubscribe page or by signing up again. scope 'campaign' (default) = this campaign only; 'everything' = every campaign and newsletter the space sends (Stop all emails). Receipts and answers to their own messages still reach them. It lands in their consent history under your name.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | ||
email | string | ||
scope | campaign | everything |
unsubscribe_newsletter_subscriber
Scope: agent_mail:run
Take someone off a newsletter because they asked (a reply saying "stop", a message elsewhere). scope 'campaign' (default) = this newsletter only — the space's other newsletters keep coming, like the link at the foot of each email does; scope 'everything' = every email the space sends in bulk (campaigns, newsletters, automations), the same as "Stop all emails" on the unsubscribe page. Receipts and answers to their own messages still reach them. It lands in their consent history under your name.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
contact_id | string | yes | |
scope | campaign | everything |
unsuppress_mail_address
Scope: agent_mail:write
Remove an address from the space-wide do-not-send list (e.g. a mis-flagged temporary bounce). The address becomes sendable again — do NOT use this for spam complaints unless the owner asked.
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | yes |
update_brand_footer
Scope: agent_mail:write
Update the space's legal email footer: enable it, set the fine print, and choose whether the unsubscribe line is included. Passing address or social_links here writes them to the space BRAND RECORD (they are brand, not mail — the same address a page or an invoice uses) — prefer update_space_brand for those.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | boolean | ||
address | string | Postal mailing address. Saved on the space brand record — the one address every surface reads. | |
fine_print | string | ||
show_unsubscribe | boolean | ||
social_links | object[] | [{ platform, url }] — platform: facebook|x|twitter|instagram|youtube|linkedin|tiktok|website|email. | |
show_logo | boolean | ||
dark_band | boolean | Render the footer on a dark band. |
update_campaign_draft
Scope: agent_mail:write
Edit one recipient's personalized draft (subject and/or body_html). Marks it 'edited' — still needs approval before it sends. Use list_campaign_drafts for draft ids.
| Parameter | Type | Required | Description |
|---|---|---|---|
draft_id | string | yes | |
subject | string | ||
body_html | string |
update_campaign_step
Scope: agent_mail:write
Edit ONE step of a drip in place (subject, body, delay, template, personalization, sender override). The step keeps its id, so the per-recipient drafts already written for it survive. Only the fields you pass change. Step ids come from get_mail_campaign.
| Parameter | Type | Required | Description |
|---|---|---|---|
step_id | string | yes | |
subject | string | ||
body_text | string | ||
body_html | string | ||
template_id | string,null | ||
delay_minutes | number | ||
from_address_override | string,null | ||
personalization_enabled | boolean | When true, each recipient gets a unique AI-written email for this step. The step's subject/body become the base template the AI rewrites per contact. | |
personalization_prompt | string | What you should know about THIS email — both how to write it and how to handle someone who answers it (e.g. 'first touch; lead with the finding for their building. If they ask for the one-pager, send it. If they ask price, hand it to the owner.'). Sent to you with every reply to this email, and where it differs from your usual way of working, it wins. Applies whether or not personalization_enabled is set. |
update_designed_mail_template
Scope: agent_mail:write
Edit a designer-grade template and recompile. Update name/subject/description/category/theme, and/or pass blocks to REPLACE the entire block tree (read the current tree first with get_mail_template_blocks, modify, send the full new tree). Same block vocabulary as create_designed_mail_template.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes | |
name | string | ||
subject | string | ||
description | string | ||
category | string | ||
theme | object | ||
blocks | object[] | If provided, REPLACES the whole block tree. |
update_mail_campaign
Scope: agent_mail:write
Edit a draft/paused/scheduled email campaign. Sending controls (daily_send_cap, send_window_, min_send_gap_minutes, frequency_cap_hours, stop_on_reply/click, sender_rotation_ids, warmup_) and the agent controls (agent_bot_id, personalization_review, description) can ALSO be tuned while the campaign is active. DO NOT pass steps to add or edit one email — it REPLACES the whole sequence, which deletes every per-recipient draft ever written for this campaign (refused outright once drafts exist). Use add_campaign_step / update_campaign_step instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
name | string | ||
kind | newsletter | custom | Switch the kind — only before anything was sent. | |
newcomers | hold | first_email | custom: someone added later is held (hold) or starts at email 1. | |
welcome_on | boolean | newsletter: the welcome email on or off. | |
on_preferences | boolean | newsletter: listed on the unsubscribe page, where a reader switches each newsletter on or off (default true). | |
public_blurb | string,null | newsletter: the one line a reader sees under its name on the unsubscribe page (≤200), e.g. "Every Friday: three things we learned". | |
subject | string | ||
body_text | string | ||
body_html | string | ||
from_address_id | string | ||
audience_id | string | ||
template_id | string | ||
enrollment_mode | one_shot | continuous | ||
trigger_audience_id | string | ||
scheduled_at | string | ||
steps | object[] | REPLACES every step — only for a campaign that has no per-recipient drafts yet. To append or edit one email, use add_campaign_step / update_campaign_step. Same shape as create_mail_campaign's steps. | |
is_outreach | boolean | Cold-email preset marker (drips): defaults stop_on_reply=true, daily_send_cap=50, 9-17 Mon-Fri send window unless overridden. | |
daily_send_cap | number,null | Max sends/day for this campaign. null = unlimited. | |
send_window_start_hour | number,null | Send window start hour 0-23 (campaign timezone). | |
send_window_end_hour | number,null | Send window end hour 1-24. | |
send_window_days | array,null | Allowed weekdays 0=Sun…6=Sat. null/[] = every day. | |
send_window_timezone | string,null | IANA timezone for the send window (defaults to the space timezone). | |
frequency_cap_hours | number | Min hours between any two emails to the same contact, across ALL campaigns. 0 = off. NOT the spacing of this campaign's own sends — that is min_send_gap_minutes (between people) and each step's delay (between emails to one person). | |
min_send_gap_minutes | number,null | Minimum minutes between two sends of THIS campaign — 'one every 15 minutes'. null = spacing is whatever daily_send_cap works out to across the send window. Works with no window too. When both apply, the tighter of the gap and the cap wins. | |
stop_on_reply | boolean | Stop the campaign for a contact once they reply. Every campaign comes with it ON: it is the automation "Someone replies → stop it for them" (made for the campaign, visible in Automations). false switches that automation off. | |
stop_on_click | boolean | Stop the campaign for a contact once they click a link — an automation made for the campaign, like stop_on_reply. | |
description | string,null | What this campaign is for, in a few sentences (who it is for, the offer, the goal — e.g. 'Offers our LL97 audit to NYC building owners; every reply should lead to a 20-minute call.'). Read by whoever writes its emails or answers its replies. The RULES — what may be promised, what goes to the owner, which template answers what — belong in the running agent's own instructions, not here; a single email's specifics go in that step's personalization_prompt. Changeable while the campaign runs. | |
sender_rotation_ids | array,null | Pool of sending-address ids rotated per send. Each send goes to whichever mailbox has used the least of its OWN daily allowance, so a small or newly-warming address takes its share instead of waiting for a bigger one to drain; when nothing has a stated limit it simply round-robins. Set the ceilings with set_mail_address_daily_limit. Overrides from_address_id for drip sends, and a step's from_address_override overrides this. | |
warmup_enabled | boolean | Gradually ramp the daily cap for new senders. | |
warmup_start_cap | number | Warmup day-1 daily cap (default 10). | |
warmup_daily_increment | number | How much the warmup cap grows per day (default 5). | |
personalization_review | none | owner | any | For AI-personalized steps, who APPROVES each recipient's draft: 'none' = auto-send (hands-off), 'owner' = a human must approve in the app, 'any' = you (the agent) may approve too. Default 'owner'. | |
agent_bot_id | string,null | Which MACHINE AGENT in this space runs this campaign — writes each person's email when a step is personalized. Any engine (hermes, claude_code, codex, grok, opencode, dsh). Call whoami for your own agent_id, or list_agents for the other agents in this space (use their agent_id, NOT assigned_to_workspace_id). Changeable at any time: the new agent reads the description and each email's note, and works by its own instructions. NULL = none picked, which resolves to the space's single active agent and refuses when there are several. |
update_mail_template
Scope: agent_mail:write
Edit an email template (name, subject, bodies, category, variables).
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes | |
name | string | ||
subject | string | ||
body_html | string | ||
body_text | string | ||
description | string | ||
category | string | ||
variables | object[] |
update_mailbox_signature
Scope: agent_mail:write
Set a sending address's signature details and how they are rendered — this is the tool for "change my phone number in my signature" or "use the narrow template on my mailbox". Only the fields you pass change. Pass layout for a built-in arrangement (stacked | beside | compact) or template_id for one of this space's templates; a template wins when set, and passing template_id: null returns the mailbox to its layout. Address parts are kept separate (street/city/region/postal_code/country) because the country decides their ORDER — a French address prints its postcode before the city. show_country hides the country name without changing that ordering. Writing any of this clears an assigned library signature, since details and a finished signature cannot both be what the address sends.
| Parameter | Type | Required | Description |
|---|---|---|---|
address_id | string | yes | |
enabled | boolean | Whether this address signs its plain emails at all. | |
fullName | string | ||
title | string | ||
company | string | ||
phone | string | Include the country code, e.g. +1 646 503 7082. | |
email | string | Contact address shown in the sign-off. | |
website | string | ||
street | string | ||
city | string | ||
region | string | State, province or county. | |
postalCode | string | ||
country | string | Country NAME, e.g. 'United States', 'France', 'Japan'. Decides the address ordering. | |
showCountry | boolean | ||
layout | stacked | beside | compact | ||
template_id | string | A signature template id, or null to go back to the layout. | |
imageUrl | string | Hosted URL of a photo or logo. Data URIs do not render in email. | |
imageShape | round | square |
update_message_template
Scope: agent_mail:write
Edit a space message template (name, channel, subject, body, variables).
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes | |
name | string | ||
channel | sms | email | ||
subject | string | ||
body | string | ||
variables | object[] |
update_signature_template
Scope: agent_mail:write
Edit a signature template. Every mailbox using it is re-rendered immediately, so the change reaches the addresses that adopted it instead of leaving them on the old markup.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | yes | |
name | string | ||
html | string | ||
description | string |
whoami
Scope: agent_mail:read
Who you are in this space, resolved from your own key: your agent_id, name, engine and status. agent_id is what mail_campaigns.agent_bot_id points at — call this when you need to assign yourself something. (list_agents returns a WORKSPACE id, which is for assigning tasks and is a different id.) Returns is_agent=false if this key belongs to a person rather than a machine agent.
write_campaign_draft
Scope: agent_mail:write
Write one recipient's personalized draft YOURSELF — you compose the subject and body_html with your own model, so nothing is billed to the space wallet (unlike generate_campaign_drafts, which has the platform write it). Upserts by (campaign_id, step_id, contact_id): use it to fill missing recipients, replace failed ones, or supply your own copy for a pending draft. The step must have personalization_enabled. Drafts already approved/skipped are only replaced with overwrite=true. Review mode still applies: 'none' auto-approves; otherwise the draft waits in the review queue (approve_campaign_drafts). Body is an HTML fragment (<p>, <br>, <a>, <strong>, <em>) — write final copy, no signature/unsubscribe footer (the platform appends those); merge tags like {{first_name}} are still filled at send time if you use them. Use list_campaign_recipients for contact ids and list_campaign_drafts (status filter) to see who still needs one.
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | string | yes | |
step_id | string | yes | A personalized step of this campaign. |
contact_id | string | yes | |
subject | string | yes | |
body_html | string | yes | HTML fragment — the final email body for this one recipient. |
overwrite | boolean | Also replace a draft that's already approved or skipped. |