Skip to content

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_outreach are flagged destructive so an MCP client asks for confirmation and an autopilot defers to a human.

The tools

StepToolClass
Create a campaign draftcreate_outreach_campaignreversible
Source + qualify prospectssource_prospectsreversible
Draft per-prospect bodiesdraft_all_pending (or draft_outreach_body)reversible
Read the ranked review queuelist_review_queueread-only
Approve / deny prospectsapprove_prospects / deny_prospectsapprove = destructive
Launch the campaignlaunch_outreachdestructive
Check the funneloutreach_campaign_statusread-only
Triage repliesoutreach_replies_digestread-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.