SDK spec
What the startup-builder package exports, and what each call resolves to.
The startup-builder package exports $, a typed client with one method per operation on every Noun.
Install
npm install startup-builderImport
$ reads its key from SB_TOKEN; the key selects the namespace.
import { $ } from 'startup-builder'
const offer = await $.Offer.publish({ offer: 'offer_2hzYe1ERo0' })Modes
| Call | Mode | Resolves to |
|---|---|---|
await $.Offer.publish({ offer: | The Verb default | The result |
await $.try.Offer.publish({ offer: | try | The result; nothing is stored |
await $.send.Offer.publish({ offer: | send | The pending Action |
await $.do.Offer.publish({ offer: | do | The result, after the Action finishes |
Results and actions
await resolves to the result when the Action ends done. It rejects with the Action when it ends failed, refused or canceled, and with the pending Action when the wait is cut off (below). .settled never rejects for the Action's status: it resolves to the Action once it settles, or as it stands when the wait is cut off.
import { $ } from 'startup-builder'
const action = await $.Offer.publish({ offer: 'offer_2hzYe1ERo0' }).settled
if (action.status === 'done') console.log(action.result)
else if (action.status === 'pending') console.log(action.reason, action.$id) // still running: read it later
else console.log(action.error?.type) // https: //startupbuilder.dev/ errors/ <slug>When a call stops waiting
A call in do Mode, or a Verb whose default is do, waits while its Action is pending: running, waiting out a delay window (awaitingTime), or waiting on an Approval (awaitingAction). The wait has a ceiling. When it passes, the caller gets the pending Action and the work goes on, the same as a send call. Nothing is canceled because the caller stopped waiting.
| Interface | Waits up to | At the cutoff |
|---|---|---|
| API | Prefer:: 30 by default, 300 at most | 202 Accepted with the pending Action in the body and its URL in Location, the response a send call gets. Preference-Applied echoes the wait. |
| CLI | --timeout <seconds>: 30 by default, 300 at most | Prints the pending Action and exits 3. |
| MCP | The do tool's timeout: 30 seconds by default, 300 at most | do returns { status:, the same result a call that stops on an Approval returns. |
| SDK | timeout: 30 seconds by default, 300 at most | await rejects with the Action, status: and its reason; .settled resolves to it. |
To pick the work up again, read the Action at its $id, wait for its Event, or repeat the call with the same Idempotency-Key: the repeat returns the same Action and waits again. A call that waits on an Approval waits the same way, so the cutoff, not the Approval, decides when the caller hears back.
Ids
A record's $id is its URL, and every input that names a record takes it, so a returned record passes straight into the next call. get, update and delete name the record id; every other Verb names it by its Noun. The API spec has the rule.
import { $ } from 'startup-builder'
const offer = await $.Offer.create({ price: { amount: 29, currency: 'USD' }, interval: 'month' })
await $.Offer.publish({ offer: offer.$id })
await $.Offer.update({ id: offer.$id, terms: 'Cancel any time from the billing portal.' })Lists
A list call resolves to one page, an array with the after cursor of the next. .all() walks every page, fetching each as the loop reaches it.
import { $ } from 'startup-builder'
const page = await $.Invoice.list({ where: 'paidAt = null', limit: 50 })
const next = await $.Invoice.list({ where: 'paidAt = null', after: page.after })
for await (const invoice of $.Invoice.list({ where: 'paidAt = null' }).all()) console.log(invoice.$id)Call options
Every method takes an options object after its input. $.with(options) returns a client scoped by namespace, key, env or for, whose calls default to the same options.
| Option | Default | Does |
|---|---|---|
mode | The Verb default | try, send or do |
idempotencyKey | Generated per call | Makes a repeat return the first Action |
timeout | 30 seconds | Stops waiting and rejects with the pending Action; the work goes on. 300 seconds at most |
signal | None. | An AbortSignal that stops waiting when it fires |
maxRetries | 3 | How often to retry what time fixes |
headers | None. | Extra headers, such as traceparent |
Events
Subscribe by Noun and past tense, or by the full Event name. Each handler gets the Event record: $id, $type, name, at, seq and data.
import { $ } from 'startup-builder'
$.on.Offer.published((event) => console.log(event.seq, event.data))
$.on('Offer.published', (event) => console.log(event.name, event.at))Webhooks
verifyWebhook checks a delivery's StartupBuilder-Signature header against the raw body and the Watch's signing secret, and returns the Event, the same record $.on hands over. It throws when no signature matches, or when the newest is older than tolerance seconds (300 by default).
import { verifyWebhook } from 'startup-builder'
export async function POST(request: Request) {
const event = verifyWebhook(await request.text(), request.headers.get('StartupBuilder-Signature') ?? '', process.env.WATCH_SECRET ?? '')
if (event.name === 'Order.delivered') console.log(event.data.order.$id)
return new Response(null, { status: 204 })
}Types
startup-builder.d.ts holds the types of $ for every public operation: each record as a top-level type (Offer), the client and its runtime types in SB (SB.Action, SB.Call), and the project-file helpers (Startup, fn, workflow, cases, env, evaluator, mapping). The subpaths have their own: startup-builder/react.d.ts, startup-builder/test.d.ts, startup-builder/ui.d.ts, startup-builder/globals.d.ts. Each caller also gets its own from https:: only the Verbs its Role may run.