Debugging
Every response names its request, and the request log follows it through every check, the Action it started, the Events it emitted and the logs it wrote.
Every response from api.sb carries a request id. From that id, the request log shows each check, the Action it started, the Events it emitted and the logs it wrote.
Start from the request id
Every response carries a Request-Id header, and every problem document repeats it as requestId. The CLI prints it under any error, and the SDK puts it on every result and thrown error as requestId.
shipwatch.dev is a Dataset API of parcel tracking events, sold to online stores by the call. A store reports that switching to the higher plan fails on shipwatch's billing page. The developer copies one line from the failing response, Request-Id:, and needs nothing else.
npx startup-builder show /requests/ req_7Hk2The request log
The request log holds every request a namespace received, in test and live. It opens in startups.studio, for one Startup or across a whole Portfolio. show prints the same record:
{
"$id": "https: //api.sb/ shipwatch.dev/ requests/ req_7Hk2",
"at": "2026-10-22T14: 03: 11Z",
"method": "POST",
"path": "/subscriptions/ subscription_12/ switch",
"status": 403,
"key": "pk_live_…a91f",
"durationMs": 38,
"gates": [
{ "gate": "key", "verdict": "pass" },
{ "gate": "permission", "verdict": "fail", "rule": "Publishable keys run only public Verbs, and Subscription.switch is not public" }
],
"action": null,
"events": []
}gates lists the checks in the order they ran, and the first fail is the answer. Here the billing page sends shipwatch's publishable key from the browser, and that key runs only public Verbs. The fix is to make the call from shipwatch's server, with a secret key, for the signed-in Customer.
Gates, in order
A call passes up to six gates before its Verb runs. Each verdict names the rule that decided it.
| Gate | It checks | A failure returns |
|---|---|---|
key | The key exists, is current and reaches this namespace | unauthorized |
input | The input matches the Verb's schema | invalid-input |
permission | The caller's Permissions cover this Verb | forbidden |
rate | The caller is under its request rate | rate-limited |
limit | Spend and counts stay inside every Limit and Mandate | An Approval wait, or limit-exceeded |
state | The record's current state allows the Verb | A refused Action |
The first four reject at the door, so no Action exists. The last two create one, and the request log links to it.
An Action that waits
A call that passed every gate but has not finished is a pending Action. Its reason says what it waits for, and the Action records every attempt:
npx startup-builder show /actions/ action_31awaitingSource means an outside service is down, and the platform retries it. awaitingWorker points at an open Task.
awaitingTime shows when its delay window closes. To give up, cancel the Action; to run a failed one again, retry it.
Traces
Send a W3C traceparent header, and the platform joins your trace instead of starting one. Every step under the call reports into it: Workflows, model calls, sandboxes and outbound requests.
A slow call then reads as one timeline in your own tracing tool. Code outside the platform sends spans over OTLP into the same trace. Observability is the capability behind traces and logs.
Logs
Code a Startup runs writes logs, tagged with the request id and the Action. Logs are kept forever.
npx startup-builder logs tail --request req_7Hk2
npx startup-builder logs tail --level error --follow--follow keeps the stream open. An Agent that debugs reads the same request log, Actions and logs through MCP, with the same ids.
Reproduce in test
To reproduce a failure safely, run the same call in test with the Mode try. It passes the same gates and stores nothing. The request log of a try call shows the verdicts exactly as a recorded call would.
Next
- Errors and testing: every error and how to force it in
test. - Modes: the reasons an Action waits, and what each one waits for.
- Keys: secret, publishable and session keys.