Skip to content

Commit b7ee914

Browse files
committed
docs(automation): document structured errors from ctx.api in hook bodies
The docs-drift check on #3937 flagged `automation/hook-bodies.mdx`. Nothing there was made stale by this branch, but the page is the reference for what a sandboxed body gets from `ctx.api`, and this branch gives it something new: a rejected call now carries `code` and `fields` alongside `name`/`message`. Adds a short "Errors from ctx.api" section under nested cross-object writes, covering the two properties, the worked `VALIDATION_FAILED` case, the fact that the passthrough is a deliberate ALLOWLIST (body code can read anything on a rejection, so host errors must not arrive with driver state or record payloads attached), and that an escaping or re-thrown error keeps the payload on the way back out to the action's HTTP response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeZ7DmSKjiKmQzYh8L5CJS
1 parent f30370c commit b7ee914

1 file changed

Lines changed: 25 additions & 0 deletions

File tree

content/docs/automation/hook-bodies.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,31 @@ Per-invocation budgets default to **250ms** (hooks) / **5000ms** (actions) of **
151151

152152
A body may write *other* objects — e.g. `await ctx.api.object('parent').update({ ... })` from a child's `afterInsert`/`afterUpdate` (requires `api.write`). The target's own hooks fire too: the nested write runs in a **fresh sandbox VM** while the calling body is suspended, and this composes to any depth. This is the natural "when a child changes, roll the total up to the parent" automation — it does **not** need a denormalized, hand-maintained mirror field. Because each body's budget is **CPU time** (ADR-0102), the caller is **not** charged for the nested write's own run — so the stock 250ms default comfortably covers deep rollup chains, and you rarely need to raise `timeoutMs` (the spec still permits up to 30_000ms for a genuinely CPU-heavy body).
153153

154+
### Errors from `ctx.api`
155+
156+
A rejected `ctx.api` call gives your body the host error's `name` and `message`, plus two structured properties when the host supplied them:
157+
158+
| Property | Meaning |
159+
|:---|:---|
160+
| `e.code` | The semantic code, e.g. `'VALIDATION_FAILED'` |
161+
| `e.fields` | Per-field validation envelopes — `{ field, code, message }[]` |
162+
163+
```js
164+
try {
165+
await ctx.api.object('invoice').update({ id: input.id, status: 'sent' });
166+
} catch (e) {
167+
if (e.code === 'VALIDATION_FAILED') {
168+
// e.fields → [{ field: 'issued_on', code: 'required', message: 'issued_on is required' }]
169+
throw e; // re-throwing keeps the payload; see below
170+
}
171+
throw e;
172+
}
173+
```
174+
175+
Nothing else crosses into the VM. That is a deliberate **allowlist**, not an oversight: host errors routinely carry driver state, connection details or whole record payloads, and anything reachable on a rejection is readable by body code.
176+
177+
An error your body lets escape — or re-throws — keeps `code` and `fields` on the way back out too, so an action's HTTP response can carry them (`data.code` / `data.fields`) and a form can highlight the offending input rather than only raising a toast. Errors you construct yourself are treated the same way: set `e.code` before throwing and it reaches the caller.
178+
154179
## L3 — Compiled modules (intentionally disabled)
155180

156181
An earlier design allowed the CLI to emit a sibling `objectstack-runtime.<hash>.mjs` that `objectos` would `import()` at runtime. We removed that path because:

0 commit comments

Comments
 (0)