ObjectStackObjectStack

Job schema — System Protocol reference

Job schemas of the ObjectStack System Protocol: CronSchedule, IntervalSchedule, Job and 5 more — each property with its type, default and a TypeScript example.

Source: packages/spec/src/system/job.zod.ts

TypeScript Usage

import { CronScheduleSchema, IntervalScheduleSchema, JobSchema, JobExecutionSchema, JobExecutionStatus, OnceScheduleSchema, RetryPolicySchema, ScheduleSchema } from '@objectstack/spec/system';
import type { CronSchedule, IntervalSchedule, Job, JobExecution, JobExecutionStatus, OnceSchedule, RetryPolicy, Schedule } from '@objectstack/spec/system';

// Validate data
const result = CronScheduleSchema.parse(data);

CronSchedule

Properties

PropertyTypeRequiredDescription
type'cron'✅
expressionstring | { dialect: 'cron'; source?: string; ast?: any; meta?: object }✅Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezonestringoptional (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.

IntervalSchedule

Properties

PropertyTypeRequiredDescription
type'interval'✅
intervalMsinteger✅Interval in milliseconds

Job

Properties

PropertyTypeRequiredDescription
namestring✅Job name (snake_case)
labelstringoptionalHuman-readable label
descriptionstringoptionalJob description / purpose
schedule{ type: 'cron'; expression: string | object; timezone?: string } | { type: 'interval'; intervalMs: integer } | { type: 'once'; at: string }✅Job schedule configuration
handlerstringoptionalHandler 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; … }optionalJob 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 }optionalPull 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.
organizationstringoptionalOrganization 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; … }optionalRetry 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.
timeoutMsintegeroptionalPer-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.
timeoutneveroptional[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.
enabledbooleanoptional (default: true)Whether the job is enabled
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

Nested Shape: Job.schedule[type='cron']

PropertyTypeRequiredDescription
type'cron'✅
expressionstring | { dialect: 'cron'; source?: string; ast?: any; meta?: object }✅Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezonestringoptional (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.

Nested Shape: Job.schedule[type='interval']

PropertyTypeRequiredDescription
type'interval'✅
intervalMsinteger✅Interval in milliseconds

Nested Shape: Job.schedule[type='once']

PropertyTypeRequiredDescription
type'once'✅
atstring✅ISO 8601 datetime when to execute

Nested Shape: Job.body

PropertyTypeRequiredDescription
language'js'✅
sourcestring✅Function body source
capabilitiesEnum<'api.read' | 'api.write' | 'api.transaction' | 'crypto.uuid' | 'log'>[]optional (default: [])Granted capability tokens
timeoutMsintegeroptionalPer-invocation timeout (ms)
memoryMbintegeroptionalPer-invocation memory cap (MB)

Nested Shape: Job.pull

PropertyTypeRequiredDescription
mappingstring✅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.

Nested Shape: Job.retryPolicy

PropertyTypeRequiredDescription
maxRetriesintegeroptional (default: 0)Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in.
backoffMsintegeroptional (default: 1000)Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultipliernumberoptional (default: 1)Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMsintegeroptional (default: 30000)Ceiling for a single backoff delay (ms)
jitterbooleanoptional (default: false)Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMsneveroptional[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.

JobExecution

Properties

PropertyTypeRequiredDescription
jobIdstring✅Job identifier
startedAtstring✅ISO 8601 datetime when execution started
completedAtstringoptionalISO 8601 datetime when execution completed
statusEnum<'running' | 'success' | 'failed' | 'timeout' | 'degraded'>✅Execution status
errorstringoptionalError message if failed
durationMsintegeroptionalExecution duration in milliseconds

JobExecutionStatus

Allowed Values

  • running
  • success
  • failed
  • timeout
  • degraded

OnceSchedule

Properties

PropertyTypeRequiredDescription
type'once'✅
atstring✅ISO 8601 datetime when to execute

RetryPolicy

Properties

PropertyTypeRequiredDescription
maxRetriesintegeroptional (default: 0)Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in.
backoffMsintegeroptional (default: 1000)Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultipliernumberoptional (default: 1)Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMsintegeroptional (default: 30000)Ceiling for a single backoff delay (ms)
jitterbooleanoptional (default: false)Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMsneveroptional[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.

Schedule

Union Options

This schema accepts one of the following structures:

Option 1

Type: cron

Properties

PropertyTypeRequiredDescription
type'cron'✅
expressionstring | { dialect: 'cron'; source?: string; ast?: any; meta?: object }✅Cron expression — cron0 0 * * * for daily at midnight. Build emits {dialect:"cron",source} envelope.
timezonestringoptional (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.

Option 2

Type: interval

Properties

PropertyTypeRequiredDescription
type'interval'✅
intervalMsinteger✅Interval in milliseconds

Option 3

Type: once

Properties

PropertyTypeRequiredDescription
type'once'✅
atstring✅ISO 8601 datetime when to execute


On this page