Skip to content

Work old leads: the run

Work old leads: the files, the agent’s call, the result, what refused, and the command that grades your own agent.

Leads nobody touched in 60 days, and Deals lost on price, worked with one fresh reason each. The run holds a quarter back to prove what it added.

Your timeSpendFirst signalResult
about 4 h asked, 4.2 h measured$48 of enrichment · $31 Worker costday 3day 30
Work old leadsMode try
You, to your agent

Work the 812 leads nobody has touched in 60 days, and the Deals we lost on price.

curl "https://api.sb/leaseline.app/leads?where=status%20%3D%20%22nurture%22%20and%20touchedAt%20%3C%20%222026-08-20%22&total=true" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl "https://api.sb/leaseline.app/actions?where=status%20%3D%20%22pending%22%20and%20reason%20%3D%20%22awaitingInput%22%20and%20by%20%3D%20%22prospector%22&limit=20" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl -X POST "https://api.sb/leaseline.app/actions/action_412/redirect?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"changes":{"body":"Since you asked in May: Leaseline now sends each owner their own renewal notice."}}'

curl -X POST "https://api.sb/leaseline.app/messages/message_51/schedule?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-21T14:00:00Z"}'

curl -X POST "https://api.sb/leaseline.app/leads/lead_88/assign?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"owner":"worker_3"}'

curl -X POST "https://api.sb/leaseline.app/approvals/approval_9/approve?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note":"$48 of enrichment this month"}'

# Not live yet (gaps #2)
curl -X POST "https://api.sb/leaseline.app/campaigns/lead-reactivation/start?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"budget":{"amount":48,"currency":"USD","per":"month"}}'
import { $ } from 'startup-builder'
const sb = $.with({ namespace: 'leaseline.app' })

const { total } = await sb.Lead.list({ where: 'status = "nurture" and touchedAt < "2026-08-20"', total: true })   // 812 Leads, plus Deals lost on price or a missing feature
const { actions: drafts } = await sb.Action.list({ where: 'status = "pending" and reason = "awaitingInput" and by = "prospector"', limit: 20 })   // wave 1: 140 drafts, each with one fresh reason
await sb.Action.redirect({ action: 'action_412', changes: { body: 'Since you asked in May: Leaseline now sends each owner their own renewal notice.' } }, { mode: 'try' })   // Dana reads 20 and redirects 3; each edit becomes a test it must pass
await sb.Message.schedule({ message: 'message_51', at: '2026-10-21T14:00:00Z' }, { mode: 'try' })
// 422 · Harbor Property Co. holds an active Subscription; the Message to its office manager did not send.
// customer-collision: Send it to their CSM instead: the Contact leaves the List. · then Lead.assign
await sb.Lead.assign({ lead: 'lead_88', owner: 'worker_3' }, { mode: 'try' })   // Northgate Rentals wrote "interested, send times": routed to the AE in 40 s
await sb.Approval.approve({ approval: 'approval_9', note: '$48 of enrichment this month' }, { mode: 'try' })
return await sb.Campaign.start({ campaign: 'lead-reactivation', budget: { amount: 48, currency: 'USD', per: 'month' } }, { mode: 'try' })   // the whole run as one record · Not live yet, gaps #2
npx startup-builder do Lead.list --json '{"where":"status = \"nurture\" and touchedAt < \"2026-08-20\"","total":true}'
npx startup-builder do Action.list --json '{"where":"status = \"pending\" and reason = \"awaitingInput\" and by = \"prospector\"","limit":20}'
npx startup-builder do Action.redirect --json '{"action":"action_412","changes":{"body":"Since you asked in May: Leaseline now sends each owner their own renewal notice."}}' --try
npx startup-builder do Message.schedule --json '{"message":"message_51","at":"2026-10-21T14:00:00Z"}' --try
npx startup-builder do Lead.assign --json '{"lead":"lead_88","owner":"worker_3"}' --try
npx startup-builder do Approval.approve --json '{"approval":"approval_9","note":"$48 of enrichment this month"}' --try
npx startup-builder do Campaign.start --json '{"campaign":"lead-reactivation","budget":{"amount":48,"currency":"USD","per":"month"}}' --try   # Not live yet (gaps #2)
{
  "name": "do",
  "arguments": {
    "code": "const sb = $.with({ namespace: 'leaseline.app' })\nconst { total } = await sb.Lead.list({ where: 'status = \"nurture\" and touchedAt < \"2026-08-20\"', total: true })   // 812 Leads, plus Deals lost on price or a missing feature\nconst { actions: drafts } = await sb.Action.list({ where: 'status = \"pending\" and reason = \"awaitingInput\" and by = \"prospector\"', limit: 20 })   // wave 1: 140 drafts, each with one fresh reason\nawait sb.Action.redirect({ action: 'action_412', changes: { body: 'Since you asked in May: Leaseline now sends each owner their own renewal notice.' } }, { mode: 'try' })   // Dana reads 20 and redirects 3; each edit becomes a test it must pass\nawait sb.Message.schedule({ message: 'message_51', at: '2026-10-21T14:00:00Z' }, { mode: 'try' })\n// 422 · Harbor Property Co. holds an active Subscription; the Message to its office manager did not send.\n// customer-collision: Send it to their CSM instead: the Contact leaves the List. · then Lead.assign\nawait sb.Lead.assign({ lead: 'lead_88', owner: 'worker_3' }, { mode: 'try' })   // Northgate Rentals wrote \"interested, send times\": routed to the AE in 40 s\nawait sb.Approval.approve({ approval: 'approval_9', note: '$48 of enrichment this month' }, { mode: 'try' })\nreturn await sb.Campaign.start({ campaign: 'lead-reactivation', budget: { amount: 48, currency: 'USD', per: 'month' } }, { mode: 'try' })   // the whole run as one record · Not live yet, gaps #2",
    "mode": "try"
  }
}
Response200application/json
{
  "$id": "https://api.sb/leaseline.app/campaigns/lead-reactivation",
  "$type": "Campaign",
  "play": "play:lead-reactivation@3",
  "audience": {
    "selected": 812,
    "afterSuppression": 800
  },
  "experiment": {
    "metric": "reactivated-pipeline",
    "unit": "per-100",
    "heldBack": 200,
    "decision": "ship"
  },
  …
sample · 1 Not live yetPreview
Not live yet · gaps #2 · today: POST /{namespace}/campaigns and POST /{namespace}/processes/{process}/pause run today; a Stop reads as a Process pause.

The files

audiences/lapsed-leads.mdx
---
query: leads where status = 'nurture' and touchedAt older than 60d, plus deals lost more than 90d ago with lostReason in [price, missing-capability]
fill: [enrichment]
---
plays/lead-reactivation.mdx
---
audience: lapsed-leads
approval: first-send
---

Every Message cites one fresh reason with its public source. A closed-lost Deal gets a Message only when a shipped Change or Offer term answers its lost reason.
npx startup-builder show plays/lead-reactivation
audience lapsed-leads · approval first-send (catalog: delay-window; a file only tightens)   from your file   plays/lead-reactivation.mdx:2-3 (Ask, 2026-10-20)
family demand · process lead-nurture · holdout 0.25 · sequence reactivation-round · worker prospector · stops bounce, complaint, reply-floor, unanswered-positive   from catalog   play:lead-reactivation@3

The Worker’s call

DecisionValue
Chosedrop the Offer-term reason
Why0.6 replies per 100 against 3.1 for shipped Changes
Fromboth reasons · shipped Changes only
Expectsreactivated-pipeline ≥ 2.5 per 100
Reopens whena new Offer term ships
Byagents/sales/prospector
Decision · sample
{
  "options": [
    "both reasons",
    "shipped Changes only"
  ],
  "choice": "drop the Offer-term reason",
  "reason": "0.6 replies per 100 against 3.1 for shipped Changes",
  "by": "agents/sales/prospector",
  "stakes": "low",
  "on": "schedule: daily",
  "expected": "reactivated-pipeline ≥ 2.5 per 100",
  "reopensWhen": "a new Offer term ships"
}

Result

+18 Deals · +$1,388 ARR projected · ■■□□ Early sign · n 600 · path → arr · $48 spend · $31 Worker cost · 4.2 h

SelectedAfter suppressionHeld backExtra DealsProjectedConfidence
812800200+18+$1,388 ARR■■□□ Early sign · n 600

sample · leaseline.app · these figures replay from the records until a capture replaces them

What refused

422RefusedHarbor Property Co. holds an active Subscription; the Message to its office manager did not send.
CheckFixThen
customer-collision fail · Not live yet, gaps #1Send it to their CSM instead: the Contact leaves the List.POST /{namespace}/leads/{lead}/assign
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Harbor Property Co. holds an active Subscription; the Message to its office manager did not send.",
  "checks": [
    {
      "check": "customer-collision",
      "status": "fail",
      "doc": "/code/refusals#check-customer-collision",
      "fix": "Send it to their CSM instead: the Contact leaves the List.",
      "then": "Lead.assign"
    }
  ]
}

It says no, says why, and names the fix.

It says no, says why, and names the fix.

CheckWhat it refusesFixThen
customer-collisiona new-lead send to a Contact with an active SubscriptionSend it to their CSM instead: the Contact leaves the List.POST /{namespace}/leads/{lead}/assign

A Stop pauses the whole run, where a Check refuses one send: unanswered-positive.

unanswered-positive · Stop · sample
{
  "event": "Campaign.paused",
  "reason": {
    "limit": "unanswered-positive",
    "observed": "64m",
    "bound": "60m"
  },
  "task": "tasks/task_88"
}

Run it with your agent

The two connect lines
claude mcp add --transport http salesbuilder-docs https://salesbuilder.dev/mcp
claude mcp add --transport http startup-builder https://api.sb/mcp
Who runs itScoreRefusals recoveredChecks hitModel spend
The built-in Worker · sample3.0 Deals per 1002 of 23$1.10
Grade your agent on the same run
npx startup-builder eval --scenario reactivation-812 --agent "claude -p"

Compiled, not written: edit its page, then regenerate. Every step runs from a sentence or a line of code. In your words: the CRM words you know, and where each lives here.

Contents