sendEmail action. In a workflow body you call it the same way as allocate or delay — destructure sendEmail from the scope and pass the mailbox uuid, recipient, subject, and body.
Send from a play
plays/outreach.ts
mailboxUuid is the mailbox handle’s uuid token, not the domain and not the address. A mailbox you did not declare in this repo can still be referenced with a literal uuid (or mailboxRef("uuid").uuid).
The node returns
messageUuid, rfcMessageId, providerMessageId, and sentAt. Each delivered email costs 0.1 credits.
A suppressed recipient, a missing or inactive mailbox, or missing credentials fail without retry — those need a human. A daily cap or a transport error does retry, because the cap lifts on its own and the transport can recover. Volume beyond today’s allowance is not a failure at all: it waits. See the rate limit.
sendEmail is deliberately not serialized behind a lock. When a workflow has
both a lock and a rate limit, the lock wins and the rate limit is skipped —
which would unpace every send from that mailbox.sendEmail directly. Wrap it in a tool and put that tool in the agent’s uses.
Wait for a reply, view, or unsubscribe
waitEmailEvent pauses the run until a mailbox event lands on the thread of a sent email. Point it at the messageUuid sendEmail just returned — the wait loads that message and matches events on any send in the same conversation. If timeoutHours elapses first (default 72, cap 720), the node fails the run.
If the activity already happened, the node completes immediately — it does not wait.
Warm-up and the daily ceiling
Two different “warm-ups” sit on the same mailbox, and only one of them moves the send cap.
Starting provider warm-up sets
warmupStartedAt and the ramp begins. A new mailbox is enrolled automatically the first time it becomes active (the same status poll that issues credentials). Until then — still pending, enrolment failed, or stopped — the mailbox stays at the floor of 5 real sends per rolling 24 hours. Stopping warm-up clears the timestamp and the ramp starts again from 5 the next time you start it. A later status poll will not enrol a stopped mailbox on its own; use Start or start-warmup.
The ceiling itself is a formula evaluated whenever allowance is read (the mailbox page, sendEmail pacing, and the send backstop):
dailySendLimit can only tighten that, never raise it — a mailbox created this morning cannot send 500 by setting an override.
Set the cap on the mailbox page → Send allowance → Edit, with dailySendLimit on PUT /mailboxManagement/mailboxes/{uuid}, or in code on defineMailbox. It accepts 0 to 40, and null goes back to following the ramp.
0 is not a volume, it is a state: outreach from the mailbox is paused. Sends fail immediately with mailboxSendingPaused — a terminal reason, not the retryable dailyLimitReached — and the pacer does not queue behind them, so a paused mailbox fails fast and says why. Nothing else stops a mailbox: pausing warm-up only pauses the provider’s own traffic, and stopping it clears warmupStartedAt, which drops the ramp back to the 5/day floor rather than to zero.
The 45-day shape matches Mailpool’s default warm-up schedule so Cargo’s own pacing and the provider’s dummy traffic ramp together rather than fighting each other. 40/day is deliberately below the 50/day figure cold-outreach playbooks quote: the fleet scales by adding mailboxes, not by pushing any single one to its limit.
How the number updates
Nothing writes6, then 7, into a column as days pass. Each read of send allowance does two things:
dailyLimit— the formula above, right now, tightened bydailySendLimitwhen one is set.rampLimitalongside it is the same formula untightened, which is what the mailbox page compares your cap against.sentCount— successful deliveries in the last 24 hours (rolling, not midnight). Pending and error rows do not count.remainingCountisdailyLimit − sentCount.
success message row. The next allowance read counts it, Left drops by one, and the gap between sends is unchanged. When that send ages out of the window, Left comes back. A mailbox that emptied its quota at 23:00 does not get a fresh burst at midnight — that burst is what providers penalise.
warmupDailyTarget on the mailbox is Mailpool’s dummy-mail target. It is not the real-send cap.
Per-mailbox rate limit
Before eachsendEmail node runs, orchestration asks the action how to pace it. It answers with one limit, keyed per mailbox, that sets both a ceiling and a spacing:
The two are set separately because one number cannot do both. Deriving the gap from the ceiling — cutting the day into 40 — is what left sends 36 minutes apart, or ~4.8 hours on a mailbox at 5/day, so a mailbox could never spend its allowance inside the hours a workflow actually runs. A campaign confined to one afternoon delivered a single email.
A send claims its place in the day and its place in the queue at the same moment, so one that is cancelled while waiting out its gap gives both back.
The ceiling is read per send rather than declared, because it tracks the warm-up ramp: a mailbox on day 2 and one on day 45 do not share a limit. It is the ramp’s figure for today, not what is left of it — the window already counts this mailbox’s own sends, so subtracting them would count them twice.
The gap is drawn again for every send. A mailbox sending on an exact metronome reads as automation to the providers grading it.
Nothing fails for being early
The limit carries no wait budget, and that is deliberate: a send that cannot go out yet waits until it can.- Blocked by the ceiling? The run sleeps until the window rolls over and asks again — so a send beyond today’s allowance leaves tomorrow, and the day after if the backlog is deeper than that.
- Blocked by the gap? The run sleeps the wait it was quoted, however long the queue ahead of it is.
rateLimitWaitTooLong, the rest waking to a spent allowance and failing at the send backstop with dailyLimitReached. Neither is a failure a campaign should have: the volume was not wrong, it was early.
What does bound the waiting is the run, not the pacing. A workflow may live 15 days, so a backlog deeper than about 15 days of allowance — roughly 600 sends on a warmed mailbox — outlives the run waiting for it. Past that a fleet needs more mailboxes rather than more patience.
Two things still refuse rather than wait. A node Cargo cannot resolve a mailbox for — no uuid at all, or a uuid belonging to no mailbox in this workspace — is broken rather than early, so it shares a workspace-wide slot and is refused immediately instead of queueing for a day. That send was going to fail with mailboxNotFound regardless, and pacing it against the named mailbox would spend the allowance of whoever actually owns it. And the send backstop still re-checks the rolling allowance at delivery; if it has gone in the meantime the node returns dailyLimitReached, which retries.
A mailbox that is yours but has been closed by a dailySendLimit of 0 is refused too, on its own key so it never blocks another mailbox. Waiting cannot help it — only lifting the override can — so the node reports the closure instead of queueing behind a ceiling it can never spend.
Idle time does not accrue credit. A backlog after a quiet period is still admitted one slot at a time, not dumped all at once.
Why a send-email span stays pending
A span is created aspending as soon as the run reaches the node. The workflow then sleeps before it executes the send, waiting for a place under the limit. Behind an empty queue that is at most 3 minutes; behind a backlog it is one gap per send ahead of it, and once today’s ceiling is spent it is however long is left of the window.
That is the pacer, not a hung worker. Nothing has been delivered yet, and other mailboxes are unaffected. A pending send-email span means the email is queued, not lost.
Threads, tracking, and suppressions
Each send is filed into a thread — a new uuid when the message starts a conversation, otherwise the parent matched viaIn-Reply-To / References. Replies pulled from IMAP become events on that thread rather than new message rows. The workspace Emails view and the mailbox Emails tab are thread lists.
Events are the reporting surface, in roughly the order they can happen:
The daily ramp counts successful message rows, not
sent events, so a dropped event can never widen a mailbox’s allowance.
Suppression is workspace-wide. A recipient who unsubscribes, bounces, or is added manually is opted out of the sender, not of one address the sender happens to own. A suppressed to is refused before a row is written.
One-off send
To send without a play, execute the same native action. Inputs go in--data; action.config stays empty:

