Run outreach with an external agent
Drive a full cold-outreach run — create, source, draft, review, approve, launch, read replies — from an external agent over MCP or the CLI, using the same campaign tools the in-app assistant has.
Tinbox campaigns are managed by one tool manifest, and that manifest drives
all three external surfaces at once — the in-app assistant, the /mcp endpoint
(Claude, Cursor, any MCP client), and the generated tinbox CLI commands. There is
no separate "outreach API" to learn: every operation an autonomous agent needs is a
manifest tool, so an agent running outside your Worker manages a campaign with
exactly the surface the in-worker assistant has.
This guide walks the full cold-outreach loop — create → source → draft → review →
approve → launch → read replies — driven by an external agent. The same tool names
work whether the agent calls them over MCP tools/call or you run them as CLI
commands.
The safety rails are enforced in the engine, not the tool, so they hold on every surface: cold sends only leave a verified cold subdomain, every send is approval-gated, and
approve_prospects/launch_outreachare flagged destructive so an MCP client asks for confirmation and an autopilot defers to a human.
The tools
| Step | Tool | Class |
|---|---|---|
| Create a campaign draft | create_outreach_campaign | reversible |
| Source + qualify prospects | source_prospects | reversible |
| Draft per-prospect bodies | draft_all_pending (or draft_outreach_body) | reversible |
| Read the ranked review queue | list_review_queue | read-only |
| Approve / deny prospects | approve_prospects / deny_prospects | approve = destructive |
| Launch the campaign | launch_outreach | destructive |
| Check the funnel | outreach_campaign_status | read-only |
| Triage replies | outreach_replies_digest | read-only |
(Personalized warm broadcasts use the sibling tools personalize_campaign,
list_personalized_review, and approve_personalized — same review-queue shape, fed
by first-party usage stats instead of cold signal.)
Over MCP
Connect an MCP client (see Connect an MCP client); the
16 campaign/outreach tools appear in tools/list automatically. A run is a sequence
of tools/call requests — the agent reads each structured result and decides the
next step:
// 1. Create the campaign draft (nothing sends).
{ "method": "tools/call", "params": {
"name": "create_outreach_campaign",
"arguments": { "name": "YC W26 founders", "brief": "Seed-stage B2B founders hiring eng",
"sendingDomainId": "sd_cold", "fromEmail": "hi@cold.acme.dev" } } }
// 2. Source + qualify prospects into the campaign.
{ "method": "tools/call", "params": {
"name": "source_prospects",
"arguments": { "campaignId": "oc_123", "domains": ["acme.dev", "globex.io"] } } }
// 3. Draft a personalized body for every sourced prospect.
{ "method": "tools/call", "params": {
"name": "draft_all_pending", "arguments": { "campaignId": "oc_123" } } }
// 4. Read the ranked review queue (hottest first) and pick winners.
{ "method": "tools/call", "params": {
"name": "list_review_queue", "arguments": { "campaignId": "oc_123", "stage": "drafted" } } }
// 5. Approve the good ones (destructive — the MCP client confirms first).
{ "method": "tools/call", "params": {
"name": "approve_prospects", "arguments": { "campaignId": "oc_123",
"prospectIds": ["p_1", "p_2", "p_5"] } } }
// 6. Launch — gated to the verified cold subdomain; sends fire on the throttled tick.
{ "method": "tools/call", "params": {
"name": "launch_outreach", "arguments": { "id": "oc_123" } } }
// 7. Watch it. Replies thread into the campaign-tagged Inbox.
{ "method": "tools/call", "params": {
"name": "outreach_campaign_status", "arguments": { "id": "oc_123" } } }
{ "method": "tools/call", "params": {
"name": "outreach_replies_digest", "arguments": { "id": "oc_123" } } }
Over the CLI
The same tools are generated tinbox commands (snake_case → kebab-case). Every
command takes --json for a stable, agent-parseable result, and the destructive
ones require --yes:
# 1. Create the campaign draft.
tinbox create-outreach-campaign --name "YC W26 founders" \
--brief "Seed-stage B2B founders hiring eng" \
--sendingDomainId sd_cold --fromEmail hi@cold.acme.dev --json
# 2. Source + qualify.
tinbox source-prospects --campaignId oc_123 --domains acme.dev,globex.io --json
# 3. Draft every pending body.
tinbox draft-all-pending --campaignId oc_123 --json
# 4. Review the ranked queue.
tinbox list-review-queue --campaignId oc_123 --stage drafted --json
# 5. Approve (destructive → --yes required).
tinbox approve-prospects --campaignId oc_123 --prospectIds p_1,p_2,p_5 --yes --json
# 6. Launch (destructive → --yes; engine rejects a warm/unverified domain).
tinbox launch-outreach --id oc_123 --yes --json
# 7. Status + replies digest.
tinbox outreach-campaign-status --id oc_123 --json
tinbox outreach-replies-digest --id oc_123 --json
Over the SDK
In TypeScript, the same operations are ergonomic aliases on tinbox.tools.*
(generated 1:1 from the manifest), or the full typed router at tinbox.api.*:
const oc = await tinbox.tools.createOutreachCampaign({ name: "YC W26 founders", brief });
await tinbox.tools.sourceProspects({ campaignId: oc.id, domains: ["acme.dev"] });
await tinbox.tools.draftAllPending({ campaignId: oc.id });
const queue = await tinbox.tools.listReviewQueue({ campaignId: oc.id, stage: "drafted" });
await tinbox.tools.approveProspects({ campaignId: oc.id, prospectIds: ["p_1", "p_2"] });
await tinbox.tools.launchOutreach({ id: oc.id });
const digest = await tinbox.tools.outreachRepliesDigest({ id: oc.id });
// list_outreach_campaigns takes no input, so it stays on the typed router:
const all = await tinbox.api.outreach.list.query();
Where replies go
Outreach never hosts the conversation. When a human replies, the thread lands in
your Inbox, tagged campaign:<id>, and the prospect flips to replied (which
stops the sequence). outreach_replies_digest is the triage summary — how many
replied, of how many sent — with a pointer to the Inbox where you (or the
assistant) actually respond. That keeps the cold-outreach machine and the real
conversation cleanly separated.