Job schemas of the ObjectStack System Protocol: CronSchedule, IntervalSchedule, Job and 5 more — each property with its type, default and a TypeScript example.
Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezone
string
optional (default: "UTC")
IANA time zone the cron expression is evaluated in (e.g., "America/New_York"). Refused at parse unless it is a member of iana_time_zone — the same closed domain and the same Intl.DateTimeFormat membership probe sys_job.timezone is written against; a UTC offset such as "UTC+8" is not one.
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.
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.
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.
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.
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; --write applies the ones it can prove, and you apply the rest by hand.
Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezone
string
optional (default: "UTC")
IANA time zone the cron expression is evaluated in (e.g., "America/New_York"). Refused at parse unless it is a member of iana_time_zone — the same closed domain and the same Intl.DateTimeFormat membership probe sys_job.timezone is written against; a UTC offset such as "UTC+8" is not one.
Name of the mapping whose connectorSource this job pulls on its schedule — a mapping the same stack declares, with a connectorSource (refused at defineStack / os validate otherwise). The mapping says where the rows come from, the field map, the write mode and the match key; the job says when.
Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in.
backoffMs
integer
optional (default: 1000)
Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultiplier
number
optional (default: 1)
Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMs
integer
optional (default: 30000)
Ceiling for a single backoff delay (ms)
jitter
boolean
optional (default: false)
Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMs
never
optional
[REMOVED] retryDelayMs was removed in @objectstack/spec 17.0.0 — the retry policy now has ONE spelling for its base delay across every surface that carries it: job.retryPolicy, a try_catch node's retry and flow.errorHandling. Rename the key to backoffMs; the value (milliseconds before the first retry) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in.
backoffMs
integer
optional (default: 1000)
Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultiplier
number
optional (default: 1)
Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMs
integer
optional (default: 30000)
Ceiling for a single backoff delay (ms)
jitter
boolean
optional (default: false)
Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMs
never
optional
[REMOVED] retryDelayMs was removed in @objectstack/spec 17.0.0 — the retry policy now has ONE spelling for its base delay across every surface that carries it: job.retryPolicy, a try_catch node's retry and flow.errorHandling. Rename the key to backoffMs; the value (milliseconds before the first retry) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezone
string
optional (default: "UTC")
IANA time zone the cron expression is evaluated in (e.g., "America/New_York"). Refused at parse unless it is a member of iana_time_zone — the same closed domain and the same Intl.DateTimeFormat membership probe sys_job.timezone is written against; a UTC offset such as "UTC+8" is not one.