Skip to content

Commit 4797c00

Browse files
committed
docs(spec): JobSchema.body, defineJob and the jobs page state that every door runs a job body
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
1 parent a4c02a4 commit 4797c00

3 files changed

Lines changed: 16 additions & 12 deletions

File tree

‎content/docs/automation/jobs.mdx‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,18 +159,21 @@ export const CloseStaleTasksJob = defineJob({
159159
`,
160160
capabilities: ['api.read', 'api.write', 'log'],
161161
},
162-
handler: 'closeStaleTasks', // deprecated — kept until the runtime runs job bodies (see below)
162+
handler: 'closeStaleTasks', // deprecated and optional — `body` wins when both are present
163163
timeoutMs: 120000,
164164
});
165165
```
166166

167-
<Callout type="warn">
168-
**The runtime does not run a job `body` yet.** Scheduling a job's body is a
169-
separate change that has not landed: until it does, a job is scheduled through
170-
its `handler`, and a job with a `body` and no `handler` is skipped at boot with
171-
a `warn`. `os validate` and `os build` say so wherever a `body` is set. Keep
172-
`handler` beside `body` for now; the `body` is validated today, so it is ready
173-
the day the runtime starts honouring it.
167+
<Callout>
168+
**Every door runs a job `body`.** The boot (a config, or `os start --artifact`)
169+
and `os package install` (on install, and again on every restart) schedule a
170+
job's `body` through the same binder. A `handler` is code: it travels only in the
171+
artifact's runtime module, so it runs only on a boot that loads that module (a
172+
config, or `os start --artifact`). `os package install` therefore refuses a
173+
package whose enabled job has no `body`, with `422 VALIDATION_ERROR` and the
174+
remedy: give the job a `body`, or boot it with `os start --artifact`.
175+
Uninstalling a package stops its scheduled jobs at once, and a reinstall whose
176+
new version drops a job stops that job.
174177
</Callout>
175178

176179
What running in the sandbox means for the code in `source`:

‎content/docs/references/system/job.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const result = CronScheduleSchema.parse(data);
5858
| **description** | `string` | optional | Job description / purpose |
5959
| **schedule** | `{ type: 'cron'; expression: string \| object; timezone?: string } \| { type: 'interval'; intervalMs: integer } \| { type: 'once'; at: string }` | ✅ | Job schedule configuration |
6060
| **handler** | `string` | optional | Handler function name (must match a key in `defineStack({ functions })`) — DEPRECATED, prefer `body`. When both are present `body` wins; a job must declare one of the two. |
61-
| **body** | `{ language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Job body — a sandboxed JS (L2) body, the same shape hooks and actions use; an expression (L1) body is refused, because a job runs for its effects and an expression has none. Preferred over `handler`: when both are present `body` wins. It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. The runtime binder that schedules job bodies has not landed yet: until it does a job runs through `handler`, so keep `handler` beside `body`. |
61+
| **body** | `{ language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Job body — a sandboxed JS (L2) body, the same shape hooks and actions use; an expression (L1) body is refused, because a job runs for its effects and an expression has none. Preferred over `handler`: when both are present `body` wins. It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body`. |
6262
| **retryPolicy** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = min(backoffMs * backoffMultiplier^(retry-1), maxRetryDelayMs), optionally jittered) up to maxRetries retries after the initial attempt. Omit the block for a single attempt; declaring it without `maxRetries` also means no retry since 17.0.0 — state a count to opt in. |
6363
| **timeoutMs** | `integer` | optional | Per-attempt time limit in milliseconds; an over-limit run is recorded with execution status "timeout". A `handler` run is abandoned, not forcibly cancelled. For a job with a `body` this is the ONE time limit: one attempt is one sandbox invocation, the runtime bounds that invocation by this value, and the body shape's own `timeoutMs` (capped at 30000 for hooks and actions) is refused on a job — so this key, which has no such cap, is where long-running work states how long it needs. Omit for no per-attempt limit; a `body` run is then still bounded by the sandbox's own default invocation limits. |
6464
| **timeout** | `never` | optional | [REMOVED] `job.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description while the sibling `retryPolicy.backoffMs` spells its own, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |

‎packages/spec/src/system/job.zod.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export const JobSchema = lazySchema(() => strictObject({
267267
+ 'Preferred over `handler`: when both are present `body` wins. '
268268
+ 'It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. '
269269
+ "Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. "
270-
+ 'The runtime binder that schedules job bodies has not landed yet: until it does a job runs through `handler`, so keep `handler` beside `body`.',
270+
+ "Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body`.",
271271
),
272272
retryPolicy: RetryPolicySchema.optional().describe('Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = min(backoffMs * backoffMultiplier^(retry-1), maxRetryDelayMs), optionally jittered) up to maxRetries retries after the initial attempt. Omit the block for a single attempt; declaring it without `maxRetries` also means no retry since 17.0.0 — state a count to opt in.'),
273273
// Renamed from `timeout` (#14478): the unit (milliseconds) lived only in the
@@ -312,8 +312,9 @@ export type JobParsed = z.infer<typeof JobSchema>;
312312
* source: "const open = await ctx.api.object('task').find({ where: { status: 'open' } }); ctx.log.info('open tasks', { count: open.length });",
313313
* capabilities: ['api.read', 'log'],
314314
* },
315-
* // Deprecated, kept beside `body` until the runtime binder runs job bodies:
316-
* // must be registered in defineStack({ functions: { syncMetadata: () => ... } })
315+
* // Deprecated and optional beside `body`, which wins when both are present. It
316+
* // names a defineStack({ functions }) entry, which only a boot that loads the
317+
* // artifact's runtime module (a config, or `os start --artifact`) can run.
317318
* handler: 'syncMetadata',
318319
* });
319320
* ```

0 commit comments

Comments
 (0)