These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

Mail

Email on inbox.sails.app. Bodies live under .sails/threads/. Inbox and sent stubs point at the thread. Two mailbox kinds:

KindCreateInboundFolder
Agentagent create plus optional agent claimStarts a background goal (events/email.received.md)agents/{id}/mail/
Folder inboxinbox create USERNAMEStores stubs only. No goalinboxes/{username}/mail/

MCP clients that should read and send themselves use a folder inbox. Agents use mail when inbound should wake a goal with a soul.

Done looks like

Outbound: mail send succeeds. The recipient gets one HTML email from markdown --text (plain-text fallback kept). With --thread, it is the same conversation in Sails and in Gmail.

Inbound reply from Gmail: it lands in the same Sails thread as your send, because Sails matches the reply's In-Reply-To header to the Message-ID it stamped on the way out.

Inbound to an agent: stub under agents/{id}/mail/inbox/, then a background goal as that agent. First tool call is thread show for that thread.

Inbound to a folder inbox: stub under inboxes/{username}/mail/inbox/ only. No goal.

Worked example: folder inbox for an MCP client

Job: claim helpdesk, send one note, find the reply, hydrate, answer in the same thread.

inbox create helpdesk
# {org}-helpdesk@inbox.sails.app
# or helpdesk@inbox.sails.app when your org owns the mail domain
# folder: inboxes/helpdesk/mail/inbox/

mail send --as helpdesk --to user@example.com --subject "Hi" --text "Hello **world**"

thread list --as helpdesk                       # find the THREAD_ID
thread show THREAD_ID --limit 5 --as helpdesk   # last 5 messages, full bodies
mail send --as helpdesk --to user@example.com --thread THREAD_ID --subject "Hi" --text "Following up."

inbox create, inbox claim-bare, and inbox rm are repository owner only. Folder inboxes require --as (su only works for agents). Owners may pass --as AGENT|INBOX without su.

Agent send (owner, no su):

mail send --as site-bot --to you@example.com --subject "Site update" --text "Shipped the **subscribe form**."

In a background goal running as the agent, prefer the mail_send tool (to, subject, text, thread_id). Bash mail send works when workspace commands are wired.

What the output looks like

inbox show reed on an org that owns the domain (real output, ids from a live repo):

{
  "address": "01a1500b-6815-7615-a3fe-1a9d2fd1bc6e@inbox.sails.app",
  "alias_address": "ryan-reed@inbox.sails.app",
  "claimed_address": "reed@inbox.sails.app",
  "kind": "mailbox",
  "mail_bare": true,
  "mail_inbox": "inboxes/reed/mail/inbox",
  "mail_username": "reed",
  "name": "reed",
  "note": "Inbound mail is stored here; no agent goal is started. Hydrate with thread show."
}

mail_bare: true means From is the bare address. alias_address is the old prefixed form, still accepted inbound. A prefixed inbox shows mail_bare: false and alias_address: null.

mail inbox --as reed lists stub file names, not bodies:

{
  "inbox": "/ryan/home/inboxes/reed/mail/inbox",
  "kind": "mailbox",
  "messages": ["698711d9-5b19-4894-82ab-2d9f02e5a9ab.md"],
  "note": "Use thread show THREAD_ID to hydrate full history"
}

thread show returns thread_id, subject, and messages[], each with from_address, to_address, subject, body, created_at, and in_reply_to.

Addresses

FormMeaning
{uuid}@inbox.sails.appAgent or folder inbox UUID. Always works
{org}-{username}@inbox.sails.appAfter agent claim USERNAME or inbox create USERNAME
{username}@inbox.sails.appSame commands, when your org owns the mail domain

Outbound From prefers the claim when set. Claims are unique per org across agents and folder inboxes. Bare names are unique per domain, cannot collide with an existing {org}-{username} address, and cannot be a reserved local-part (postmaster, abuse, support, and similar). Do not publish inboxes/ as a site root.

Bare addresses for domain owners

An org owns the mail domain when it is the instance operator, or when it has a verified published hostname that is the mail domain or a DNS parent of it (sails.app owns inbox.sails.app). That org gets the exact local-part:

inbox create reed
# reed@inbox.sails.app

An inbox claimed before the org owned the domain stays prefixed until you promote it:

inbox claim-bare reed
# or by id: inbox claim-bare 01a1500b-6815-7615-a3fe-1a9d2fd1bc6e

After that, mail send --as reed sends From: reed@inbox.sails.app, and {org}-reed@inbox.sails.app keeps delivering as an inbound alias. Other orgs cannot reserve a bare name on a domain they do not own; their claims stay {org}-USERNAME@.

Threading

Always pass --thread when replying. Without it, the send is a new conversation. Subject is prefixed with Re: when threading and the subject does not already start with Re:.

Every outbound send sets Message-ID: <{message_uuid}@{domain}>. When the recipient replies, Gmail sends In-Reply-To and References, and Sails files the reply into the thread whose stored Message-ID matches.

Sends made before Message-IDs were stored have none, so a reply to one of them opens a new Sails thread (you will see two threads with the same subject, one starting Re:). Sails repairs these on its own when an inbound lookup misses (this repository, up to 25 rows). To repair a mailbox in one pass:

mail backfill-rfc --as helpdesk

The inbox stub is not the full conversation. Call thread show THREAD_ID before you decide or reply. Inbound agent goals prepend: first tool call should be thread_show for that id.

Commands

Live usage (mail --help, inbox --help, thread --help):

mail send --to ADDR [--subject S] [--thread ID] [--text BODY | --html-file PATH] [--as AGENT|INBOX]
mail inbox [--as AGENT|INBOX]
mail show PATH|ID [--as AGENT|INBOX]
mail backfill-rfc [--as AGENT|INBOX]

thread show THREAD_ID [--limit N] [--as AGENT|INBOX]
thread list [--as AGENT|INBOX]

inbox create USERNAME
inbox list
inbox show USERNAME|ID
inbox claim-bare USERNAME|ID
inbox rm USERNAME|ID
Flag / commandWhat it does
--toRecipient. Required for mail send
--subjectSubject line
--textMarkdown body (default). Prefer this over HTML
--html-file PATHUse only when you already have HTML
--thread IDContinue a conversation; required for replies
--as AGENT or --as INBOXOwner acts as that mailbox without su. Required for folder inboxes
mail inboxList inbox stub file names
mail show PATH|IDOne stub by path, or by id under mail/inbox/
mail backfill-rfcFill missing Message-IDs on older sends so replies thread
thread listEvery thread the mailbox is in, with id and subject
thread show ID --limit NFull history (or the last N messages) before you reply
inbox createClaim a folder inbox (owner only)
inbox claim-barePromote a prefixed claim to the bare address (domain owners)

Delivery

Same-domain addresses on inbox.sails.app deliver locally (agent and folder inboxes). External addresses go through the platform (Resend).

Agent and folder inboxes are platform-managed. Do not webhook add for {uuid}@inbox.sails.app. Use Start a goal from a webhook only when connecting your own email provider.

Failure modes

CLI errors are copied from live runs.

What you seeWhat it meansWhat to do
mail: run as an agent (su AGENT) or pass --as ...No mailbox chosenAdd --as NAME, or su AGENT first
su: inboxes are not agents; use mail --as USERNAMEYou tried su on a folder inboxmail ... --as USERNAME
mail send: --to is requiredMissing recipientAdd --to ADDR
inbox not found: NAMENo folder inbox by that name or idinbox list for the real names
thread show: invalid THREAD_IDNot a thread UUIDCopy id from thread list
mail show: ENOENT ... mail/inbox/ID.mdThat stub id is not in this mailboxUse a name from mail inbox, or pass the full path
Reply opens a second thread starting Re:Your send had no stored Message-ID, or you omitted --threadmail backfill-rfc --as NAME; always reply with --thread
Goal acts on a stub and gets it wrongStub is not the bodyFirst call: thread show THREAD_ID
Mail to name@inbox.sails.app never arrivesNo claim, wrong local-part, or bare form on an org that does not own the domainUse {uuid}@inbox.sails.app or the claimed_address from inbox show
inbox create helpdesk failsUsername already claimed by an agent or another inbox, or reservedPick another username
You used webhook add for agent mailWrong ingressPlatform mail is not a custom webhook
Same-repo agent never got the letterYou used an external addressSend to {uuid or claim}@inbox.sails.app for local delivery

Agent-to-agent inside the repo without email is message send, not mail send.

How to: Mail from an agent.