Skip to content

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

Terminal
npm install startup-builder

Import

$ reads its key from SB_TOKEN; the key selects the namespace.

TypeScript
import { $ } from 'startup-builder'

const offer = await $.Offer.publish({ offer: 'offer_2hzYe1ERo0' })

Modes

CallModeResolves to
await $.Offer.publish({ offer: 'offer_2hzYe1ERo0' })The Verb defaultThe result
await $.try.Offer.publish({ offer: 'offer_2hzYe1ERo0' })tryThe result; nothing is stored
await $.send.Offer.publish({ offer: 'offer_2hzYe1ERo0' })sendThe pending Action
await $.do.Offer.publish({ offer: 'offer_2hzYe1ERo0' })doThe 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.

TypeScript
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.

InterfaceWaits up toAt the cutoff
APIPrefer: wait=<seconds>: 30 by default, 300 at most202 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 mostPrints the pending Action and exits 3.
MCPThe do tool's timeout: 30 seconds by default, 300 at mostdo returns { status: "pending", reason, action, waitingOn }, the same result a call that stops on an Approval returns.
SDKtimeout: 30 seconds by default, 300 at mostawait rejects with the Action, status: 'pending' 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.

TypeScript
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.

TypeScript
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.

OptionDefaultDoes
modeThe Verb defaulttry, send or do
idempotencyKeyGenerated per callMakes a repeat return the first Action
timeout30 secondsStops waiting and rejects with the pending Action; the work goes on. 300 seconds at most
signalNone.An AbortSignal that stops waiting when it fires
maxRetries3How often to retry what time fixes
headersNone.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.

TypeScript
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).

TypeScript
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://api.sb/{namespace}/$.d.ts: only the Verbs its Role may run.