references/examples/recipes.md
# Mailbox recipes
Copy-paste starting points. Replace `<…>` placeholders with real UUIDs. Two rules run through
all of them: **price the fleet before you provision it** (a mailbox bills monthly, forever), and
**run the three checks before the first send** — basis, suppression, relevance
([`../../../cargo-gtm/references/acceptable-use.md`](../../../cargo-gtm/references/acceptable-use.md) §3).
See [`../warmup-and-allowance.md`](../warmup-and-allowance.md) for the ramp and
[`../sending.md`](../sending.md) for the send itself.
---
## 1. Price a fleet before provisioning it
The conversation that has to happen first. Nothing here spends anything.
```bash
cargo-ai mailboxManagement pricing get
# → {"monthlyCredits":{"google":125,"outlook":160,"shared":100,"private":100}}
cargo-ai mailboxManagement mailbox list | jq '.mailboxes | length' # what already exists
cargo-ai billing subscription get # what is left to spend
```
Then say it out loud, in this shape: *"3 google mailboxes is 375 credits **per month**, and they
reach 120 sends/day only after 45 days of warm-up — 15/day next week. Provisioning is not a
one-off charge and `mailbox remove` is the only way to stop it. Go ahead?"*
Steady-state capacity is `40 × mailboxes`. If the volume target is bigger than the qualified
audience justifies, the answer is a smaller audience, not a bigger fleet.
---
## 2. Provision one mailbox and wait for `active`
```bash
# The domain UUID does not come from the CLI — web app, or CDK defineDomain + cargo.state.json
cargo-ai mailboxManagement mailbox create \
--domain-uuid <domain-uuid> \
--type google \
--username jane \
--first-name Jane --last-name Doe \
--signature '<p>Jane Doe · Acme</p>'
# → {"mailbox":{"uuid":"<mailbox-uuid>","status":"pending", …}}
cargo-ai mailboxManagement mailbox refresh-status <mailbox-uuid> | jq -r '.mailbox.status'
# repeat until "active" — or wait, a sweep runs every 5 minutes
```
`pending` is normal: the provider has not issued SMTP credentials yet. A send before `active`
fails with `credentialsMissing`, which retries — so an early batch recovers rather than losing
rows.
---
## 3. Start warm-up, and read the ramp honestly
```bash
cargo-ai mailboxManagement mailbox start-warmup <mailbox-uuid> --daily-target 40
cargo-ai mailboxManagement mailbox get-send-allowance <mailbox-uuid>
# day 1 → {"allowance":{"dailyLimit":5,"sentCount":0,"remainingCount":5}}
# day 15 → {"allowance":{"dailyLimit":16, …}}
# day 45 → {"allowance":{"dailyLimit":40, …}}
cargo-ai mailboxManagement mailbox get-warmup-stats <mailbox-uuid> # next CLI release
# → {"stats":{"sentCount":120,"deliveredCount":114,"spamCount":6,"inboxRate":95,"spamRate":5}}
```
`--daily-target` is the provider's **dummy** warm-up traffic, not your allowance. To pause the
dummy mail without losing the ramp use `update-warmup --status paused`; `stop-warmup` resets
the anchor and sends the mailbox back to 5/day for another 45 days.
`inboxRate` below ~90 while warm-up is running is the signal to stop adding real volume, not to
push harder.
---
## 4. Send one first-touch, checking the allowance first
```bash
# Free: is this recipient allowed to be contacted at all?
cargo-ai mailboxManagement suppression list --reasons unsubscribed,bounced,complained,manual \
| jq -r '.suppressions[].email' | grep -Fx 'jane@acme.com' && echo "SUPPRESSED — stop"
# Free: is there room today?
cargo-ai mailboxManagement mailbox get-send-allowance <mailbox-uuid> | jq .allowance.remainingCount
# 0.1 credits
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<mailbox-uuid>","to":"jane@acme.com","subject":"Quick question about <signal>","bodyHtml":"<p>…</p>"}' \
--wait-until-finished
# → {"messageUuid":"…","rfcMessageId":"<abc@cargo>","sentAt":"…"}
```
Keep `rfcMessageId` — the follow-up needs it. There is no dry run, so send the first one to
yourself and read it in a real client before pointing this at anyone else.
---
## 5. Thread a follow-up onto the reply
```bash
# Find the reply
cargo-ai mailboxManagement event list --mailbox-uuid <mailbox-uuid> --kinds replied --limit 20 \
| jq -r '.events[] | "\(.occurredAt) \(.actorEmail) \(.snippet)"'
# Read the conversation whole
cargo-ai mailboxManagement thread get <thread-uuid>
# Reply on-thread: references carries the WHOLE chain, oldest first
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<mailbox-uuid>","to":"jane@acme.com","subject":"Re: Quick question","bodyHtml":"<p>…</p>","inReplyTo":"<abc@cargo>","references":["<abc@cargo>"]}' \
--wait-until-finished
```
`inReplyTo` alone threads the first reply and then breaks — mail clients need the full ancestry
in `references`.
---
## 6. The weekly reply queue
What actually happened, in one pass. All free.
```bash
cargo-ai mailboxManagement event list \
--kinds replied,unsubscribed \
--occurred-after 2026-08-14 --limit 200 \
| jq -r '.events[] | "\(.kind)\t\(.actorEmail)\t\(.snippet // "")"'
cargo-ai mailboxManagement thread list --mailbox-uuid <mailbox-uuid> --statuses replied --limit 50 \
| jq -r '.threads[] | "\(.updatedAt) \(.toEmail) \(.subject)"'
cargo-ai mailboxManagement mailbox list --statuses inactive \
| jq -r '.mailboxes[] | "\(.email) \(.errorCode) \(.errorMessage)"'
```
Report it as counts and a table, not raw JSON. An `inactive` mailbox with `errorCode: "402"` is
a spam block — stop sending from that domain and look at what was sent.
Do **not** report "0 bounces" as a deliverability result: `bounced` has no producer yet.
---
## 7. Honour an opt-out that arrived out of band
Someone replies "take me off your list" rather than clicking unsubscribe.
```bash
cargo-ai mailboxManagement suppression create --email opted-out@acme.com
# → {"suppression":{"reason":"manual", …}}
# Prove it: the next send is refused by the engine, before it costs anything
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<mailbox-uuid>","to":"opted-out@acme.com","subject":"…","bodyHtml":"…"}' \
--wait-until-finished
# → run error: recipientSuppressed
```
Suppression is workspace-wide and idempotent, and there is no removal command. Also subtract it
from the **next** sourcing run, not just from the send: a suppressed person re-entering as a
"new" lead is the failure this list exists to prevent
([`../../../cargo-gtm/references/acceptable-use.md`](../../../cargo-gtm/references/acceptable-use.md) §5).
---
## 8. Retire a mailbox
```bash
cargo-ai mailboxManagement mailbox list | jq -r '.mailboxes[] | "\(.email)\t\(.type)\t\(.chargedUntil)"'
cargo-ai mailboxManagement mailbox remove <mailbox-uuid>
```
`remove` deletes the inbox at the provider as well as in Cargo, and it is the only way monthly
billing stops — there is no pause. Threads, messages, and events already recorded stay
readable; the suppression list is workspace-wide and is unaffected.
If the mailbox was declared with CDK's `defineMailbox`, remove it there instead and
`cargo-ai cdk deploy`, or the next deploy will provision it again.
references/response-shapes.md
# Response shapes and enums
What each command returns, and the enums you filter on. Every command in this domain is a
single synchronous HTTP call — there is no run wrapper and nothing to poll.
## Envelopes
| Command | Returns | `count`? | Default limit | Max limit |
| --- | --- | --- | --- | --- |
| `mailbox list` | `{ mailboxes: Mailbox[] }` | **no** | **none** | 1000 |
| `mailbox get` / `create` / `update` / `remove` / `refresh-status` / `*-warmup` | `{ mailbox: Mailbox }` | — | — | — |
| `mailbox get-send-allowance` | `{ allowance: { dailyLimit, sentCount, remainingCount } }` | — | — | — |
| `mailbox get-warmup-stats` | `{ stats: WarmupStats \| null }` | — | — | — |
| `message list` | `{ count, messages: Message[] }` | yes | 50 | 200 |
| `message get` | `{ message: Message }` | — | — | — |
| `thread list` | `{ count, threads: Thread[] }` | yes | 50 | 200 |
| `thread get` | `{ thread: Thread }` | — | — | — |
| `event list` | `{ count, events: Event[] }` | yes | 50 | 200 |
| `suppression list` | `{ count, suppressions: Suppression[] }` | yes | **none** | 1000 |
| `suppression create` | `{ suppression: Suppression }` | — | — | — |
| `pricing get` | `{ monthlyCredits: Record<MailboxType, number> }` | — | — | — |
**Two traps.** `mailbox list` is the only list with no `count` — count the array. And `mailbox
list` / `suppression list` have no default limit, so an unbounded call returns everything up to
1000 while the other three quietly stop at 50.
There is no `event get`. Fetch events through `event list --message-uuid` or `--thread-uuid`.
## Enums
```
MailboxType google | outlook | shared | private # outlook cannot deliver
MailboxStatus pending | active | inactive
MailboxWarmupStatus disabled | pending | active | paused | failed
MailboxProviderSlug mailpool # the only provider
MailboxTransportSlug smtp | graph # outlook → graph, rest → smtp
MessageStatus pending | success | error
MessageListStatus pending | error | sent | opened | clicked | replied | bounced | unsubscribed
EventKind sent | opened | clicked | replied | bounced | unsubscribed
SuppressionReason unsubscribed | bounced | complained | manual
```
- **`MessageStatus` is not what you filter on.** `message list --statuses` and
`thread list --statuses` take `MessageListStatus`, where `success` does not appear: a
delivered message reads as `sent` (or a later event). `pending` and `error` come from the
message row; everything else is the latest event.
- **`bounced` has no producer yet.** Nothing parses delivery-status notifications, so no
`bounced` events are written and bounces do not auto-suppress. An empty bounce count is not
evidence of a clean list.
- Every `--statuses` / `--kinds` / `--reasons` flag is comma-separated **with no spaces**.
## `Mailbox`
```jsonc
{
"uuid": "…", "workspaceUuid": "…",
"domainUuid": "…|null", // the sending domain, when Cargo owns it too
"folderUuid": "…|null", // null = workspace root
"provider": "mailpool",
"meta": {}, // opaque, provider-owned
"email": "jane@acme-outreach.com",
"firstName": "Jane", "lastName": "Doe",
"signature": "…|null",
"type": "google", "transport": "smtp",
"credentials": { "smtp": {…}, "imap": {…} }, // encrypted at rest; only transports decrypt
"status": "active",
"errorCode": "…|null", // set when status is inactive: 401 auth, 402 spam
"errorMessage": "…|null",
"warmupStatus": "active",
"warmupDailyTarget": 40,
"warmupStartedAt": "…|null", // the ramp anchor — see warmup-and-allowance.md
"dailySendLimit": null, // an override that can only TIGHTEN the ramp
"userUuid": "…",
"chargedUntil": "…", // end of the month already paid for
"inboundUidValidity": null, "inboundLastUid": null,
"inboundJunkUidValidity": null, "inboundJunkLastUid": null,
"inboundSyncedAt": "…|null", // last IMAP poll for replies
"createdAt": "…", "updatedAt": "…", "deletedAt": "…|null"
}
```
The fields worth reading: `status` + `errorCode` (can it send at all), `warmupStatus` +
`warmupStartedAt` (how much can it send), `chargedUntil` (what it is costing), and
`inboundSyncedAt` (are replies being picked up).
## `Message`
```jsonc
{
"uuid": "…", "workspaceUuid": "…", "mailboxUuid": "…",
"toEmail": "jane@acme.com", "subject": "…",
"bodyHtml": "…|null", "bodyText": "…|null",
"rfcMessageId": "<…>", // pass to a follow-up's inReplyTo / references
"inReplyTo": "<…>|null",
"threadUuid": "…", // assigned by Cargo at send time
"providerMessageId": "…|null",
"status": "success", // pending | success | error
"errorMessage": "…|null",
"sentAt": "…|null",
"createdAt": "…", "updatedAt": "…",
"lastEvent": { … } | null // null when nothing has happened yet
}
```
## `Thread`
```jsonc
{
"uuid": "…", // its own uuid, NOT the first message's
"workspaceUuid": "…", "mailboxUuid": "…",
"toEmail": "jane@acme.com",
"subject": "…", // first outbound's subject, without a leading Re:/Fwd:
"createdAt": "…", "updatedAt": "…",
"lastEmail": { …Message } | null,
"lastEvent": { …Event } | null
}
```
`thread list --created-after` / `--created-before` filter on **last activity**, not creation —
which is what you want for a reply queue, and surprising if you read the flag name literally.
## `Event`
```jsonc
{
"uuid": "…", "workspaceUuid": "…", "mailboxUuid": "…",
"messageUuid": "…", "threadUuid": "…",
"kind": "replied",
"occurredAt": "…",
"actorEmail": "jane@acme.com", // recipient for opens/clicks; From on a reply
"url": "…|null", // clicked only — the original href
"inboundRfcMessageId": "<…>|null", // replied only
"userAgent": "…|null",
"snippet": "…|null", // first characters of a reply body
"meta": {},
"createdAt": "…"
}
```
Events are append-only and are **not** denormalised onto the message row: to count opens you
count events, and `message.lastEvent` is only the most recent one.
## `Suppression`
```jsonc
{
"uuid": "…", "workspaceUuid": "…",
"email": "opted-out@acme.com", // normalised: trim().toLowerCase()
"reason": "unsubscribed",
"mailboxUuid": "…|null", // the mailbox whose message triggered it
"messageUuid": "…|null",
"createdAt": "…", "updatedAt": "…"
}
```
Workspace-wide, not per mailbox. `suppression create` always records `manual` and is idempotent
— re-suppressing returns the existing row. There is no removal command.
## Warm-up stats
```jsonc
{ "stats": { "sentCount": 120, "deliveredCount": 114, "spamCount": 6,
"inboxRate": 95, "spamRate": 5 } }
```
`inboxRate` and `spamRate` are **0–100 integers**, and `null` until something has been sent.
`{"stats": null}` means the inbox is still joining the warm-up pool; a 400 `warmupNotStarted`
means warm-up is `disabled` or `failed`. Different problems, different fixes.
references/sending.md
# Sending: the `sendEmail` action
Delivery is not a `mailboxManagement` command. It is a **native orchestration action**, so that
every send inherits orchestration's pacing, retry, credit accounting, and run history rather
than bypassing them. This page is everything that action does.
## The call
```bash
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<mailbox-uuid>","to":"jane@acme.com","subject":"Quick question","bodyHtml":"<p>…</p>"}' \
--wait-until-finished
```
No `config` on the action — inputs go in `--data`, as with every action
([`../../cargo-orchestration/SKILL.md`](../../cargo-orchestration/SKILL.md)).
| Field | Required | Meaning |
| --- | --- | --- |
| `mailboxUuid` | ✅ | Mailbox to send from. Must be `active`. |
| `to` | ✅ | Recipient address. Checked against the workspace suppression list first. |
| `subject` | ✅ | Subject line. Honest and descriptive — a misleading subject is a §2 refusal. |
| `bodyHtml` | — | HTML body. |
| `bodyText` | — | Plain-text fallback. Generated from `bodyHtml` when omitted. |
| `inReplyTo` | — | `Message-ID` this message replies to. |
| `references` | — | Every `Message-ID` in the thread so far, **oldest first**. |
Output: `{ messageUuid, rfcMessageId, providerMessageId, sentAt }`.
**Cost: 0.1 credits per send**, fixed, regardless of size or outcome.
## Threading
`inReplyTo` alone threads the first reply and then breaks. Mail clients need the full ancestry:
`references` must carry the whole chain, oldest first, per RFC 5322 §3.6.4.
```bash
# 1. First touch — keep the rfcMessageId it returns
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<uuid>","to":"jane@acme.com","subject":"Quick question","bodyHtml":"<p>…</p>"}' \
--wait-until-finished
# → {"messageUuid":"…","rfcMessageId":"<abc@cargo>","…":"…"}
# 2. Follow-up on the same thread
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<uuid>","to":"jane@acme.com","subject":"Re: Quick question","bodyHtml":"<p>…</p>","inReplyTo":"<abc@cargo>","references":["<abc@cargo>"]}' \
--wait-until-finished
```
Cargo assigns the `threadUuid` itself: a new one when the message starts a thread, otherwise
copied from the parent matched via `In-Reply-To` / `References`. Inbound replies are recorded as
**events** on the outbound message, not as message rows — `thread get <uuid>` is where you read
a conversation whole.
## Refusals: `notExecuted`, not an exception
A send that does not happen is a **node error with a reason**, not a thrown error and not a
silent success. Read the reason; it decides whether retrying is pointless.
| Reason | Meaning | Retries? |
| --- | --- | --- |
| `recipientSuppressed` | The address is on the workspace suppression list | ❌ Needs a human — and the answer is not to remove the suppression |
| `mailboxNotActive` | Mailbox is `pending`, or the provider disabled it (`errorCode` 401 auth / 402 spam) | ❌ Fix the mailbox |
| `transportNotSupported` | An `outlook` mailbox — Graph delivery has not shipped | ❌ Permanent |
| `mailboxNotFound` | Bad `mailboxUuid` | ❌ |
| `dailyLimitReached` | The ramp's allowance is exhausted for the rolling 24h | ✅ Lifts on its own |
| `credentialsMissing` | Provisioning has not finished issuing SMTP credentials | ✅ Resolves itself |
| `deliveryFailed` | The transport rejected it; `errorMessage` carries the detail | ✅ |
The distinction matters at batch scale: three of these will never succeed on retry, so a batch
full of `recipientSuppressed` is a list problem, not a transient one.
## Pacing
The action is rate-limited **per mailbox**, keyed `mailboxManagement:mailboxes:<uuid>`, with a
`spread` strategy — one send per slot, slots sized as `24h ÷ dailyLimit`. Two effects:
- Sends from one mailbox are spaced across the day rather than bursting, which is the pattern
mailbox providers reward.
- A burst larger than the remaining allowance **fails fast** rather than parking. With 40 left,
the 41st of 100 fails immediately with `dailyLimitReached` instead of waiting a day.
When the allowance cannot be read at all, pacing falls back to **1 per day** rather than
unlimited — an unanswered question slows sending to a crawl by design.
So: call `mailbox get-send-allowance <uuid>` and read `remainingCount` **before** enrolling a
batch. Rows past the allowance each burn a run and deliver nothing.
## What Cargo adds to every message
Injected automatically; you do not write these and must not strip them.
- **`List-Unsubscribe`** — a signed link. A recipient using it writes a workspace-wide
`suppression` row with reason `unsubscribed`, and every later send to that address is refused.
- **Open pixel** — produces an `opened` event.
- **Click redirect** — outbound hrefs are rewritten; produces a `clicked` event carrying the
original `url`.
All three carry HMAC-signed tokens (the recipient is not a Cargo user, so the token is the whole
credential), which is why they cannot be hand-assembled or replayed against another workspace.
What is **not** added: a postal address. Where the sender's jurisdiction requires one — CAN-SPAM
does — it has to be in the body you supply
([`../../cargo-gtm/references/acceptable-use.md`](../../cargo-gtm/references/acceptable-use.md) §4).
## No dry run from the CLI
The engine supports a dry execution (it returns `✅ Would send "<subject>" to <address>` without
delivering), but no `orchestration action execute` flag reaches it. **From the CLI, a send is
live the moment you run the command.**
The practical substitutes, in order:
1. Send to your own address first and read it in a real client — the only way to see the
rendered HTML, the signature, and the unsubscribe footer as the recipient will.
2. `cargo-ai orchestration action get-output-schema --action '{"kind":"native","actionSlug":"sendEmail"}'`
resolves the output shape for free, without sending.
3. For a workflow graph, `cargo-ai orchestration node diagram` draws the routing for free and
shows which nodes bill — approve the graph before deploying it.
## In a workflow or play
In a CDK `defineWorkflow` body the same action is the `sendEmail(...)` helper:
```ts
sendEmail({
mailboxUuid: mailbox.uuid,
to: row.email,
subject: `…`,
bodyHtml: `…`,
});
```
A **play or scheduled tool that calls it re-bills on every run** — and, more importantly,
re-contacts the same audience on every run. That is the cadence gate in `acceptable-use.md` §6
as much as the spend gate in
[`../../cargo-gtm/references/cost-discipline.md`](../../cargo-gtm/references/cost-discipline.md).
Cap the touch count, stop on reply, opt-out, or bounce, and check the segment is not re-enrolling
people you already wrote to.
references/troubleshooting.md
# Troubleshooting
Ordered by when you hit them: provisioning, then warm-up, then sending, then reading results.
## Provisioning
### "I need a `--domain-uuid` and nothing lists domains"
Correct — this is a real gap, not a missing flag. `mailbox create` requires a sending domain
UUID, and `domainManagement` has an API but **no `cargo-ai` commands**. Two ways out:
- **Web app** — open the sending domain in Cargo and take the UUID from the URL.
- **CDK** — declare it with `defineDomain` (`adopt: true` for a domain already bought in the
app), `cargo-ai cdk deploy`, and read the UUID back from `cargo.state.json`. See
[`../../cargo-cdk/SKILL.md`](../../cargo-cdk/SKILL.md).
Say this to the user rather than guessing a UUID, and file it:
```bash
cargo-ai workspaceManagement report create \
--title "No CLI surface for domainManagement blocks mailbox create" \
--description "mailbox create requires --domain-uuid; the CLI exposes no command that lists sending domains."
```
### `mailbox create` failure reasons
| Reason | What it means | Fix |
| --- | --- | --- |
| `domainNotFound` | The UUID is not a domain in this workspace | Re-read it from the app or `cargo.state.json` |
| `domainNotActive` | The domain is registered but not live (DNS still propagating, or `failed`) | Wait, or fix the zone |
| `mailboxAlreadyExists` | Something already holds `username@domain` | Pick another local part, or `mailbox list --domain-uuid <uuid>` to find it |
| `transportNotSupported` | `--type outlook` | Use `google`, `shared`, or `private` |
| `folderNotFound` | `--folder-uuid` is wrong, or the folder is not of kind `mailbox` | `workspaceManagement folder list` |
| `notEnoughCredits` | The workspace cannot cover the monthly charge | Top up, or provision fewer |
| `provisioningFailed` | The provider refused | Retry once; if it repeats, file a report |
### The mailbox is stuck at `pending`
`create` always returns `pending` — the provider has not issued credentials yet. It clears when
`refresh-status` says so, or on its own within five minutes (a maintenance sweep runs every five
minutes). Poll it:
```bash
cargo-ai mailboxManagement mailbox refresh-status <uuid> # repeat until status is "active"
```
Still `pending` after ~10 minutes, or `refresh-status` returns `providerMailboxNotFound`?
Provisioning did not complete at the provider. File a report with the mailbox UUID.
A send before `active` fails with `credentialsMissing` — which retries, so a batch launched too
early recovers on its own rather than losing rows.
### The mailbox went `inactive`
The provider disabled it. `errorCode` says why: **401** is an auth failure (credentials
rotated or revoked), **402** is a spam/abuse block. Neither is fixable by re-running a command.
A 402 in particular means the domain's reputation is at risk — stop sending from the whole
domain and look at what was sent, not at the mailbox.
### `--type outlook` never works
The flag accepts it, the API refuses it, every time, with `transportNotSupported`. Outlook
mailboxes only expose a Graph transport and Graph delivery has not shipped. CDK's
`defineMailbox` omits `outlook` from its type union for this reason. Use `google`, `shared`,
or `private`.
## Warm-up
| Symptom | Cause | Fix |
| --- | --- | --- |
| `warmupAlreadyStarted` | `start-warmup` on a running warm-up | Use `update-warmup` to change the target |
| `warmupNotStarted` | `update-warmup` or `get-warmup-stats` on a mailbox that never started (or that is `disabled` / `failed`) | `start-warmup` first |
| `warmupNotSupported` | The provider cannot warm this flavour | Nothing to do |
| `mailboxNotActive` | Still `pending`, or provider-disabled | Resolve the status first |
| `get-warmup-stats` → `{"stats": null}` | Warm-up is running; the inbox is still joining the pool | Wait — this is **not** the same as `warmupNotStarted` |
| `get-warmup-stats` prints the `mailbox` group help instead of running | The CLI predates the release that added the command — an unknown subcommand falls back to group help rather than erroring | `mailbox --help` to see what your CLI has, then re-run the session refresh in [`../../cargo/SKILL.md`](../../cargo/SKILL.md) |
| Allowance stuck at 5/day | Warm-up never started, or `stop-warmup` reset the anchor | `start-warmup`, then wait — the ramp takes 45 days |
| Allowance lower than expected | An explicit `dailySendLimit` is clamping it, or the ramp is younger than you think | See [`warmup-and-allowance.md`](warmup-and-allowance.md) |
| Raising `dailySendLimit` changes nothing | It can only tighten, never loosen | By design; the ramp wins |
## Sending
`sendEmail` returns a node error with a reason rather than throwing. The full table is in
[`sending.md`](sending.md); the ones that surprise people:
- **`recipientSuppressed`** — the address opted out, bounced, complained, or was suppressed
manually. Suppression is workspace-wide and absolute; the fix is to remove the row from your
audience, never to remove the suppression.
- **`dailyLimitReached`** — the ramp's allowance is spent. It retries and lifts on its own, but
a batch larger than `remainingCount` burns a run per excess row. Check
`mailbox get-send-allowance` first.
- **A batch that "ran" but delivered nothing** — read the run errors, not the run count. Three
reasons never succeed on retry (`recipientSuppressed`, `mailboxNotActive`,
`transportNotSupported`), so a batch full of them is a list or a mailbox problem, not a
transient one. [`../../cargo-diagnostics/SKILL.md`](../../cargo-diagnostics/SKILL.md) sweeps a
batch by root cause.
There is **no dry run from the CLI**. Send to yourself first — it is the only way to see the
rendered HTML, the signature, and the unsubscribe footer as the recipient will.
## Reading results
| Symptom | Cause |
| --- | --- |
| Filtering `--statuses success` returns nothing | `success` is not a list status — a delivered message reads as `sent`. See [`response-shapes.md`](response-shapes.md) |
| `--statuses sent, replied` returns nothing | The space. CSV flags are comma-separated with **no spaces** |
| Zero bounces on a list you expect to bounce | `bounced` has **no producer yet** — nothing parses delivery-status notifications. Do not read the empty count as a clean list |
| `mailbox list` has no `count` | Only that list omits it; count the array |
| A list stopped at 50 rows | `message` / `thread` / `event` default to `--limit 50` (max 200). `mailbox` and `suppression` have no default (max 1000) |
| `--folder-uuid none` on `list` returned unfiled mailboxes, not an error | `none` is the sentinel — on `list` it means "in no folder", on `update` it means "clear it" |
| A reply is not in `message list` | Inbound replies are **events**, not messages. `event list --kinds replied`, or `thread get <uuid>` |
| Replies are not appearing at all | Check `inboundSyncedAt` on the mailbox — null means IMAP has never polled it |
| `thread list --created-after` matched threads created earlier | On threads it filters on **last activity**, which is usually what you want for a reply queue |
## Still stuck
Re-refresh the CLI and skills first — a fix may have shipped since the session started. If a
documented flag or shape still does not match what you observe, file it; every report is read
by the team.
```bash
cargo-ai workspaceManagement report create \
--title "<one-line summary>" \
--description "<exact command(s), errorMessage verbatim, expected vs actual, UUIDs>"
```
references/warmup-and-allowance.md
# Warm-up and the send ramp
Everything a mailbox may send is derived, not configured. This page is the arithmetic behind
`mailbox get-send-allowance`, the state machine behind `start-warmup` / `update-warmup` /
`stop-warmup`, and the fleet-sizing consequence of both.
## The ramp
Real (non-warm-up) sends ramp **linearly from 5 a day to 40 a day over 45 days**, counted from
`warmupStartedAt`:
```
dailyLimit = floor( 5 + (40 - 5) * elapsedDays / 45 ) for 0 < elapsedDays < 45
= 5 when warm-up has never started
= 40 from day 45 onwards
```
| Day | `dailyLimit` |
| --- | --- |
| never started warm-up | 5 |
| 1 | 5 |
| 7 | 10 |
| 15 | 16 |
| 30 | 28 |
| 45+ | 40 |
Three consequences worth stating to a user before they plan a campaign:
1. **A mailbox that never starts warm-up is stuck at 5/day forever.** There is no "it warms up
by being used". `start-warmup` sets `warmupStartedAt`, and `warmupStartedAt` is the ramp.
2. **`stop-warmup` resets the anchor.** The mailbox drops to 5/day and the 45 days start over.
To pause the provider's dummy traffic without losing the ramp, use
`update-warmup --status paused`.
3. **40/day is the ceiling by design.** It sits deliberately below the 50/day figure the
cold-outreach playbooks quote: the fleet scales by adding mailboxes, not by pushing one to
its limit. A request to raise the ceiling is an evasion refusal, not a config change
([`../../cargo-gtm/references/acceptable-use.md`](../../cargo-gtm/references/acceptable-use.md) §2).
## `dailySendLimit` is a brake, never a bypass
A mailbox row can carry an explicit `dailySendLimit`. It is clamped against the ramp:
```
effectiveLimit = max( min(dailySendLimit, rampedLimit), 0 )
```
So it can only ever *tighten*. Setting it to 500 on a mailbox created this morning yields 5,
not 500 — an override that outranked the ramp would be a way to send 500/day from a cold inbox,
which is the exact failure warm-up exists to prevent. Use it to hold a mailbox below its ramp
(a shared inbox you want kept quiet); never expect it to raise anything.
## Allowance vs warm-up stats
Two commands, two different populations of mail. Do not report one as the other.
| | `get-send-allowance` | `get-warmup-stats` |
| --- | --- | --- |
| Population | Your real outreach | The provider's warm-up network (dummy mail between pooled inboxes) |
| Window | Rolling **24 hours** | Today, **UTC day** |
| Fields | `dailyLimit`, `sentCount`, `remainingCount` | `sentCount`, `deliveredCount`, `spamCount`, `inboxRate`, `spamRate` |
| Reads as | Capacity | Reputation |
| Empty means | Nothing sent today | Nothing measurable yet — see below |
`inboxRate` and `spamRate` are **0–100 integers**, not fractions, and are `null` until something
has been sent. They are the closest thing Cargo has to a deliverability score — there is no
reputation or health metric on the mailbox itself.
`get-warmup-stats` has two distinct "no data" outcomes, and they mean opposite things:
- `{"stats": null}` — warm-up is running but the inbox is still joining the pool. Wait.
- HTTP 400 `warmupNotStarted` — warm-up is `disabled` or `failed`. Nothing is coming. Fix it.
`sentCount` on the allowance counts **delivered** messages, not queued ones: a dropped or
pending row never widens the allowance.
## `warmupStatus` transitions
`disabled` · `pending` · `active` · `paused` · `failed`
| From | Command | To |
| --- | --- | --- |
| `disabled` | `start-warmup <uuid> [--daily-target n]` | `pending`, then `active` once the provider accepts the inbox |
| `active` | `update-warmup --uuid <uuid> --status paused` | `paused` (ramp anchor preserved) |
| `paused` | `update-warmup --uuid <uuid> --status active` | `active` |
| any | `stop-warmup <uuid>` | `disabled` — **and the ramp resets to day 0** |
`update-warmup --status` accepts the whole enum, but only `active` and `paused` are actionable:
`pending` and `failed` are states the provider reaches on its own, and `disabled` belongs to
`stop-warmup`. `--daily-target` (1–40, default 40) sets the provider's dummy volume at full
ramp; it does **not** change your send allowance.
Errors you will meet: `warmupAlreadyStarted` (already running), `warmupNotStarted`
(update/stats on a mailbox that never started), `warmupNotSupported` (the provider cannot warm
this flavour), `mailboxNotActive` (still `pending`, or disabled by the provider).
## Sizing a fleet
The ramp makes volume a function of **mailbox count × age**, and the pricing makes mailbox count
a monthly bill. Both halves belong in the same sentence when you propose a fleet.
Daily capacity at steady state is `40 × mailboxes`. Monthly cost is
`mailboxes × monthlyCredits[type]` — read live, never from memory:
```bash
cargo-ai mailboxManagement pricing get
```
At the figures returned at the time of writing (`google` 125, `outlook` 160, `shared` 100,
`private` 100 credits/month):
| Fleet | Steady-state sends/day | Monthly credits (`google`) |
| --- | --- | --- |
| 1 | 40 | 125 |
| 3 | 120 | 375 |
| 5 | 200 | 625 |
| 10 | 400 | 1,250 |
Two things to say before anyone provisions:
- **Steady state is 45 days away.** A fleet bought today sends `5 × mailboxes` tomorrow. If the
campaign is next week, more mailboxes will not fix it.
- **The bill recurs.** Provisioning is not a one-off spend, and `mailbox remove` is the only way
to stop it. Apply the approval discipline in
[`../../cargo-gtm/references/cost-discipline.md`](../../cargo-gtm/references/cost-discipline.md):
quote the count, quote the monthly credit estimate, and get an explicit yes.
And the check that comes before all of it: a fleet sized to a volume target rather than to a
qualified audience is the volume-in-place-of-relevance refusal. Size the audience first.
skill-metadata.json
{
"$comment": "Generated by .github/scripts/skills-metadata.mjs — do not hand-edit. Regenerate with: node .github/scripts/skills-metadata.mjs --write .",
"name": "cargo-mailbox-management",
"version": "1.0.2",
"documents": [
{
"path": "SKILL.md",
"kind": "entrypoint",
"title": "Cargo CLI — Mailbox Management"
},
{
"path": "references/examples/recipes.md",
"kind": "example",
"title": "Mailbox recipes"
},
{
"path": "references/response-shapes.md",
"kind": "reference",
"title": "Response shapes and enums"
},
{
"path": "references/sending.md",
"kind": "reference",
"title": "Sending: the `sendEmail` action"
},
{
"path": "references/troubleshooting.md",
"kind": "reference",
"title": "Troubleshooting"
},
{
"path": "references/warmup-and-allowance.md",
"kind": "reference",
"title": "Warm-up and the send ramp"
}
],
"contentHash": "d343f42fdf3e32dd0c02e576185585abd0b68c95bf8c106a26080e4eca0ea9e5"
}
SKILL.md
---
name: cargo-mailbox-management
description: "Send mail from inboxes Cargo owns — provision mailboxes on a sending domain, run provider warm-up and the 5→40/day send ramp, deliver with the `sendEmail` action, and read back threads, replies, delivery events, and the workspace suppression list. Triggers: \"set up a sending mailbox\", \"provision inboxes for outbound\", \"warm up this mailbox\", \"how many sends do I have left today\", \"send this from Cargo\", \"did they reply\", \"who unsubscribed\", \"suppress this recipient\", \"take me off your list\", \"never email them again\", \"what do mailboxes cost\", \"my mailbox is stuck pending\". A mailbox is a recurring monthly credit charge, and every send is gated on basis, suppression, and relevance. Skip when: writing the copy or building the audience — use cargo-gtm; the mailbox belongs in git — use cargo-cdk."
version: "1.0.2"
compatibility: Requires @cargo-ai/cli (npm). Sign in or create an account with `cargo-ai login --email` (emailed code, no browser), `--oauth`, or an API token
homepage: https://github.com/getcargohq/cargo-skills
metadata:
author: getcargo
openclaw:
requires:
bins:
- cargo-ai
install:
- kind: node
package: "@cargo-ai/cli@latest"
bins:
- cargo-ai
homepage: https://github.com/getcargohq/cargo-skills
---
# Cargo CLI — Mailbox Management
**Mailboxes.** A mailbox is a real sending inbox the workspace **owns** — provisioned through
Cargo on a sending domain the workspace also owns, with SMTP and IMAP credentials Cargo holds
and uses to deliver outbound mail and to read the replies that come back. Two things follow,
and both are easy to get wrong: a mailbox is a **recurring monthly credit charge** rather than
a per-record one, and this domain does **not** send. It provisions the inbox, ramps it, and
reports on what it did; the send itself is the `sendEmail` action, under
[`cargo-orchestration`](../cargo-orchestration/SKILL.md).
```bash
cargo-ai mailboxManagement mailbox … # provision, warm-up, send allowance
cargo-ai mailboxManagement message … # outbound sends
cargo-ai mailboxManagement thread … # conversations, and the replies on them
cargo-ai mailboxManagement event … # sent / opened / clicked / replied / unsubscribed
cargo-ai mailboxManagement suppression … # workspace-wide do-not-send list
cargo-ai mailboxManagement pricing … # monthly credits per mailbox flavour
```
> **Version note.** Everything on this page is live in the pinned CLI (1.0.66), including
> `mailbox get-warmup-stats` and the `--daily-target` default of 40 — both of which were
> merged-but-unpublished when this skill was written. On an **older** CLI, `--daily-target`
> reads "provider default if omitted" and `get-warmup-stats` **silently prints the `mailbox`
> group help instead of erroring**, so a missing subcommand here looks like a usage mistake
> rather than a stale install. `cargo-ai mailboxManagement mailbox --help` lists what your CLI
> actually has; re-run the session refresh in [`../cargo/SKILL.md`](../cargo/SKILL.md) to catch up.
## Bootstrap
Already signed in (`cargo-ai whoami` returns a workspace)? Skip to the next section.
```bash
npm install -g @cargo-ai/cli # no global install? prefix every command with `npx @cargo-ai/cli`
cargo-ai login --email you@company.com # emailed code, no browser; creates the account on first use
# alternatives: --oauth (browser) · --token <api-token> (CI)
cargo-ai whoami # confirm the active workspace before any write
```
Every command prints JSON to stdout; failures exit non-zero with `{"errorMessage": "..."}`.
Nothing in this domain is asynchronous — every command is a single HTTP call, so there is no
run to poll (the one thing that *looks* async, a freshly created mailbox sitting at `pending`,
is polled with `mailbox refresh-status`, not with `run get`). Mailboxes are guarded by
`mailboxManagement:read` / `mailboxManagement:write` permissions, which an admin and an editor
both hold and a viewer does not; if a create or update returns a permission error, the token is
read-only ([`../cargo-workspace-management/SKILL.md`](../cargo-workspace-management/SKILL.md)).
When the full skill bundle is installed, [`../cargo/references/prerequisites.md`](../cargo/references/prerequisites.md)
adds the CLI version pin, token scopes, and the admin-only surface.
## Before any send — three checks
**This is a blocking gate, not advice.** Cargo owning the mailbox changes who presses send; it
changes nothing about whether the message should be sent. The canonical rules are
[`../cargo-gtm/references/acceptable-use.md`](../cargo-gtm/references/acceptable-use.md) §3 and
they apply here unchanged — run all three before the first `sendEmail`, not after:
- **Basis** — which permission covers this audience (customers, opted-in contacts, event
attendees, or a documented legitimate-interest case for a B2B role)?
- **Suppression** — subtract the workspace suppression list from the audience *before* you
enrich or send. `suppression list` is free and this domain is the source of truth for it.
- **Relevance** — can you name, per recipient, why this message is for them?
Any check that fails is a stop-and-ask. Two obligations are specific to Cargo-owned sending:
- **The ramp is a ceiling, not a target.** `get-send-allowance` reports what a mailbox may send
today; it is not a quota to fill. Asking to raise it, to spread one campaign across a fleet of
fresh mailboxes to clear the same volume, or to rotate identities so filters see less from
each, is the evasion refusal in `acceptable-use.md` §2 — not a configuration question.
- **Unsubscribes are automatic and absolute.** Every send carries a signed `List-Unsubscribe`
header; a recipient using it writes a workspace-wide `suppression` row, and the next send to
that address is refused by the engine. There is no removal command, and that is the point —
never route around a suppression to re-contact someone.
## The two numbers that govern a mailbox
They look similar and mean opposite things. Confusing them is the most common way to
mis-read a mailbox's health.
| | `mailbox get-send-allowance` | `mailbox get-warmup-stats` |
| --- | --- | --- |
| Measures | **Your** real outreach | The provider's **dummy** warm-up traffic |
| Window | Rolling 24 hours | Today, UTC |
| Returns | `dailyLimit`, `sentCount`, `remainingCount` | `sentCount`, `deliveredCount`, `spamCount`, `inboxRate`, `spamRate` |
| Answers | "How many more may I send?" | "Is this inbox landing in the inbox or in spam?" |
`dailyLimit` is not a setting — it is derived from how long warm-up has been running. The
arithmetic, the `warmupStatus` transitions, and how to size a fleet against it are in
**[`references/warmup-and-allowance.md`](references/warmup-and-allowance.md)**. Read it before
you promise anyone a send volume.
## Commands
All commands output JSON. Reads need `mailboxManagement:read`; everything that provisions,
updates, deletes, or suppresses needs `mailboxManagement:write`.
### Provision a mailbox
**First, the gap you will hit.** `mailbox create` requires `--domain-uuid`, and there is no
`cargo-ai` command that lists sending domains — the `domainManagement` API exists but has no
CLI surface yet. Get the UUID from the Cargo web app, or declare the domain in a CDK repo with
`defineDomain` and read it back from `cargo.state.json`. Say this to the user rather than
guessing a UUID.
```bash
cargo-ai mailboxManagement mailbox create \
--domain-uuid <domain-uuid> \
--type google \
--username jane \
--first-name Jane \
--last-name Doe \
--signature '<p>Jane Doe · Acme</p>' \
--folder-uuid <folder-uuid>
cargo-ai mailboxManagement mailbox refresh-status <uuid> # repeat until status is "active"
```
- `--type` — `google`, `shared`, or `private`. **`outlook` is accepted by the flag and always
fails** with `transportNotSupported`: the Graph transport has not shipped, so an Outlook
mailbox cannot deliver. Pick one of the other three.
- `--username` — the local part only (`jane` for `jane@acme.com`). Lowercased; letters, digits,
dots, dashes and underscores, 1–64 characters, starting and ending alphanumeric.
- `--first-name` / `--last-name` — the From header the recipient sees. Use a real person's name
under a real identity; a fabricated sender is a §2 refusal.
- `--signature` — HTML, stored on the mailbox (max 10,000 characters).
- `--folder-uuid` — a folder of kind `mailbox`, from [`cargo-workspace-management`](../cargo-workspace-management/SKILL.md).
`create` returns immediately with `status: "pending"` — the provider has not issued credentials
yet. It reaches `active` when `refresh-status` says so, or on its own within five minutes; a
send attempted before then fails with `credentialsMissing`.
```bash
cargo-ai mailboxManagement mailbox list # every mailbox
cargo-ai mailboxManagement mailbox list --statuses active # comma-separated, no spaces
cargo-ai mailboxManagement mailbox list --domain-uuid <uuid>
cargo-ai mailboxManagement mailbox get <uuid>
cargo-ai mailboxManagement mailbox update --uuid <uuid> --first-name Janet
cargo-ai mailboxManagement mailbox update --uuid <uuid> --folder-uuid none # "none" clears
cargo-ai mailboxManagement mailbox remove <uuid> # deletes at the provider too
```
- `--statuses` — `pending`, `active`, `inactive`, comma-separated **with no spaces**. An
`inactive` mailbox was disabled by the provider; `errorCode` says why (`401` auth, `402`
spam) and it will not send until it is fixed.
- `none` is the sentinel for "clear it" on `--folder-uuid` and `--signature`. On `list`,
`--folder-uuid none` means "mailboxes in no folder".
- `remove` is how monthly billing stops. There is no pause.
### Warm it up
A mailbox that never starts warm-up is pinned at **5 real sends a day, forever**. Warm-up is
what moves it, and it takes 45 days to finish.
```bash
cargo-ai mailboxManagement mailbox start-warmup <uuid> --daily-target 40
cargo-ai mailboxManagement mailbox get-warmup-stats <uuid> # next CLI release
cargo-ai mailboxManagement mailbox update-warmup --uuid <uuid> --status paused
cargo-ai mailboxManagement mailbox update-warmup --uuid <uuid> --daily-target 25
cargo-ai mailboxManagement mailbox stop-warmup <uuid> # resets the ramp
```
- `--daily-target` — warm-up messages per day at full ramp, 1–40 (default 40). This is the
provider's dummy traffic, **not** your send allowance.
- `--status` on `update-warmup` accepts the whole enum, but only `active` and `paused` do
anything: `pending` and `failed` are states the provider reaches on its own, and `disabled`
is what `stop-warmup` is for.
- `stop-warmup` **resets the Cargo send ramp** as well as tearing down provider warm-up — the
mailbox drops back to 5/day and starts the 45 days over. Pause instead unless you mean it.
### Check the allowance before you send
```bash
cargo-ai mailboxManagement mailbox get-send-allowance <uuid>
# → {"allowance":{"dailyLimit":12,"sentCount":4,"remainingCount":8}}
```
Read `remainingCount` before enrolling a batch. Sends past it do not queue for tomorrow — they
fail immediately with `dailyLimitReached`, one wasted run per row.
### Read what happened
```bash
cargo-ai mailboxManagement message list --mailbox-uuid <uuid> --statuses sent,replied --limit 50
cargo-ai mailboxManagement message get <uuid>
cargo-ai mailboxManagement thread list --mailbox-uuid <uuid> --search acme
cargo-ai mailboxManagement thread get <uuid>
cargo-ai mailboxManagement event list --kinds replied,unsubscribed --occurred-after 2026-08-01
```
- **Message vs thread vs event.** A *message* is one outbound send. A *thread* is a
conversation — its `lastEmail` and `lastEvent` are what you sort a reply queue on. An *event*
is something that happened to a message (`sent`, `opened`, `clicked`, `replied`, `bounced`,
`unsubscribed`). Inbound replies are events, not message rows; only outbound mail is a message.
- `--statuses` on `message list` / `thread list` is the **list** status — `pending`, `error`,
or an event kind. `success` is not in that set: a delivered message reads as `sent` or later.
- `--kinds`, `--statuses`, `--reasons` are all comma-separated with no spaces.
- **`bounced` never fires yet.** Nothing parses delivery-status notifications, so bounces do not
produce events and do not auto-suppress. Do not build a deliverability report that treats an
empty bounce count as a clean list.
- `message`, `thread`, and `event` lists default to `--limit 50` (max 200) and return a `count`.
`mailbox list` and `suppression list` have **no** default limit (max 1000), and `mailbox list`
returns **no** `count` — see [`references/response-shapes.md`](references/response-shapes.md).
### Suppression
Workspace-wide, not per mailbox: a recipient opting out is opting out of the sender, not of one
address the sender happens to own.
```bash
cargo-ai mailboxManagement suppression list --reasons unsubscribed,manual
cargo-ai mailboxManagement suppression create --email opted-out@acme.com
```
- Reasons are `unsubscribed` (the recipient's own choice, via `List-Unsubscribe`), `bounced`,
`complained`, and `manual`. `suppression create` always records `manual`.
- It is idempotent — suppressing an already-suppressed address returns the existing row.
- Addresses are normalised (`trim().toLowerCase()`) on both write and check, so casing and
stray whitespace cannot slip a suppressed recipient back into a send.
- There is no `suppression remove`. That is deliberate.
### Pricing
```bash
cargo-ai mailboxManagement pricing get
# → {"monthlyCredits":{"google":125,"outlook":160,"shared":100,"private":100}}
```
Read this **live** before quoting a fleet cost — the figures above are what the workspace
returned at the time of writing, not a constant.
## Sending: the `sendEmail` action
Delivery is deliberately not in this CLI domain. It is a native orchestration action so that
sends inherit orchestration's pacing, retry, and credit machinery:
```bash
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"sendEmail"}' \
--data '{"mailboxUuid":"<mailbox-uuid>","to":"jane@acme.com","subject":"...","bodyHtml":"<p>…</p>"}' \
--wait-until-finished
```
- **0.1 credits per send**, fixed. The action carries no `config`; the inputs go in `--data`, like every
other action ([`../cargo-orchestration/SKILL.md`](../cargo-orchestration/SKILL.md)).
- Optional `bodyText` (generated from the HTML when omitted), `inReplyTo`, and `references`.
- **To keep a reply threaded, send the whole chain.** `references` is every `Message-ID` in the
thread so far, oldest first — not just the parent. Mail clients break the thread otherwise.
- The action is rate-limited **per mailbox** to that mailbox's own daily limit, spread across
the day. A burst of 100 on a mailbox with 40 left fails the 41st immediately rather than
parking it for a day.
- **A refused send is a node error, not a thrown exception.** `recipientSuppressed`,
`mailboxNotActive`, and `transportNotSupported` need a human and do not retry;
`dailyLimitReached`, `credentialsMissing`, and `deliveryFailed` retry on their own.
- **There is no dry run from the CLI.** The engine has one, but no `action execute` flag reaches
it — the send is live the moment you run the command. Send to yourself first.
Threading, the full refusal table, and what Cargo injects into every message (unsubscribe
header, open pixel, click redirect) are in **[`references/sending.md`](references/sending.md)**.
## Cost discipline
This domain bills differently from the rest of the pack, and the difference is the thing to say
out loud before provisioning anything.
- **A mailbox is a monthly, recurring charge** — 100–160 credits *per mailbox, per month*, for
as long as it exists. Five mailboxes is 500–625 credits every month, not once. `mailbox
remove` is the only way to stop it; there is no pause. Quote the fleet size and the monthly
credit estimate from a live `pricing get`, and get an explicit yes, before the first `create`.
- **Sends are 0.1 each**, so volume is cheap and the fleet is not. Do the arithmetic in that
order.
- **A play or scheduled tool that calls `sendEmail` re-bills on every run** — and re-contacts
the same people on every run, which is the §6 cadence gate in `acceptable-use.md` as much as a
spend gate. Check `get-send-allowance` before enrolling a batch: rows past the allowance burn
a run each and deliver nothing.
- The full spend rules — sampling before a full enrollment, the approval message, the receipt —
are [`../cargo-gtm/references/cost-discipline.md`](../cargo-gtm/references/cost-discipline.md).
## Declarative alternative: `defineMailbox` (CDK)
For the inbox itself, **prefer CDK** — the `mailbox create` help says so, and the reason is that
a mailbox is long-lived infrastructure with a monthly cost, which is exactly what belongs in
git and in a plan you can review. `defineMailbox` (with `defineDomain` for the sending domain)
covers it; `adopt: true` binds a mailbox bought in the web app instead of provisioning a second
one. See [`../cargo-cdk/SKILL.md`](../cargo-cdk/SKILL.md) and "Declarative vs imperative" in
[`../cargo/SKILL.md`](../cargo/SKILL.md).
Use this skill's imperative commands for one-off provisioning, and for everything CDK does not
model at all: warm-up, allowance, messages, threads, events, and suppressions.
## When the CLI surprises you
If a documented flag or response shape doesn't match what you observe, re-refresh the CLI and
skills; if it still doesn't add up, file a report — it's read by the team. The missing
`domainManagement` surface is a live example: `mailbox create` needs a `--domain-uuid` that no
command can produce.
```bash
cargo-ai workspaceManagement report create \
--title "<one-line summary>" \
--description "<exact command(s), errorMessage verbatim, expected vs actual, UUIDs>"
```
## Presenting results
Follow [`../cargo/references/interaction.md`](../cargo/references/interaction.md): lead with the
outcome ("mailbox active, 8 of today's 12 sends left, 2 replies since Monday"), summarize a
fleet or a reply queue as a compact table, and never dump raw `mailbox get` or `event list` JSON
into the conversation. When you report a fleet, report its **monthly** cost, not a one-off one.