cargo-mailbox-management
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 mail
Install
npx skills add https://github.com/getcargohq/cargo-skills/tree/main/cargo-mailbox-management
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install getcargohq-cargo-skills@llmmart
git clone https://github.com/getcargohq/cargo-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole getcargohq/cargo-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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-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-statsand the--daily-targetdefault of 40 — both of which were merged-but-unpublished when this skill was written. On an older CLI,--daily-targetreads "provider default if omitted" andget-warmup-statssilently prints themailboxgroup help instead of erroring, so a missing subcommand here looks like a usage mistake rather than a stale install.cargo-ai mailboxManagement mailbox --helplists what your CLI actually has; re-run the session refresh in../cargo/SKILL.mdto catch up.
Bootstrap
Already signed in (cargo-ai whoami returns a workspace)? Skip to the next section.
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).
When the full skill bundle is installed, ../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 §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 listis 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-allowancereports 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 inacceptable-use.md§2 — not a configuration question. - Unsubscribes are automatic and absolute. Every send carries a signed
List-Unsubscribeheader; a recipient using it writes a workspace-widesuppressionrow, 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. 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.
Pricing a domain before buying it has the same gap, and one direct API call closes it:
GET /v1/domainManagement/domains/search?name=<domain> returns available and priceCredits
(where null means not purchasable, not free). The domain side of a fleet — that call,
sizing domains by blast radius rather than capacity, the apex redirect, DMARC, naming, and why
dnsRecords must stay undeclared — is
references/sending-domains.md.
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, orprivate.outlookis accepted by the flag and always fails withtransportNotSupported: the Graph transport has not shipped, so an Outlook mailbox cannot deliver. Pick one of the other three.--username— the local part only (janeforjane@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 kindmailbox, fromcargo-workspace-management.
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.
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. Aninactivemailbox was disabled by the provider;errorCodesays why (401auth,402spam) and it will not send until it is fixed.noneis the sentinel for "clear it" on--folder-uuidand--signature. Onlist,--folder-uuid nonemeans "mailboxes in no folder".removeis 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.
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.--statusonupdate-warmupaccepts the whole enum, but onlyactiveandpauseddo anything:pendingandfailedare states the provider reaches on its own, anddisabledis whatstop-warmupis for.stop-warmupresets 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
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
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
lastEmailandlastEventare 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. --statusesonmessage list/thread listis the list status —pending,error, or an event kind.successis not in that set: a delivered message reads assentor later.--kinds,--statuses,--reasonsare all comma-separated with no spaces.bouncednever 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, andeventlists default to--limit 50(max 200) and return acount.mailbox listandsuppression listhave no default limit (max 1000), andmailbox listreturns nocount— seereferences/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.
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, viaList-Unsubscribe),bounced,complained, andmanual.suppression createalways recordsmanual. - 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
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:
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). - Optional
bodyText(generated from the HTML when omitted),inReplyTo, andreferences. - To keep a reply threaded, send the whole chain.
referencesis everyMessage-IDin 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, andtransportNotSupportedneed a human and do not retry;dailyLimitReached,credentialsMissing, anddeliveryFailedretry on their own. - There is no dry run from the CLI. The engine has one, but no
action executeflag 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.
Reading events back from a workflow: listEmailEvents
mailboxManagement event list is the CLI read, but inside a workflow the delivery events on a
thread come from a native action (CLI ≥ 1.0.86), so a play can branch on whether someone
replied:
cargo-ai orchestration action execute \
--action '{"kind":"native","actionSlug":"listEmailEvents"}' \
--data '{"threadUuid":"<thread-uuid>","kinds":["replied","clicked"],"limit":50}' \
--wait-until-finished
threadUuidis required;kindsis optional (omit for every event on the thread) andlimitdefaults to 50, capped at 200, newest first.- Event kinds are
sent,opened,clicked,replied,bounced,unsubscribed. Note the wire value isopenedeven though the UI labels it "Viewed". - Each event returns
uuid,mailboxUuid,messageUuid,threadUuid,kind,occurredAt,actorEmail, plus nullableurl(onclicked),inboundRfcMessageId, andsnippet. bouncedstill has no producer — nothing parses delivery-status notifications yet, so filtering on it returns nothing and proves nothing. Never report an empty bounce count as a clean list.
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 removeis the only way to stop it; there is no pause. Quote the fleet size and the monthly credit estimate from a livepricing get, and get an explicit yes, before the firstcreate. - 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
sendEmailre-bills on every run — and re-contacts the same people on every run, which is the §6 cadence gate inacceptable-use.mdas much as a spend gate. Checkget-send-allowancebefore 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.
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-project/SKILL.md and "Declarative vs imperative" in
../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.
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: 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.
Files (cargo-skills)
-
references
-
examples
-
recipes.md 7.2 KB
# 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 project deploy`, or the next deploy will provision it again.
-
-
response-shapes.md 7 KB
# 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. -
sending-domains.md 7.3 KB
# Sending domains The domain half of a fleet. [`warmup-and-allowance.md`](warmup-and-allowance.md) sizes capacity in mailboxes; this page is where those mailboxes live, what it costs, and the two choices that decide whether the fleet lands in inboxes. `domainManagement` has no `cargo-ai` command surface. Everything here is either a direct API call or a CDK declaration. ## Checking availability and price The only way to price a domain before buying it: ```http GET /v1/domainManagement/domains/search?name=tryacme.com Authorization: Bearer <accessToken> selected-workspace-uuid: <workspace uuid> -> { "name": "tryacme.com", "available": true, "priceCredits": 1200 } ``` The token is in `~/.config/cargo-ai/credentials.json` after `cargo-ai login`. **`priceCredits: null` does not mean free.** It means the domain is not purchasable through Cargo: either unavailable, or premium-priced above the configured ceiling. Read `null` as unavailable. Never present a candidate name without having checked it. An unavailable name is not a choice the user can make, and quoting a guessed price on a domain is how a fleet gets approved on a number nobody can honour. ## Sizing: mailboxes are capacity, domains are blast radius Capacity comes from mailboxes (`40 × mailboxes` at steady state). Domains come from how much you are willing to lose at once: ``` mailboxes = ceil(target_daily_sends / 40) domains = ceil(mailboxes / mailboxes_per_domain) # 3 is the recommendation ``` **A domain that gets flagged takes every mailbox on it.** So `mailboxes_per_domain` is a blast-radius decision before it is a cost one. Three is the starting recommendation: packing more onto one domain is a supported choice with a stated cost, and going to one mailbox per domain wastes a registration. Worth working the arithmetic out loud, because the shape is what makes the number defensible: > 500 sends a day needs 13 mailboxes, so 5 domains at 3 each, which is 15 mailboxes and a ceiling of > 600 a day. The 100 of headroom is the margin for a mailbox that has to be pulled. Then the part the arithmetic does not say: **the ceiling arrives 45 days after warm-up starts, not on deploy day.** Give the calendar date. A fleet approved the week a campaign launches carries `5 × mailboxes` that week. ## Never send from the primary domain The fleet exists so that a burned sending domain is a disposable one. Put the company's own domain in it and a bad campaign takes the company's real mail with it: invoices, password resets, recruiting. Derive the primary domain from the signed-in address (`cargo-ai whoami`) and confirm it rather than asking. It is the one domain that must never be in the fleet, and it is the redirect target below. ## Forward the apex A lookalike domain that serves nothing is a parked page, and a parked page is a signal in its own right to both filters and to a prospect who types the domain out of a From header. `redirectUrl` on `defineDomain` forwards the apex to the real site. It is one line and it is the difference between a domain that looks owned and one that looks bought. ## DMARC starts at `none` `dmarcEmail` is the `rua=` mailbox that receives aggregate reports. `dmarcPolicy` is the `p=` tag: what receivers do with mail that fails authentication. | `p=` | Receiver behaviour on a failing message | | --- | --- | | `none` | deliver as normal and report it (monitor-only) | | `quarantine` | treat as suspicious, usually the spam folder | | `reject` | refuse at SMTP | Start at `none` with a reporting mailbox, and tighten to `quarantine` and then `reject` once the reports come back clean. `none` is not weakness: it publishes the record and starts the reports while the fleet has no sending history to judge. **Enforcing before SPF and DKIM are proven blackholes your own mail, and Cargo does not see the bounce.** The domain keeps reporting `active` and so does every mailbox on it. Two things about `rua=`: the mailbox has to actually accept mail or the reports bounce, and pointing several domains at one address on a *different* domain requires a `_report._dmarc` authorisation record on the receiving domain. A fleet of six lookalikes all reporting to the primary needs six of them, or the receivers that enforce cross-domain reporting drop the reports. ## Do not declare `dnsRecords` on a sending domain `dnsRecords` is the whole zone, not a patch. Declaring it replaces every live record, including the MX and DKIM records the registrar wrote at purchase and the mailboxes depend on. The failure is silent in the worst way: the domain stays `active`, every mailbox stays `active`, and mail stops arriving. `redirectUrl`, `dmarcEmail` and `dmarcPolicy` are additive and are the supported way to configure the zone. ## Naming Build the name from the brand, not near it: | Pattern | On `acme.com` | Reads as | | --- | --- | --- | | verb prefix | `tryacme`, `useacme` | a product page | | preposition | `withacme`, `joinacme` | natural English | | suffix | `acmehq`, `acmeteam` | the company, not a campaign | | verb compound | `workwithacme`, `runacme` | obviously owned | Rules that are not taste: - **Stay on `.com`.** Alternative TLDs carry worse default reputation at several filters, and `.com` is what a human expects from a company. A cheaper TLD is not a saving. - **No hyphens, no digits.** `get-acme.com` and `acme01.com` are spam-filter patterns. - **No near-typos of the primary.** `acmme.com` and `acrne.com` read as phishing, because that is what phishing uses. The fleet should look owned, not almost-right. - **Say it out loud.** A name that cannot be dictated on a phone call cannot be repeated by a prospect. - **Check the history.** A domain that was registered and dropped can arrive carrying someone else's reputation, and the fleet inherits it silently. Generate about twice as many candidates as domains needed, because availability removes some. A list the user already holds skips generation, but not the availability check and not the spend approval. ## What it costs Two charges with different shapes, and conflating them is how a fleet gets approved on the wrong number. | | Domain | Mailbox | | --- | --- | --- | | Shape | one-time registration, then annual renewal | recurring **every month** until removed | | Quote from | `domains/search` (above), live | `cargo-ai mailboxManagement pricing get`, live | | Stopping it | let the renewal lapse | `mailbox remove` only — there is no pause | Sends are billed separately, per message, through the `sendEmail` orchestration action. Volume is the cheap part; the fleet is not. Present one-time and recurring as two separate lines, multiply the recurring by twelve so the annual shape is visible, and get an explicit yes before the first registration. The spend rules are [`../../cargo-gtm/references/cost-discipline.md`](../../cargo-gtm/references/cost-discipline.md). ## The refusal that comes first A fleet sized to a volume target rather than to a qualified audience is the volume-in-place-of- relevance refusal, and spreading one campaign across a fleet to clear what a single mailbox's ramp would not allow is the evasion case ([`../../cargo-gtm/references/acceptable-use.md`](../../cargo-gtm/references/acceptable-use.md) §2). Size the audience first. Every mailbox is a real person under a real identity; a fabricated sender is a §2 refusal and also the fastest way to fail a recipient who searches the name. -
sending.md 6.8 KB
# 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. -
troubleshooting.md 7.4 KB
# 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 project deploy`, and read the UUID back from `cargo.state.json`. See [`../../cargo-project/SKILL.md`](../../cargo-project/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>" ``` -
warmup-and-allowance.md 5.8 KB
# 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 1.2 KB
{ "$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.2.0", "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-domains.md", "kind": "reference", "title": "Sending domains" }, { "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": "dc3b6c1b42243cce0419dc3d166aeb0f60b85bbeabeb4e097395c3b4f83c37da" } -
SKILL.md 19.9 KB
--- 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-project." version: "1.2.0" 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. Pricing a domain before buying it has the same gap, and one direct API call closes it: `GET /v1/domainManagement/domains/search?name=<domain>` returns `available` and `priceCredits` (where **`null` means not purchasable, not free**). The domain side of a fleet — that call, sizing domains by blast radius rather than capacity, the apex redirect, DMARC, naming, and why `dnsRecords` must stay undeclared — is **[`references/sending-domains.md`](references/sending-domains.md)**. ```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)**. ### Reading events back from a workflow: `listEmailEvents` `mailboxManagement event list` is the CLI read, but inside a workflow the delivery events on a thread come from a **native action** (CLI ≥ 1.0.86), so a play can branch on whether someone replied: ```bash cargo-ai orchestration action execute \ --action '{"kind":"native","actionSlug":"listEmailEvents"}' \ --data '{"threadUuid":"<thread-uuid>","kinds":["replied","clicked"],"limit":50}' \ --wait-until-finished ``` - `threadUuid` is required; `kinds` is optional (omit for every event on the thread) and `limit` defaults to **50**, capped at **200**, newest first. - Event kinds are `sent`, `opened`, `clicked`, `replied`, `bounced`, `unsubscribed`. Note the wire value is **`opened`** even though the UI labels it "Viewed". - Each event returns `uuid`, `mailboxUuid`, `messageUuid`, `threadUuid`, `kind`, `occurredAt`, `actorEmail`, plus nullable `url` (on `clicked`), `inboundRfcMessageId`, and `snippet`. - **`bounced` still has no producer** — nothing parses delivery-status notifications yet, so filtering on it returns nothing and proves nothing. Never report an empty bounce count as a clean list. ## 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-project/SKILL.md`](../cargo-project/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.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.