diff --git a/.changeset/21703-job-body-describe-pull.md b/.changeset/21703-job-body-describe-pull.md new file mode 100644 index 00000000000..7d5c268ea87 --- /dev/null +++ b/.changeset/21703-job-body-describe-pull.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': patch +--- + +`JobSchema.body`'s description now says an enabled `pull` job installs through `os package install` when its `pull` binds and is refused when it does not + +Clause-②: no + +The description said `os package install` refuses an enabled job with no `body`, "a `pull` job excepted: it is data too". Read plainly, that says the install never refuses a `pull` job. That stopped being true when the install-local door (`os package install`, `POST /api/v1/marketplace/install-local`) began refusing an enabled job whose `pull` does not bind, with the same `422 VALIDATION_ERROR` it gives a job whose `body` the declaration refuses. + +The sentence now reads: a `pull` is data too, so an enabled `pull` job is judged by its `pull` instead. It installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. The generated reference page for `job` carries the same text. + +Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old sentence needs the new one. diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index bfbd69a11e2..509dd269d80 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -58,7 +58,7 @@ const result = CronScheduleSchema.parse(data); | **description** | `string` | optional | Job description / purpose | | **schedule** | `{ type: 'cron'; expression: string \| object; timezone?: string } \| { type: 'interval'; intervalMs: integer } \| { type: 'once'; at: string }` | ✅ | Job schedule configuration | | **handler** | `string` | optional | Handler function name (must match a key in `defineStack({ functions })`) — DEPRECATED, prefer `body`. When both are present `body` wins; refused beside `pull`. A job must declare one of `body`, `handler` or `pull`. | -| **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` (a `pull` job excepted: it is data too). Refused beside `pull`. | +| **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`. A `pull` is data too, so an enabled `pull` job is judged by its `pull` instead: it installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. Refused beside `pull`. | | **pull** | `{ mapping: string }` | optional | Pull run form: on each run the platform pulls the named mapping's `connectorSource` (one action call, one response) and writes the rows through the import runner — no code. A refused pull records the run `failed` (retried per `retryPolicy`); a pull whose rows the import runner refused records it `degraded`. Data like `body`, so every door schedules it. Refused beside `body` or `handler`. | | **organization** | `string` | optional | Organization id (sys_organization.id) this job runs as — its `body`'s `ctx.api`, the execution context its `handler` is handed, and its `pull`'s reads and writes alike, as a system run carrying that organization. A scheduled run has no session to inherit one from. Judged at bind by the posture rule scheduled flows use: required under the isolated tenancy posture (a job that declares none is not scheduled); optional under group (undeclared, the run carries no organization and a tenant-scoped write it makes is refused); not required under single (the install's one organization is resolved beneath each write). Where declared, it is the organization the run acts as on every posture. | | **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. | diff --git a/packages/spec/src/system/job.zod.ts b/packages/spec/src/system/job.zod.ts index d84b4af1f8a..02454615c12 100644 --- a/packages/spec/src/system/job.zod.ts +++ b/packages/spec/src/system/job.zod.ts @@ -319,7 +319,8 @@ export const JobSchema = lazySchema(() => strictObject({ + '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` (a `pull` job excepted: it is data too). " + + "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`. " + + 'A `pull` is data too, so an enabled `pull` job is judged by its `pull` instead: it installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. ' + 'Refused beside `pull`.', ), /**