Agentic engagement
Deploy an AI agent that holds email conversations with leads from a mailbox your workspace owns: a sending domain, a mailbox, native sendEmail and listEmailEvents on the agent, a native email trigger on reply and unsubscribe, and a heartbeat that checks thread status when nothing inbound happened.
Set up the agentic-engagement cookbook in this project.
1. If the Cargo CLI is not installed yet, follow every step in https://api.getcargo.io/INSTALL.md
2. From the Cargo project, run: cargo-ai cdk add cookbook/agentic-engagement
3. Then follow .claude/skills/agentic-engagement/SKILL.md. Stop for my approval before any paid call, and stop at "cargo-ai project plan" before deploying anything.$cargo-ai cdk add cookbook/agentic-engagementSay this to your agent
Give our jordan@ mailbox an agent that answers leads who reply to my first emails, and checks quiet threads once for a single follow-up.
Illustrative output #
Fictional records, to show the shape of what comes back.
From: Priya Raman <priya@northwind.example>
Status: replied (native email trigger wakes the engager's chat)
Subject: Re: Routing inbound leads at Northwind
> Interesting. Does this work if our CRM is Salesforce, not HubSpot?
From: Jordan <jordan@outreach.fabrikam.example> (the bound mailbox)
Subject: Re: Routing inbound leads at Northwind
In-Reply-To: <msg-2@northwind.example>
References: <msg-1@outreach.fabrikam.example> <msg-2@northwind.example>
> Yes. Salesforce is a native connector, same as HubSpot, so routing writes
> straight to your lead owner field. Worth 20 minutes next week to see it on
> your own queue?
One threaded reply per wake, grounded in the workspace context; had Priya unsubscribed instead, the same
wake would have ended with no send, and a thread left at sent gets at most one heartbeat follow-up.
What you will be asked #
| Input | Why | |
|---|---|---|
sendingDomain (infra/domains/outreach.ts) | Looked up | A mailbox cannot be created on a domain the workspace does not own. Registering a second copy of a domain the workspace already has fails instead of binding. |
mailboxIdentity (infra/mailboxes/rep.ts) | Asked | A fabricated sender is refused. Domain, username and type are create-only; getting the local part wrong means destroy plus a new inbox. |
languageModel (infra/connectors/openai.ts) | Looked up | Conversation quality and per-turn cost both live here. |
| mailboxCharge | Asked | A mailbox bills every month until destroy. Silence is not approval of a fleet. |
Done when #
cargo-ai cdk planreports the domain, the mailbox and the engager, and the operator approved the monthly mailbox line against a livepricing getnode --import tsx evals/contract.mjspasses: the agent trigger is nativeemailwithkinds: ["replied", "unsubscribed"], a heartbeat is declared whose prompt nameslistEmailEvents, the engager uses nativesendEmailandlistEmailEvents, andmailboxUuidis locked on thesendEmailuse- the mailbox reaches
active(cargo-ai mailboxManagement mailbox refresh-status) and warm-up has been started (mailbox start-warmup) so the send ramp can climb - a first send to yourself delivers from the declared address
- a reply to that send wakes the engager without a human prompting it
- the agent’s reply lands in the same thread in the mailbox (same subject,
In-Reply-Toset), not as a new conversation - a second reply continues that same thread; an unsubscribe or “not interested” produces no further send
- an unanswered first send is still a live chat: the heartbeat is declared, and the agent treats that wake as a status check, not a sequence
What it costs #
A mailbox is a monthly, recurring charge for as long as it exists. Immediately before the
plan, run cargo-ai mailboxManagement pricing get and read monthlyCredits for the chosen type.
Say that number out loud as a per-month cost, not a one-off. destroy is the only way it stops.
If the plan also shows + create domain:…, that registration is a separate, non-refundable
charge. Adopting an existing domain does not.
Each delivery is a sendEmail native action. Run cargo-ai connection native-integration get (or orchestration action list sendEmail) for the current per-send cost. The engager
adds one LLM turn per wake, including heartbeats. The per-thread cap is the cost control:
one send per wake, at most one unanswered follow-up, and a stop when the lead closes.
A mailbox that never starts warm-up stays at the floor of five real sends a day. Starting
warm-up is what moves the ramp; it takes forty-five days to finish. Check
mailbox get-send-allowance before promising volume.
To let an agent carry email conversations with leads, give it a mailbox your workspace owns and wake
it on every reply, unsubscribe or silence. This cookbook declares the sending domain, the mailbox,
native sendEmail and listEmailEvents on the agent, a native email trigger on reply and
unsubscribe, and a heartbeat that checks thread status when nothing inbound happened.
cargo-ai cdk add cookbook/agentic-engagement
What it does #
- Owns the inbox.
defineDomainplusdefineMailboxprovision (or adopt) the From address the lead sees. The mailbox is a recurring monthly charge. - Sends through native
sendEmail.mailboxUuidis locked in the action’sconfigso every send is from that inbox, and so the native trigger knows which chats to wake. - Reads thread status.
listEmailEventsis how a heartbeat (and any wake without a fresh event payload) seesreplied,unsubscribed,bounced, or silence. Opens and clicks are tracking, not a reason to write. - Wakes on inbound status.
agentNativeTrigger({ agentSlug: "email", config: { kinds: ["replied", "unsubscribed"] } })continues the thread or stops it. - Checks silence. A
heartbeatre-wakes the same chat so the agent can list events on a thread that stayed atsent. At most one unanswered follow-up. - Stops. Unsubscribe, “not interested”, or a request for a human ends the loop. A second unanswered send is a sequence; this agent is not one.
How it works #
flowchart TD
domain["defineDomain"] --> mailbox["defineMailbox"]
mailbox --> send["sendEmail<br/>mailboxUuid locked"]
send --> agent["engager"]
list["listEmailEvents"] --> agent
agent -->|"first send, or a play calls it"| send
send --> thread["thread"]
thread -->|"status: replied or unsubscribed"| trigger["native email trigger"]
trigger --> agent
thread -->|"status still sent"| beat["heartbeat<br/>listEmailEvents"]
beat --> agent
- First outbound. A human chats with the engager, or a play calls it with
a lead. The agent calls
sendEmail. That send belongs to this agent, which is what the trigger later keys off. - Inbound status. IMAP picks up a reply;
List-Unsubscriberecords an opt-out. The native trigger wakes the same chat. - Silence. If the thread stays at
sent, the heartbeat re-wakes the chat so the agent can calllistEmailEvents. One follow-up, then stop. - The agent sends at most once per wake, passing
inReplyToand the fullreferenceschain so the thread stays a thread.
Adds 3 resource kinds plus the folders they file into.
| File | Resource | Role |
|---|---|---|
infra/domains/outreach.ts | defineDomain | sending domain the mailbox lives on |
infra/mailboxes/rep.ts | defineMailbox | the From inbox; monthly charge |
infra/agents/engager.ts | defineAgent | conversation agent + native sendEmail / listEmailEvents + email trigger + heartbeat |
infra/agents/engager.prompt.ts | (not a resource) | when to reply, how to thread, when to stop |
infra/connectors/openai.ts | defineConnector | the LLM the engager talks through |
infra/folders/index.ts | defineFolder | mailbox / agent folders named after the skill |
Why sendEmail is not a tool #
A tool whose body is one native call is ceremony, and this repo refuses those.
sendEmail is a native action the agent can call. The pin that used to live in
a wrapper now lives on the use: config: { mailboxUuid: rep.uuid }. Take that
pin out and the loop silently splits.
Placeholders (edit before deploy) #
- Sending domain —
infra/domains/outreach.ts: the domain the workspace owns. Keepadopt: trueif it was bought in the UI; drop it only to register a new one. - Mailbox identity —
infra/mailboxes/rep.ts: username, first name, last name of a real person. Type defaults togoogle. - Language model —
infra/agents/engager.ts.
What it does not do #
It does not blast a list, start warm-up (that is a CLI call after deploy), merge DNS, write to a CRM, or invent a first send from a reply trigger or a heartbeat. First outbound is a chat or a play that calls this agent; this skill is the loop after that.