Skip to content

Goal mode

Give any Workflow, agent task or Service a goal and a budget: try returns the priced plan free, and send runs it with a receipt.

Dry run
Free
SDK
startup-builder@0.3

The overview, in a founder's words: Give it a goal: the plan, the budget, the receipt.

Two inputs turn any call into a goal

goal and budget sit beside a call's own inputs. Each typed input you also give is a hard rule the agent never relaxes. Ad money is spend, never budget, so the budget means one thing: the Limit on the work.

TypeScript
type Money = { amount: number; currency: 'USD' }

/** Every Workflow, agent task and Service accepts these in place of, or beside, its typed inputs. */
type GoalModeInput = {
  goal: string                        // the outcome, in your words
  budget: Money                       // the Limit on this Action: calls, model tokens, browser steps
  spend?: Money                       // money paid to a Venue (ad spend, a paid listing), under your Mandate; default 0
  by?: string                         // the deadline, ISO 8601; default now + 7 days; at most 31 days
  ask?: 0 | 1                         // questions it may ask before money moves; default 1
  plan?: GoalPlan                     // start from this plan (what a dry run returned) instead of planning again
  // ...plus any of the call's own typed inputs. Each one you give is a hard rule.
}

type GoalPursueInput = Omit<GoalModeInput, 'goal'> & {
  objective: string                   // the hub Goal's objective
  inputs?: Record<string, unknown>    // records you already hold: { offer, icp, audience, mailbox }
  use?: string[]                      // Verbs it may call; default every composition the seat may call
}

Two doors: one call with a goal, or one goal across calls

Most sentences match one composition's guide, so the goal goes to that call. An outcome that needs several, such as booking calls, goes to Goal.pursue, which creates the hub Goal and pursues one Key Result inside one Action. Its Event carries met.

TypeScript
import { $ } from 'startup-builder'

// One call, with a goal: most asks match one composition's guide
const plan = await $.try.Audience.build({
  goal: '50 verified heads of operations at US property managers with 100 to 1,000 units',
  budget: { amount: 4, currency: 'USD' },
})

// An outcome that spans calls: Goal.pursue composes them inside one Action
const booked = await $.try.Goal.pursue({
  objective: 'Book 5 calls with property managers this month from cold email. Send only from the warmed domain.',
  budget: { amount: 40, currency: 'USD' },
  by: '2026-11-26',
  use: ['Audience.build', 'Prospect.reach', 'Lead.respond', 'Meeting.book', 'Deal.brief'],
})
DoorWhenRefuses
Any composition with { goal, budget }The sentence matches one guide’s says/errors/no-plan when no plan fits the inputs
Goal.pursueThe outcome spans compositions, or matches several guidesA horizon past 31 days, with the fix: PlayMix.compile chooses Plays week by week

How a sentence finds its call

StepWhat happens
1. MatchYour sentence is matched against every guide’s says, and its not list drops look-alikes. One match goes to that call; several, or none, go to Goal.pursue with those calls in use.
2. ReadEach phrase becomes a row of reads. Your words are hard; a filter the agent adds, such as a headcount standing in for units, is soft and may widen.
3. PlanIt starts from the guide’s default program, sizes each step with free counts, prices it at its unit price, and adds one contingency step for the likeliest shortfall.
4. Ask, at most onceOnly when a reading would change the price by more than twice or the result by more than half: “Verified: do catch-all addresses count?”

The plan is what try returns

TypeScript
/** What `try` returns: the dry run. Nothing is stored, and nothing is spent but $0 reads. */
type GoalPlan = {
  goal: { objective: string; keyResult: { metric: string; target: number; by: string } }
  reads: { said: string; means: string; hard: boolean }[]   // hard: from your words, never relaxed
  steps: { n: number; operation: string; count: number; unit: string; unitUsd: number; usd: number
           why: string; if?: string }[]                      // if: a contingency step, priced, not in expected
  estimate: { usd: { expected: number; max: number }; minutes: number; fill: string }
  question?: { text: string; choices?: string[] }            // at most one
  budget: Money & { left: number }
}

To run that exact plan, pass it back as plan. The agent starts from it, and any change of course is still a Decision on the receipt.

TypeScript
const draft = await $.try.Audience.build({ goal, budget })                         // the plan and its estimate, $0
const action = await $.send.Audience.build({ goal, budget, plan: draft })          // runs the plan you saw

A finished goal returns the result and why it stopped

TypeScript
/** What a finished goal returns, beside the call's own output. */
type GoalResult<T> = {
  result: T                           // AudienceBuilt, KeywordMap, ...
  goal: { met: boolean; reached: number; target: number
          stoppedBecause: 'met' | 'budget' | 'deadline' | 'no-more-supply' | 'refused' }
  receipt: Receipt
}
stoppedBecauseMeans
metIt reached the target and stopped, with budget left
budgetThe next call did not fit, and the Approval with its projection was refused or expired; or it reached maxSteps
deadlineIt reached by
no-more-supplyNo route had more at any price, such as every matching company already read
refusedA call it needed was refused, such as an account that needs signing in again

The receipt is a view over one Action

TypeScript
type Receipt = {
  calls: { n: number; operation: string; action: string; status: 'done' | 'failed' | 'canceled' | 'refused'
           count: number; usd: number; why?: string }[]     // MCP do's calls[], plus count, usd and why
  decisions: { $id: string; at: string; options: string[]; choice: string; reason: string; supersedes?: string }[]
  approvals: { $id: string; asked: string; status: 'approved' | 'refused' | 'expired' }[]
  budget: { amount: number; spent: number; left: number; agentUsd: number }   // agentUsd: the planner's own model cost
  spend?: { amount: number; spent: number; venue: string }
  time: { startedAt: string; endedAt?: string; activeMinutes: number }
}

Its totals equal the sum of calls[].usd, and budget.left is the budget less that sum. The planner's own model cost is agentUsd, on its own line, usually 2¢ to 10¢ a goal. The receipt is never a File: it is the Action, its child calls, their Cost entries and its Decisions.

Progress streams as Events on the goal’s Action

Every frame carries budget: { spent, left }. Filter on on, the direct parent; grandchildren, such as the routes inside a find, arrive inside their parent's Event. Over MCP, pass a progressToken on the do call for one progress notification per frame, or subscribe to the same events URL as a resource.

TypeScript
const action = await $.send.Audience.build({ goal, budget })
for await (const e of $.stream('*', { where: `on = ${action.$id}` })) {
  console.log(e.name, e.data.budget?.spent)        // Decision.created 0 · Prospect.verified 0.0005 · ...
  if (e.name === 'Audience.built') break
}
Terminal
curl -N "https://api.sb/leaseline.app/events?where=on%20%3D%20act_7Kq2" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Accept: text/event-stream"
EventWhen
Decision.createdThe plan is fixed; the plan is its data
Each child’s Event, such as Prospect.verifiedEach call finishes, with count and usd
Decision.supersededIt changes course, with the options and the reason
Approval.createdIt asks its one question, or the next call would cross the budget
The call’s own Event, such as Audience.builtIt ends, with the receipt

Failure, retries and steering keep every call once

CaseWhat happens
IdempotencyThe goal’s Action takes your Idempotency-Key; each child call takes <key>-<n>. A retry after a timeout replays finished calls and never charges twice.
A child call failsThree retries with backoff, then the next route for the same read, which no Decision records. Then it is an input to the next Decision: switch calls, skip the items, or stop.
Action.cancelStops the goal. Calls made stay on the receipt and are charged; pending sends and posts are canceled at $0.
Action.redirectReplaces inputs, such as a new by or a narrower goal; the agent plans again as a superseding Decision.
/errors/no-planThe planner returned no valid plan. No money moves before a plan exists. The fix: call it with its typed inputs.
maxStepsDefault 200 child calls. A goal that reaches it stops with stoppedBecause: budget and the reason “step limit”: a guard against loops, not cost.

The four forms, side by side

The SDK, the API, the CLI and MCP take the same goal. Each shows the dry run; send the same body with mode=send or --send to run it.

TypeScript
// SDK: npm i startup-builder@0.3 · SB_TOKEN=sk_test_... reaches the test environment for $0
import { $ } from 'startup-builder'
const goal = '50 verified heads of operations at US property managers with 100 to 1,000 units'
const budget = { amount: 4, currency: 'USD' }
const plan = await $.try.Audience.build({ goal, budget })
const done = await $.do.Audience.build({ goal, budget, plan }, { idempotencyKey: 'leaseline-audience-build-1' })
Terminal
# API
curl -X POST "https://api.sb/leaseline.app/audiences/build?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"goal":"50 verified heads of operations at US property managers with 100 to 1,000 units","budget":{"amount":4,"currency":"USD"}}'

# CLI: --do is the default for writes, so a goal names its Mode
npx startup-builder do Audience.build --try \
  --data '{"goal":"50 verified heads of operations at US property managers with 100 to 1,000 units","budget":{"amount":4,"currency":"USD"}}'
JSON
{ "name": "do", "arguments": {
    "mode": "try",
    "code": "return await $.Audience.build({ goal: '50 verified heads of operations at US property managers with 100 to 1,000 units', budget: { amount: 4, currency: 'USD' } })" } }

Contents