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).

ParameterTypeRequiredDescription
campaign_idstringyes
is_welcomebooleannewsletter: this is its welcome email (one per newsletter), sent once to each new subscriber when welcome_on.
subjectstringMerge tags allowed ({{first_name}}...). On a personalized step this is the fallback for anyone without a draft.
body_textstring
body_htmlstring
template_idstring
delay_minutesnumberWait after the previous step. Default 1440 (one day).
from_address_overridestring,nullForce 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_enabledbooleanWhen 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_promptstringWhat 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).

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idsstring[]
emailsstring[]Email addresses of contacts in this space.
start_atnumberThe email number to begin at (1 = the first). Default 1.
start_onstringA 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.

ParameterTypeRequiredDescription
draft_idstring
campaign_idstringApprove 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.

ParameterTypeRequiredDescription
campaign_idstringyes
actionpause | resume | cancelyes

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.

ParameterTypeRequiredDescription
namestringyes
subjectstring
descriptionstring
categorystring
themeobjectWhole-email theme (see description).
blocksobject[]yesOrdered 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.

ParameterTypeRequiredDescription
namestringyes
kindnewsletter | customnewsletter = each email goes to everyone subscribed when it is sent; custom = emails written per person.
newcomershold | first_emailcustom + continuous: someone added later is held until someone picks where they start (default) or starts at email 1.
welcome_onbooleannewsletter: send the welcome email to each new subscriber, once.
welcomeobjectnewsletter: the welcome email each new subscriber gets.
campaign_typebroadcast | dripOld behaviour, only when kind is omitted.
subjectstring
body_textstring
body_htmlstring
from_address_idstringOptional — campaign stays draft-only until set.
audience_idstring
template_idstring
enrollment_modeone_shot | continuous
trigger_audience_idstringContinuous drips: audience to keep enrolling from.
stepsobject[]Drip steps in order.
is_outreachbooleanCold-email preset marker (drips): defaults stop_on_reply=true, daily_send_cap=50, 9-17 Mon-Fri send window unless overridden.
daily_send_capnumber,nullMax sends/day for this campaign. null = unlimited.
send_window_start_hournumber,nullSend window start hour 0-23 (campaign timezone).
send_window_end_hournumber,nullSend window end hour 1-24.
send_window_daysarray,nullAllowed weekdays 0=Sun…6=Sat. null/[] = every day.
send_window_timezonestring,nullIANA timezone for the send window (defaults to the space timezone).
frequency_cap_hoursnumberMin 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_minutesnumber,nullMinimum 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_replybooleanStop 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_clickbooleanStop the campaign for a contact once they click a link — an automation made for the campaign, like stop_on_reply.
descriptionstring,nullWhat 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_idsarray,nullPool 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_enabledbooleanGradually ramp the daily cap for new senders.
warmup_start_capnumberWarmup day-1 daily cap (default 10).
warmup_daily_incrementnumberHow much the warmup cap grows per day (default 5).
personalization_reviewnone | owner | anyFor 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_idstring,nullWhich 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}}.

ParameterTypeRequiredDescription
namestringyes
kindhtml | plain_text
subjectstring
body_htmlstringRequired for kind 'html'.
body_textstringRequired for kind 'plain_text'.
descriptionstring
categorystring
variablesobject[]

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.

ParameterTypeRequiredDescription
email_addressstringThe whole address, e.g. sales@acme.com. Or give name + domain.
namestringThe part before the @ (when email_address is not given).
domainstringThe domain (when email_address is not given), one from list_mail_domains.
display_namestringThe name people see beside the address, e.g. "Acme Sales".
daily_send_limitnumber,nullMax 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.

ParameterTypeRequiredDescription
namestringyes
channelsms | emailyes
subjectstringEmail subject (email channel only).
bodystring
variablesobject[][{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.

ParameterTypeRequiredDescription
namestringyesShown in the picker, e.g. 'Narrow — logo left'.
htmlstringyesInline-styled HTML fragment with {{tokens}}.
descriptionstringOptional 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.

ParameterTypeRequiredDescription
campaign_idstring
step_idstringyes

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.

ParameterTypeRequiredDescription
campaign_idstringyes

delete_message_template

Scope: agent_mail:write

Delete a space message template.

ParameterTypeRequiredDescription
template_idstringyes

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.

ParameterTypeRequiredDescription
template_idstringyes

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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idstringOnly generate for this step (default: all personalized steps).
contact_idsstring[]Only (re)generate for these contacts.
regeneratebooleanAlso redo existing non-approved drafts (generated/edited/failed).
limitnumberMax pairs this call (default 15, max 50).

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.

ParameterTypeRequiredDescription
campaign_idstringyesFrom 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.

ParameterTypeRequiredDescription
campaign_idstringyes

get_mail_template

Scope: agent_mail:read

Get one email template including its body.

ParameterTypeRequiredDescription
template_idstringyes

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.

ParameterTypeRequiredDescription
template_idstringyes

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).

ParameterTypeRequiredDescription
thread_idstringyes
message_limitnumber

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.

ParameterTypeRequiredDescription
address_idstringyes

get_message_template

Scope: agent_mail:read

Get one space message template including its body and variables.

ParameterTypeRequiredDescription
template_idstringyes

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.

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstringyes

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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idstring
statusgenerated | edited | approved | skipped | failed
limitnumber

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.

ParameterTypeRequiredDescription
campaign_idstringyesFrom list_mail_campaigns.
metricall | sent | delivered | opened | clicked | replied | unsubscribed | not_arrivedWhich number: all (everyone in it) · sent · delivered · opened · clicked · replied · unsubscribed · not_arrived (bounced or refused). Default all.
email_numbernumberOne email of the campaign (1 = the first). Leave out for any of them — e.g. metric opened + email_number 2 = who opened email 2.
querystringNarrow by name, email or company.
offsetnumber
limitnumberUp 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.

ParameterTypeRequiredDescription
campaign_idstringyesFrom list_mail_campaigns.
statusactive | completed | unsubscribed | failed | paused | stopped | repliedEnrollment 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.
repliedbooleantrue = only people who wrote back. false = only those who never did — the follow-up list.
step_idstringAlso return each person's draft_status for this step, so you can see who still needs one written.
limitnumberMax rows (default 50, cap 200).
offsetnumber

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.

ParameterTypeRequiredDescription
campaign_idstringyes

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.

ParameterTypeRequiredDescription
statusstringFilter: draft, scheduled, sending, sent, failed, canceled, active, paused, completed.
limitnumber

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.

ParameterTypeRequiredDescription
thread_idstringRestrict to one mail thread (from list_mail_threads).
campaign_idstringRestrict to one campaign's messages.
directioninbound | outbound
sincestringISO timestamp — only messages created after this.
searchstringMatch subject / to / from.
include_bodybooleanInclude body_text (truncated to 4000 chars).
limitnumberMax rows (default 50, cap 200).
offsetnumber

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.

ParameterTypeRequiredDescription
searchstringFilter by email substring.
limitnumber

list_mail_templates

Scope: agent_mail:read

List reusable email templates in the space.

ParameterTypeRequiredDescription
limitnumber

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).

ParameterTypeRequiredDescription
statusstringFilter by thread status; omit to exclude archived.
searchstringMatch subject or sender.
limitnumber

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.

ParameterTypeRequiredDescription
searchstringFilter by email substring.
limitnumber

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.

ParameterTypeRequiredDescription
channelsms | 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.

ParameterTypeRequiredDescription
campaign_idstringyes
tabsubscribed | unsubscribed | bounced
qstringFind by name or email.
offsetnumber
limitnumberDefault 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".

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstring
emailstring
untilstringA future date, e.g. 2027-03-01. Leave out to pause until resumed.
reasonstring

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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idstringyesThe step to queue them onto — usually the one add_campaign_step just returned.
audiencenot_replied | opened | clicked | allnot_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_idsstring[]Narrow it to these people (still filtered by audience).
delay_minutesnumberMinutes from now until the first one sends. Defaults to the step's own delay.
dry_runbooleanCount 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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idsstring[]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.

ParameterTypeRequiredDescription
campaign_idstringyes
start_atstringISO timestamp — move the whole queue to start here instead of where it starts now. Never applied earlier than the present moment.
dry_runbooleanReport 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.

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstring
emailstring

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.

ParameterTypeRequiredDescription
template_idstringCheck one template.
campaign_idstringCheck a campaign's full sendable content (all drip steps).
is_bulkbooleantemplate_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.

ParameterTypeRequiredDescription
from_address_idstringyes
tostring,arrayyes
ccstring,array
bccstring,array
subjectstringyes
body_htmlstring
body_textstring
reply_to_message_idstring

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).

ParameterTypeRequiredDescription
campaign_idstringyes
scheduled_atstringISO 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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idstringyesThe email (a step of the newsletter) to send.
atstringSchedule it for this ISO time instead of sending now.
unschedulebooleanTake 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.

ParameterTypeRequiredDescription
tostringyesSingle recipient email address for the test.
template_idstring
campaign_idstring
step_positionnumberDrip campaigns: which step to test (default 0).
subjectstringOverride the resolved subject.
body_htmlstringInline content instead of template/campaign.
body_textstring
from_address_idstring

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.

ParameterTypeRequiredDescription
address_idstringyes
daily_send_limitnumber,nullMax 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.

ParameterTypeRequiredDescription
draft_idstring
campaign_idstring
step_idstring

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.

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstring
emailstring
reasonstringOne 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.

ParameterTypeRequiredDescription
campaign_idstringyes
actionsubmit | 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.

ParameterTypeRequiredDescription
emailstringyes
detailstringOptional 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.

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstring
emailstring
scopecampaign | 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.

ParameterTypeRequiredDescription
campaign_idstringyes
contact_idstringyes
scopecampaign | 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.

ParameterTypeRequiredDescription
emailstringyes

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.

ParameterTypeRequiredDescription
enabledboolean
addressstringPostal mailing address. Saved on the space brand record — the one address every surface reads.
fine_printstring
show_unsubscribeboolean
social_linksobject[][{ platform, url }] — platform: facebook|x|twitter|instagram|youtube|linkedin|tiktok|website|email.
show_logoboolean
dark_bandbooleanRender 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.

ParameterTypeRequiredDescription
draft_idstringyes
subjectstring
body_htmlstring

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.

ParameterTypeRequiredDescription
step_idstringyes
subjectstring
body_textstring
body_htmlstring
template_idstring,null
delay_minutesnumber
from_address_overridestring,null
personalization_enabledbooleanWhen 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_promptstringWhat 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.

ParameterTypeRequiredDescription
template_idstringyes
namestring
subjectstring
descriptionstring
categorystring
themeobject
blocksobject[]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.

ParameterTypeRequiredDescription
campaign_idstringyes
namestring
kindnewsletter | customSwitch the kind — only before anything was sent.
newcomershold | first_emailcustom: someone added later is held (hold) or starts at email 1.
welcome_onbooleannewsletter: the welcome email on or off.
on_preferencesbooleannewsletter: listed on the unsubscribe page, where a reader switches each newsletter on or off (default true).
public_blurbstring,nullnewsletter: the one line a reader sees under its name on the unsubscribe page (≤200), e.g. "Every Friday: three things we learned".
subjectstring
body_textstring
body_htmlstring
from_address_idstring
audience_idstring
template_idstring
enrollment_modeone_shot | continuous
trigger_audience_idstring
scheduled_atstring
stepsobject[]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_outreachbooleanCold-email preset marker (drips): defaults stop_on_reply=true, daily_send_cap=50, 9-17 Mon-Fri send window unless overridden.
daily_send_capnumber,nullMax sends/day for this campaign. null = unlimited.
send_window_start_hournumber,nullSend window start hour 0-23 (campaign timezone).
send_window_end_hournumber,nullSend window end hour 1-24.
send_window_daysarray,nullAllowed weekdays 0=Sun…6=Sat. null/[] = every day.
send_window_timezonestring,nullIANA timezone for the send window (defaults to the space timezone).
frequency_cap_hoursnumberMin 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_minutesnumber,nullMinimum 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_replybooleanStop 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_clickbooleanStop the campaign for a contact once they click a link — an automation made for the campaign, like stop_on_reply.
descriptionstring,nullWhat 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_idsarray,nullPool 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_enabledbooleanGradually ramp the daily cap for new senders.
warmup_start_capnumberWarmup day-1 daily cap (default 10).
warmup_daily_incrementnumberHow much the warmup cap grows per day (default 5).
personalization_reviewnone | owner | anyFor 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_idstring,nullWhich 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).

ParameterTypeRequiredDescription
template_idstringyes
namestring
subjectstring
body_htmlstring
body_textstring
descriptionstring
categorystring
variablesobject[]

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.

ParameterTypeRequiredDescription
address_idstringyes
enabledbooleanWhether this address signs its plain emails at all.
fullNamestring
titlestring
companystring
phonestringInclude the country code, e.g. +1 646 503 7082.
emailstringContact address shown in the sign-off.
websitestring
streetstring
citystring
regionstringState, province or county.
postalCodestring
countrystringCountry NAME, e.g. 'United States', 'France', 'Japan'. Decides the address ordering.
showCountryboolean
layoutstacked | beside | compact
template_idstringA signature template id, or null to go back to the layout.
imageUrlstringHosted URL of a photo or logo. Data URIs do not render in email.
imageShaperound | square

update_message_template

Scope: agent_mail:write

Edit a space message template (name, channel, subject, body, variables).

ParameterTypeRequiredDescription
template_idstringyes
namestring
channelsms | email
subjectstring
bodystring
variablesobject[]

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.

ParameterTypeRequiredDescription
template_idstringyes
namestring
htmlstring
descriptionstring

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.

ParameterTypeRequiredDescription
campaign_idstringyes
step_idstringyesA personalized step of this campaign.
contact_idstringyes
subjectstringyes
body_htmlstringyesHTML fragment — the final email body for this one recipient.
overwritebooleanAlso replace a draft that's already approved or skipped.

On this page