Type to search pages and rules.

Browse pages

Jobs and Queues

Work that outlives a request runs in a Worker: scheduled jobs from a timetable, ingest pipelines over outside records, engine-carried sagas, and reports too heavy for a request, while people clear queued items through claimed listings. This page states how each kind records its work, runs it once, and recovers. A project selects it for recurring, delayed, ingested, carried, heavy, or human-cleared work.

Markdown73 provisions

Stack: any Layer: application

Activation#

Activation scope: local.

Applicable specification kinds: job, use-case, journey, aggregate.

The consumer enables jobs for recurring, delayed, or calendar-based work. Each schedule lives on a job record with its kind, schedule, and run fields, as the automation page states. Selecting it replaces litespec/backend/architecture#declare-the-execution-host-for-work-that-outlives-a-request for scheduled work: a Worker runs the scheduler.

For human queues, a project selects jobs for a use case when the use case lists work items people claim or clears items in bulk. Ordinary reads and single-item commands do not select it.

For ingest, a project selects jobs for a job when the job ingests outside records through drop and enrich steps. A job that drives product sagas or sends notifications does not select it.

For engine-carried sagas, a project selects jobs for a saga aggregate when a workflow engine carries it. A saga driven by reactions or jobs does not select it.

For reports, a use case selects jobs for one documented query or export that the normal request path cannot serve: complex relational SQL, a large result, long processing, output in object storage, or a latency budget beyond a normal request. The use case page carries the measurements that prove the normal path falls short.

Baseline Relationship#

This extension replaces litespec/backend/architecture#declare-the-execution-host-for-work-that-outlives-a-request. The baseline lets a project declare either the WebApi process or a Worker project as the execution host for work that outlives a request. A project selecting jobs runs scheduled work in a Worker outside WebApi request handling, under litespec/ext/jobs#run-scheduled-jobs-in-worker.

The job, setting, and runbook records belong to the core. Schedules bound their retention with setting records and operators recover through runbook records. The request budget and each report bound live in setting records.

A listing query states its claim mode and its score, while a bulk money command states its preview and its confirmBy code. An ingest job lists its steps in run order, names its closed dropReasons set, and states its buffer policy. Each query reading ingested data states the freshness consequence its readers observe. A carried saga sets carriedBy: workflow, maps each engine step to exactly one use case, and states each step's compensation. The engine's retry and timeout configuration stays in Implementation, outside the records. Ordinary queries stay under litespec/backend/persistence#query-through-the-read-session; the report provisions govern only the reads whose cost or duration exceeds that path.

Standards#

Carry the Schedule on the Job Record#

rulerulecheckable
litespec/ext/jobs#carry-the-schedule-on-the-job-record
Statement

A scheduled job MUST carry its schedule in structured form with its timezone on its job record.

Applies toartifact: job

Why

The job record states when the work runs: every or cron with its timezone, so tools read the rhythm instead of parsing prose. A schedule kept outside the record runs with no recorded moment attached, and misfire behavior has no stated time to measure against. The automation page states the record fields, and the schedule-invalid check reports a schedule no tool can read.

Checkchecked by a tool

Checked bylitespec model check reports schedule-invalid (level G) for a job schedule that is neither an every duration nor a cron expression; each scheduled job record states its timezone.

Marked checkable: the tool named here checks it.

Run One Command per Item in Selection#

ruleruleguidance
litespec/ext/jobs#run-one-command-per-item-in-selection
Statement

A job MUST run one command per item in its selection within its batchSize, each in its own unit of work.

Applies toartifact: job

Why

The selection states what each run selects, the batch size bounds how many items one batch takes, and runs names the command run per item. Each item runs as one command in its own unit of work, as litespec/backend/application#issue-per-record-work-from-a-job requires: a failed item replays without replaying its batch.

Checka reviewer answers

Reviewed forEach job run runs one command per item in its selection within its batch size, each in its own unit of work.

Marked guidance: a reviewer answers this question.

Resume Driven Work Through the Cursor#

ruleruleguidance
litespec/ext/jobs#resume-driven-work-through-the-cursor
Statement

A job driving a saga MUST name where resume state lives in its cursor.

Applies toartifact: job

Why

A batch that fails halfway resumes instead of restarting: the cursor holds where the run stopped, beside the saga state it drives. Without it, the next run replays finished items or skips unfinished ones, and a scenario covering the remainder resumes has no stated position to assert.

Checka reviewer answers

Reviewed forEach job driving a saga names its resume position in its cursor, with a scenario covering the remainder after a halfway failure.

Marked guidance: a reviewer answers this question.

Run Manual Jobs Under Their Guard#

rulerulecheckable
litespec/ext/jobs#run-manual-jobs-under-their-guard
Statement

A manual job run MUST stay inside its runsWhere under its guard.

Applies toartifact: job

Why

Manual work is interactive platform work with no aggregate, such as a rehearsal harness, so no model bounds where it runs or who runs it. runsWhere names the deployment scope it may touch and the guard names who may run it and when. The automation page states the record fields, and the job-guard-missing check reports a manual job with no guard.

Checkchecked by a tool

Checked bylitespec model check reports job-guard-missing (level G) for a manual job with no guard; each manual run stays inside its runsWhere.

Marked checkable: the tool named here checks it.

Run Scheduled Jobs in Worker#

ruleruleguidance
litespec/ext/jobs#run-scheduled-jobs-in-worker
Statement

A scheduled job MUST run in Worker outside WebApi request handling.

Applies toartifact: job

Why

Scheduled work runs in a Worker, outside WebApi request handling. The Worker owns the schedule's lifetime and shuts its replicas down in a controlled order. Request handling has neither a scheduler nor a shutdown order for background work.

Checka reviewer answers

Reviewed forWorker sources hold every scheduled job, and WebApi request handling holds none.

Marked guidance: a reviewer answers this question.

Observe Job Cancellation#

ruleruleguidance
litespec/ext/jobs#observe-job-cancellation
Statement

A scheduled job MUST observe cancellation.

Applies toartifact: job

Why

Each job observes cancellation, so an occurrence ends its current step instead of running past the moment an operator asked it to stop.

Checka reviewer answers

Reviewed forEach job handler threads cancellation into its waits and stops its current step when cancellation fires.

Marked guidance: a reviewer answers this question.

Bound Job Duration#

ruleruleguidance
litespec/ext/jobs#bound-job-duration
Statement

A scheduled job MUST complete within a bounded duration.

Applies toartifact: job

Why

Each job bounds its duration, so a Worker stops without stranding work it can no longer watch.

Checka reviewer answers

Reviewed forEach job handler carries a duration bound, and the occurrence store shows completions inside it.

Marked guidance: a reviewer answers this question.

Stop Jobs During Graceful Shutdown#

ruleruleguidance
litespec/ext/jobs#stop-jobs-during-graceful-shutdown
Statement

A scheduled job MUST stop accepting new work during graceful shutdown.

Applies toartifact: job

Why

Each job stops accepting new work during graceful shutdown. An unbounded job turns every deploy into a wait for work that never ends.

Checka reviewer answers

Reviewed forThe Worker shutdown path stops schedule polling before it drains running occurrences.

Marked guidance: a reviewer answers this question.

Store Execution Leases#

ruleruleguidance
litespec/ext/jobs#store-execution-leases
Statement

A scheduled-job store MUST record lease owner, expiry, and fencing value in PostgreSQL.

Applies toartifact: job

Why

Each planned occurrence carries a lease with its owner, expiry, and fencing value in PostgreSQL. The lease names the one replica that may complete that occurrence. The lease field on the job record names the lock each run claims. The outbox extension cites this definition for its own claims.

Checka reviewer answers

Reviewed forThe lease rows in PostgreSQL carry owner, expiry, and fencing value for each occurrence.

Marked guidance: a reviewer answers this question.

Renew Only Owned Leases#

ruleruleguidance
litespec/ext/jobs#renew-only-owned-leases
Statement

A Worker replica MUST renew only its active lease and stop when renewal fails.

Applies toartifact: job

Why

A replica renews only the lease it holds and only while it stays active. A failed renewal means another replica owns the occurrence now. The loser stops instead of completing work it no longer owns.

Checka reviewer answers

Reviewed forReplica logs show no renewal after a failed renewal, and the loser stops its occurrence.

Marked guidance: a reviewer answers this question.

Prevent Duplicate Completion#

ruleruleguidance
litespec/ext/jobs#prevent-duplicate-completion
Statement

A scheduled-job lease MUST prevent two replicas from completing the same occurrence.

Applies toartifact: job

Why

Overlapping replicas must not complete the same occurrence twice. The lease admits one completion per occurrence. Two completions would bill, notify, or reconcile the same planned work twice.

Checka reviewer answers

Reviewed forThe lease admits one completion per occurrence across overlapping replicas.

Marked guidance: a reviewer answers this question.

Generate Monotonic Fencing Values#

ruleruleguidance
litespec/ext/jobs#generate-monotonic-fencing-values
Statement

A scheduled-job store MUST generate each fencing value monotonically per occurrence.

Applies toartifact: job

Why

Each fencing value rises monotonically per occurrence, so a later claim always presents a higher value than an earlier one. A Worker that lost its lease presents a stale value, and litespec/ext/jobs#restrict-completion-by-fencing-value refuses completion on a stale value.

Checka reviewer answers

Reviewed forFencing values in the store rise per occurrence, and a stale value refuses completion.

Marked guidance: a reviewer answers this question.

Scope Occurrences and Leases by Tenant#

ruleruleguidance
litespec/ext/jobs#scope-occurrences-and-leases-by-tenant
Statement

A scheduled-job occurrence and its lease MUST carry tenant scope when the product isolates tenants.

Applies toartifact: job

Why

Products that isolate tenants scope each occurrence and its lease to one tenant. One tenant's schedules then never trigger, block, or expose another tenant's work. Unscoped leases would let a replica mix tenants inside one occurrence.

Checka reviewer answers

Reviewed forOccurrence and lease rows carry tenant scope in products that isolate tenants.

Marked guidance: a reviewer answers this question.

Dispatch Application Behavior#

ruleruleguidance
litespec/ext/jobs#dispatch-application-behavior
Statement

A scheduled job MUST dispatch an Application Command or a declared Application port.

Applies toartifact: job

Why

The job record names the use cases it runs, and the use case boundary lives in Application. Each job therefore dispatches an Application Command or a declared Application port. The schedule stays a trigger while the behavior stays owned.

Checka reviewer answers

Reviewed forEach job handler calls an Application Command or a declared Application port.

Marked guidance: a reviewer answers this question.

Exclude Direct Storage and HTTP Execution#

ruleruleguidance
litespec/ext/jobs#exclude-direct-storage-and-http-execution
Statement

A scheduled job MUST NOT mutate a Marten session or DbContext directly, bypass Domain behavior, or call WebApi endpoints.

Applies toartifact: job

Why

A job that mutates a Marten session or DbContext directly, bypasses Domain behavior, or calls WebApi endpoints skips the command boundary. That boundary carries authorization, transaction, and validation the job would then lack. Jobs enter through Commands like every other caller.

Checka reviewer answers

Reviewed forJob sources mutate no Marten session or DbContext and call no WebApi endpoint.

Marked guidance: a reviewer answers this question.

Bound Job Retries#

ruleruleguidance
litespec/ext/jobs#bound-job-retries
Statement

A scheduled job MUST use bounded attempts, cancellation-aware backoff, and a poison state for permanent failure.

Applies toartifact: job

Why

Bounded attempts with cancellation-aware backoff cap what one failed occurrence consumes. A poison state parks the permanently failing occurrence instead of retrying it forever. Without the bound, one poison occurrence eats the Worker's whole capacity.

Checka reviewer answers

Reviewed forEach schedule declares bounded attempts with cancellation-aware backoff and a poison state.

Marked guidance: a reviewer answers this question.

Tolerate Duplicate Execution#

ruleruleguidance
litespec/ext/jobs#tolerate-duplicate-execution
Statement

A scheduled job Command and side effect MUST tolerate duplicate execution after lease or process failure.

Applies toartifact: job

Why

A lease or process failure can replay a job whose completion stayed uncertain. The job Command and its side effects tolerate that duplicate execution. Work that assumes exactly-once breaks on the first uncertain failure.

Checka reviewer answers

Reviewed forJob Commands and side effects show one accepted effect after a replayed occurrence.

Marked guidance: a reviewer answers this question.

Treat Unavailable Schedule Stores as Outages#

ruleruleguidance
litespec/ext/jobs#treat-unavailable-schedule-stores-as-outages
Statement

A Worker MUST treat an unavailable scheduler store as the store outage litespec/ext/outbox#distinguish-dependency-outage defines, with backed-off, rate-limited logging.

Applies toartifact: job

Why

An unavailable scheduler store is an outage of the dependency, not a failed occurrence. The Worker treats it as the store outage litespec/ext/outbox#distinguish-dependency-outage defines, with backed-off, rate-limited logging. Retry and poison outcomes belong to claimed occurrences, not to a store nobody can reach.

Checka reviewer answers

Reviewed forWorker logs during a store outage show backed-off, rate-limited entries and no poisoned occurrences.

Marked guidance: a reviewer answers this question.

Persist Occurrence Recovery Points#

ruleruleguidance
litespec/ext/jobs#persist-occurrence-recovery-points
Statement

A scheduled-job store MUST persist each occurrence's last scheduled, started, and completed time.

Applies toartifact: job

Why

Last scheduled, started, and completed times persist per occurrence. After a restart, those points separate missed work from completed work. Memory alone cannot make that split, because the restart wipes it.

Checka reviewer answers

Reviewed forEach occurrence row carries last scheduled, started, and completed times.

Marked guidance: a reviewer answers this question.

Define Misfire Recovery#

ruleruleguidance
litespec/ext/jobs#define-misfire-recovery
Statement

A scheduled-job specification MUST define skipped, once-replayed, or interval-replayed behavior for missed occurrences.

Applies toartifact: job

Why

Downtime and delay leave occurrences past their time. The specification states whether each missed occurrence is skipped, replayed once, or replayed per interval. The choice controls the business effect: a skipped reminder stays silent while a replayed one arrives late.

Checka reviewer answers

Reviewed forEach schedule states skipped, once-replayed, or interval-replayed behavior for missed occurrences.

Marked guidance: a reviewer answers this question.

Provide Operational Recovery Procedures#

ruleruleguidance
litespec/ext/jobs#provide-operational-recovery-procedures
Statement

Each schedule MUST have Runbook records for inspection, manual replay, and disable.

Applies toartifact: job

Why

Stalled, failed, or unsafe schedules need controlled operator actions: inspection, manual replay, and disable. Runbook records carry those procedures. An operator without them improvises against the store during the incident.

Each procedure names the interface that performs it. A procedure that says "disable the schedule" without naming a command, an endpoint, or a configuration value remains undiscovered until the incident it was written for. Avoid an interface that writes to the schedule store directly, because it bypasses the occurrence identity litespec/ext/jobs#derive-stable-occurrence-identity depends on.

Example

A disable procedure names the configuration value or the administrative endpoint that stops the schedule. It states what happens to an occurrence already claimed.

Checka reviewer answers

Reviewed forRunbook records cover inspection, manual replay, and disable for each schedule.

Marked guidance: a reviewer answers this question.

Derive Stable Occurrence Identity#

ruleruleguidance
litespec/ext/jobs#derive-stable-occurrence-identity
Statement

A scheduled-job store MUST derive each occurrence identity from its schedule ID and scheduled instant.

Applies toartifact: job

Why

Schedule ID plus scheduled instant gives each planned time one stable identity across retries and replicas. Retries of one occurrence then share their idempotency scope, and two replicas recognize the same occurrence.

Checka reviewer answers

Reviewed forOccurrence identities in the store combine schedule ID with scheduled instant.

Marked guidance: a reviewer answers this question.

Remove Occurrences Past Retention#

ruleruleguidance
litespec/ext/jobs#remove-occurrences-past-retention
Statement

A scheduled-job store MUST remove occurrences older than the schedule's retention setting record.

Applies toartifact: job

Why

Occurrences older than the retention setting record leave the store so the key space stays bounded.

The key space grows with the schedule's frequency and its retention. A per-second schedule kept for a year holds more than 30 million rows. Quartz states the same practice for a persistent job store.

Example

A daily schedule that retains occurrences for 90 days holds 90 rows. A minute schedule with the same retention holds more than 100,000 rows. That is a different storage decision.

Checka reviewer answers

Reviewed forThe store holds no occurrence older than its schedule's retention setting record.

Marked guidance: a reviewer answers this question.

Persist Occurrence State#

ruleruleguidance
litespec/ext/jobs#persist-occurrence-state
Statement

A scheduled-job store MUST persist scheduled, processing, completed, and dead-letter state with safe execution details.

Applies toartifact: job

Why

Scheduled, processing, completed, and dead-letter states persist with attempt, lease, start, completion, and safe error data. The persisted state tells operators and recovery code where each occurrence stands. A store without states cannot separate work in flight from work never started.

Checka reviewer answers

Reviewed forEach occurrence row carries scheduled, processing, completed, or dead-letter state with safe execution details.

Marked guidance: a reviewer answers this question.

Scope Command Idempotency by Occurrence#

ruleruleguidance
litespec/ext/jobs#scope-command-idempotency-by-occurrence
Statement

A scheduled job MUST use its occurrence identity as the command idempotency scope.

Applies toartifact: job

Why

The occurrence identity scopes the command idempotency key. Every retry of one occurrence then replays the same scheduled business action. A key scoped any wider would merge separate occurrences into one.

Checka reviewer answers

Reviewed forRetries of one occurrence share the idempotency scope of its occurrence identity.

Marked guidance: a reviewer answers this question.

Restrict Completion by Fencing Value#

ruleruleguidance
litespec/ext/jobs#restrict-completion-by-fencing-value
Statement

A Worker MUST mark completion only while its fencing value remains current.

Applies toartifact: job

Why

The Worker marks completion only while its fencing value stays current. A stale value means another replica owns the occurrence now. Completing past that point would write one replica's result over another's ownership.

Checka reviewer answers

Reviewed forCompletion writes fail on a stale fencing value in the store.

Marked guidance: a reviewer answers this question.

Audit Manual Replay#

ruleruleguidance
litespec/ext/jobs#audit-manual-replay
Statement

A manual replay MUST create a new attempt for the same occurrence identity: an audit record when audit is selected, a standalone record otherwise.

Applies toartifact: job

Why

A manual replay creates a new attempt under the same occurrence identity: an audit record where audit is selected, a standalone record otherwise. The replay keeps the original scheduled instant and its history. A replay recorded as a new occurrence would rewrite when the work was due.

Checka reviewer answers

Reviewed forEach manual replay adds an audited attempt under its original occurrence identity.

Marked guidance: a reviewer answers this question.

Prefer UTC Schedules#

ruleruleguidance
litespec/ext/jobs#prefer-utc-schedules
Statement

A scheduled job MUST use a UTC schedule unless its specification names a business-local time requirement and its IANA time zone.

Applies toartifact: job

Why

A local schedule runs twice on one date each year and skips an hour on another. A daylight-saving transition does that to a wall clock. That behavior is correct for a job whose meaning is local, such as a nightly statement at 9 AM for a reader. It is a defect for every other job. Naming the time zone in the specification states the transition behavior for a later reader.

Checka reviewer answers

Reviewed forEach schedule states UTC or names a business-local time requirement with its IANA time zone.

Marked guidance: a reviewer answers this question.

Define Business-Local Clock Behavior#

ruleruleguidance
litespec/ext/jobs#define-business-local-clock-behavior
Statement

A business-local schedule MUST record an IANA time-zone identifier and define skipped and repeated local-time behavior.

Applies toartifact: job

Why

Daylight-saving transitions omit one local hour and repeat another. A business-local schedule records its IANA time-zone identifier and defines what happens to occurrences in the skipped and repeated hours. Without that definition, each transition silently drops or doubles the local work.

Checka reviewer answers

Reviewed forEach business-local schedule records its IANA zone with skipped and repeated local-time behavior.

Marked guidance: a reviewer answers this question.

Test Time Transitions#

ruleruleguidance
litespec/ext/jobs#test-time-transitions
Statement

A scheduled-job test MUST use an injected clock and representative time-zone transitions.

Applies toartifact: job

Why

An injected clock lets tests move time to any transition on demand. Representative time-zone transitions then run deterministically instead of once a year in production. Tests on the real clock can only wait for the transition to arrive.

Checka reviewer answers

Reviewed forJob tests inject the clock and cover representative time-zone transitions.

Marked guidance: a reviewer answers this question.

Exclude Machine-Local Time#

ruleruleguidance
litespec/ext/jobs#exclude-machine-local-time
Statement

Scheduled-job production code MUST NOT depend on machine-local time.

Applies toartifact: job

Why

Machine-local time follows each host's configuration. Production code that reads it schedules differently on a developer machine, in staging, and in production. Schedules read their time from the injected clock and the declared zone instead.

Checka reviewer answers

Reviewed forProduction job sources read no machine-local clock.

Marked guidance: a reviewer answers this question.

State the Claim Mode#

ruleruleguidance
litespec/ext/jobs#state-the-claim-mode
Statement

A work queue MUST state its claim mode as disabled, optional, or required.

Applies toartifact: record:use-case

Why

Two people clearing the same item double-pay, double-approve, or double-refuse. The claim mode says whether items need no claim, accept an optional hold, or require one before action, so the queue never leaves two hands on one item.

Example

The payout queue requires a claim before release, while the review queue leaves claiming optional.

Checka reviewer answers

Reviewed forThe queue use cases state disabled, optional, or required on their claim field.

Marked guidance: a reviewer answers this question.

Order Queue Items by Score#

ruleruleguidance
litespec/ext/jobs#order-queue-items-by-score
Statement

A work queue MUST order its items by a score that cites a calculation rule.

Applies toartifact: record:use-case

Why

A queue without an order is cleared by habit: newest first, loudest first, easiest first. A score citing a calculation rule pins the order to declared inputs, so the riskiest or most valuable item surfaces first by rule instead of by accident.

Example

The dispute queue orders by rule/dispute.urgency-score, which weighs amount, age, and evidence gaps.

Checka reviewer answers

Reviewed forThe queue listings sort by a score citing the calculation rule behind it.

Marked guidance: a reviewer answers this question.

Show the Score Beside Each Item#

ruleruleguidance
litespec/ext/jobs#show-the-score-beside-each-item
Statement

A queue listing MUST show each item score with the inputs that set it.

Applies toartifact: record:use-case

Why

A hidden score is an oracle: people work around an order they cannot see or trust. Showing the score with its inputs lets each person check the order and challenge the inputs instead of the queue.

Example

Each dispute row shows urgency 82: EUR 1,900, 6 days old, evidence missing.

Checka reviewer answers

Reviewed forThe queue rows display each score with the inputs that set it.

Marked guidance: a reviewer answers this question.

Release Claims Past Their Lease#

ruleruleguidance
litespec/ext/jobs#release-claims-past-their-lease
Statement

A claimed item MUST return to the queue when its claim lease expires.

Applies toartifact: record:use-case

Why

Claims are held by people who go home: a claim with no expiry parks the item forever. The lease returns stale claims to the queue, so an abandoned item waits for the next person instead of for nobody.

Example

A payout claimed Friday and untouched returns to the queue when its one-hour lease expires.

Checka reviewer answers

Reviewed forThe claim tests show untouched items returning after their lease expires.

Marked guidance: a reviewer answers this question.

Show a Preview Before Bulk Money Actions#

ruleruleguidance
litespec/ext/jobs#show-a-preview-before-bulk-money-actions
Statement

A bulk money command MUST render a preview of every affected item before acting.

Applies toartifact: record:use-case

Why

Bulk money moves on many items at once, and one wrong filter multiplies one mistake into hundreds. The preview lists every affected item with its amount, so the person confirms the set they meant instead of the set they typed.

Example

The bulk release previews forty-two payouts totaling EUR 18,400 before asking for confirmation.

Checka reviewer answers

Reviewed forThe bulk money commands render every affected item with its amount before acting.

Marked guidance: a reviewer answers this question.

Confirm Bulk Money Actions Out of Band#

ruleruleguidance
litespec/ext/jobs#confirm-bulk-money-actions-out-of-band
Statement

A bulk money command MUST require its confirmBy code from outside the session.

Applies toartifact: record:use-case

Why

A preview confirms the set, but the session itself may be hijacked, shared, or left open. An out-of-band code arrives on a channel the session does not control, so the confirmation proves the person, not just the browser.

Example

The bulk release asks for the six-digit code sent to the finance phone before moving any money.

Checka reviewer answers

Reviewed forThe bulk money commands ask for the out-of-band code before moving money.

Marked guidance: a reviewer answers this question.

Declare Ingest Steps in Order#

ruleruleguidance
litespec/ext/jobs#declare-ingest-steps-in-order
Statement

An ingest job MUST list its steps in run order with each step marked drop or enrich.

Applies toartifact: job

Why

Order is the pipeline's meaning: enriching before dropping wastes work, and dropping before enriching loses context. Listing the steps in run order with each kind marked lets a reader replay the pipeline in their head.

Example

The catalog ingest lists drop-duplicates, then enrich-prices, then drop-below-threshold.

Checka reviewer answers

Reviewed forThe ingest job records list steps in run order with each marked drop or enrich.

Marked guidance: a reviewer answers this question.

Define the Closed Drop Reason Set#

ruleruleguidance
litespec/ext/jobs#define-the-closed-drop-reason-set
Statement

An ingest job MUST name the closed set of reasons a step drops a record.

Applies toartifact: job

Why

A dropped record with no reason is indistinguishable from a lost one. A closed set forces each drop to say why in words the team chose, so operators count drops by reason instead of guessing.

Example

The catalog ingest names duplicate, below-threshold, and poison as its only drop reasons.

Checka reviewer answers

Reviewed forThe ingest job records name the closed drop reasons with no drop outside them.

Marked guidance: a reviewer answers this question.

Record One Drop Reason per Dropped Record#

ruleruleguidance
litespec/ext/jobs#record-one-drop-reason-per-dropped-record
Statement

Each record a step drops MUST carry one reason from the closed set.

Applies toartifact: job

Why

The set alone promises nothing until each drop cites it. One reason on each dropped record turns the set into evidence: every drop answers why, in the team's own words.

Example

A feed row dropped for matching an existing SKU carries duplicate.

Checka reviewer answers

Reviewed forThe dropped records each carry one reason from the job's closed set.

Marked guidance: a reviewer answers this question.

Read Enrichment Data from Declared Sources#

ruleruleguidance
litespec/ext/jobs#read-enrichment-data-from-declared-sources
Statement

An enrich step MUST read only the sources its job declares.

Applies toartifact: job

Why

Enrichment pulls outside data into product records: prices, categories, ratings. An undeclared source smuggles values no record explains, so each enrich step reads only the sources its job names.

Example

The enrich-prices step reads the vendor price list its job declares, and nothing else.

Checka reviewer answers

Reviewed forThe enrich steps read only the sources named on their job records.

Marked guidance: a reviewer answers this question.

Bound the Ingest Buffer#

ruleruleguidance
litespec/ext/jobs#bound-the-ingest-buffer
Statement

An ingest job MUST state its buffer policy with capacity and overflow behavior.

Applies toartifact: job

Why

Feeds burst: a vendor resends a week of rows, a file lands twice. The buffer holds the surge, and the policy states how much it holds and what happens past that: block the feed, drop the oldest, or page an operator. Without the policy, the first surge picks the behavior.

Example

The catalog ingest buffers ten thousand rows and blocks the feed past that until the buffer drains.

Checka reviewer answers

Reviewed forThe ingest job records state buffer capacity with the overflow behavior past it.

Marked guidance: a reviewer answers this question.

Expose Drop Counts per Reason#

ruleruleguidance
litespec/ext/jobs#expose-drop-counts-per-reason
Statement

An ingest job MUST expose a count of dropped records per reason.

Applies toartifact: job

Why

Drop counts are the pipeline's pulse: a spike in poison means a broken feed, a spike in duplicate means a double send. Per-reason counts turn each drop into a signal the team watches.

Example

The catalog dashboard shows twelve poison drops in the last hour beside zero duplicate drops.

Checka reviewer answers

Reviewed forThe pipeline dashboards show dropped-record counts split by reason.

Marked guidance: a reviewer answers this question.

State the Freshness Consequence#

ruleruleguidance
litespec/ext/jobs#state-the-freshness-consequence
Statement

Each query reading ingested data MUST state the freshness its readers observe.

Applies toartifact: job

Why

Ingested data lags: the buffer, the batch window, and the enrich calls all take time. The query states what its readers observe, a bounded object with its window or an advisory object with its authority, so a stale price reads as declared behavior instead of a defect.

Example

The catalog search query states {"mode": "bounded", "within": "PT15M"}, matching the ingest job's quarter-hour window.

Checka reviewer answers

Reviewed forThe queries over ingested data state the freshness their readers observe.

Marked guidance: a reviewer answers this question.

Declare the Engine Carrier#

ruleruleguidance
litespec/ext/jobs#declare-the-engine-carrier
Statement

A saga carried by a workflow engine MUST set carriedBy: workflow.

Applies toartifact: aggregate

Why

A reader meeting a saga asks first what moves it: reactions, jobs, or an engine. The carrier marker answers that question on the aggregate itself, so nobody hunts the engine console for a saga reactions and jobs drive.

Example

The hold aggregate sets carriedBy: workflow while the loan aggregate stays driven by reactions and jobs.

Checka reviewer answers

Reviewed forThe engine-carried aggregates set carriedBy workflow on their aggregate records.

Marked guidance: a reviewer answers this question.

Map Each Engine Step to One Use Case#

ruleruleguidance
litespec/ext/jobs#map-each-engine-step-to-one-use-case
Statement

Each engine step MUST map to exactly one use case.

Applies toartifact: aggregate

Why

An engine step with no use case is behavior no record describes, and a step mapped to two use cases is two behaviors wearing one name. One step and one use case keep the engine definition and the records in lockstep: the map reads both ways.

Example

The engine step cancel-hold maps to use-case/lending.cancel-hold and to nothing else.

Checka reviewer answers

Reviewed forThe engine definition lists one use case beside each step with no step sharing or missing.

Marked guidance: a reviewer answers this question.

State Each Step Compensation#

ruleruleguidance
litespec/ext/jobs#state-each-step-compensation
Statement

Each mapped engine step MUST name its compensation as a use-case object or a none object with the reason.

Applies toartifact: aggregate

Why

Engines fail mid-saga: a step times out, a worker crashes, a saga aborts. The compensation is the step's undo as {"mode": "use-case", "useCase": "use-case/..."}, and a step with no undo says so as {"mode": "none", "reason": "..."}, so recovery follows the record instead of improvising.

Example

The step cancel-hold names {"mode": "use-case", "useCase": "use-case/lending.place-hold"} as its compensation, while borrow-copy names {"mode": "none", "reason": "the copy has left the shelf"}.

Checka reviewer answers

Reviewed forWhich mapped steps lack a compensation object naming its use case or its reason? Look at the map beside the process.

Marked guidance: a reviewer answers this question.

Reject Unmapped Engine Steps#

rulerulecheckable
litespec/ext/jobs#reject-unmapped-engine-steps
Statement

An engine step with no mapped use case MUST fail behavior-unrecorded.

Applies toartifact: aggregate

Why

The map is the whole contract of this extension: every step answers to a use case. A step outside the map is unrecorded behavior by definition, so it fails the same check as code with no record.

Example

A new engine step added in the console with no use case behind it fails behavior-unrecorded at the next check.

Checkchecked by a tool

Checked bylitespec model check reports behavior-unrecorded for an engine step that maps to no use case.

Marked checkable: the tool named here checks it.

Keep Engine Retry Configuration in Implementation#

ruleruleguidance
litespec/ext/jobs#keep-engine-retry-configuration-in-implementation
Statement

Engine retry and timeout configuration MUST live in code outside the records.

Applies toartifact: aggregate

Why

Retry counts, backoff curves, and step timeouts tune operations, not behavior: they change with load and incidents, not with product decisions. Records state what each step does and undoes, while code holds the knobs operators turn.

Example

The engine's five-attempt retry with exponential backoff lives in the worker configuration, while the records name only the step and its compensation.

Checka reviewer answers

Reviewed forRetry counts, backoff curves, and step timeouts live in worker code with none in the records.

Marked guidance: a reviewer answers this question.

Record Reporting Need#

ruleruleguidance
litespec/ext/jobs#record-reporting-need
Statement

A reporting proposal MUST record the report, representative data volume, plan or limitation, latency target, maximum result, and operator.

Applies toartifact: record:use-case

Why

Reporting infrastructure costs a Worker, storage, and operating attention, so the team writes down the report, a representative data volume, the plan or limitation, the latency target, the maximum result, and the operator before building any of it. The use case page carries that record, and it shows why a normal request projection cannot meet the boundary.

Checka reviewer answers

Reviewed forThe use case page holds the report, volume, plan, target, maximum, and operator.

Marked guidance: a reviewer answers this question.

Avoid Speculative Reporting Systems#

ruleruleguidance
litespec/ext/jobs#avoid-speculative-reporting-systems
Statement

A project MUST NOT create reporting infrastructure for possible future analytics.

Applies toartifact: record:use-case

Why

A reporting system built for analytics the product may never need charges its full operating cost from day one while serving no accepted query. Each report or export earns its infrastructure with a current cost boundary, not a guess about future analysis.

Checka reviewer answers

Reviewed forEach reporting system traces to an accepted current query or export.

Marked guidance: a reviewer answers this question.

Parameterize Report SQL#

ruleruleguidance
litespec/ext/jobs#parameterize-report-sql
Statement

A reporting query MUST parameterize every actor, tenant, filter, range, sort, and pagination value.

Applies toartifact: record:use-case

Why

A report concatenates actor, tenant, filters, ranges, sorting, and pagination into SQL that reads across the database. Binding every one of those values as a parameter keeps tenant and actor isolation intact and closes the injection hole string concatenation opens.

Checka reviewer answers

Reviewed forEach report query binds actor, tenant, filter, range, sort, and pagination values as parameters in code.

Marked guidance: a reviewer answers this question.

Keep Raw SQL in Infrastructure#

ruleruleguidance
litespec/ext/jobs#keep-raw-sql-in-infrastructure
Statement

Infrastructure MUST own raw reporting SQL and connection handling.

Applies toartifact: record:use-case

Why

Raw SQL names tables, dialects, and connection handling that belong to one provider. Keeping it in Infrastructure lets Application and Domain state what the report means without depending on how one database phrases it.

Checka reviewer answers

Reviewed forRaw report SQL and connection handling sit in Infrastructure code.

Marked guidance: a reviewer answers this question.

Review Report Plans#

ruleadviceguidance
litespec/ext/jobs#review-report-plans
Statement

Each report SHOULD have its query plans and indexes reviewed against representative data.

DeviationA report over a small fixed data set justifies skipping the review.

Applies toartifact: record:use-case

Why

A query that flies over a hundred local rows can scan millions in production. The team reviews query plans and indexes against representative data, so the true cost shows before the report ships rather than after it slows the database.

Checka reviewer answers

Reviewed forThe review record shows query plans and indexes checked against representative data.

Marked guidance: a reviewer answers this question.

Apply Item-Read Authorization#

ruleruleguidance
litespec/ext/jobs#apply-item-read-authorization
Statement

A report or export MUST enforce actor, role, ownership, tenant, and sensitive-field rules that apply to item reads.

Applies toartifact: record:use-case

Why

A report that reads ten thousand rows at once crosses the same actor, role, ownership, tenant, and sensitive-field boundaries as ten thousand single reads. Enforcing the item-read rules on the bulk path stops the report from exposing rows its reader may not open one by one.

Checka reviewer answers

Reviewed forEach report and export enforces the item-read rules in the authorization tests.

Marked guidance: a reviewer answers this question.

Run Budget-Exceeding Exports in Worker#

ruleruleguidance
litespec/ext/jobs#run-budget-exceeding-exports-in-worker
Statement

An export exceeding its request budget MUST run through Worker, with the budget a setting record the export's use case names.

Applies toartifact: record:use-case

Why

An export that outlasts its request budget holds an HTTP connection open while the reader waits and the server counts threads. Running it in a Worker returns the request immediately, and the budget in its setting record tells the team exactly when an export outgrows the request path.

Checka reviewer answers

Reviewed forEach export past its request budget runs through a Worker, with the budget in its setting record.

Marked guidance: a reviewer answers this question.

Protect Export Output#

ruleruleguidance
litespec/ext/jobs#protect-export-output
Statement

An export worker MUST store output in approved object storage with a named contact, an expiry, and a short-lived authorized download.

Applies toartifact: record:use-case

Why

An export file concentrates sensitive rows into one downloadable object that outlives the request that built it. Approved object storage with a named contact, an expiry, and a short-lived authorized download gives that object controlled access and a defined end instead of a permanent public link.

Checka reviewer answers

Reviewed forExport output sits in approved object storage with a named contact, an expiry, and a short-lived authorized download.

Marked guidance: a reviewer answers this question.

Scope Export Output to the Tenant#

ruleruleguidance
litespec/ext/jobs#scope-export-output-to-the-tenant
Statement

An export MUST store its output under a tenant-scoped object-storage path.

Applies toartifact: record:use-case

Why

Export addresses are predictable, and a shared folder lets one tenant guess another tenant's file name. A tenant-scoped storage path puts each tenant's exports under its own prefix, so a guessed address stays inside the guesser's own data.

Checka reviewer answers

Reviewed forEach export stores its output under a tenant-scoped path in object storage.

Marked guidance: a reviewer answers this question.

Define Report Limits#

ruleruleguidance
litespec/ext/jobs#define-report-limits
Statement

A report specification MUST state each bound as a setting record: maximum date range, rows, execution time, concurrent jobs, output size, and storage lifetime.

Applies toartifact: record:use-case

Why

Date range, rows, execution time, concurrent jobs, output size, and storage lifetime each cap one dimension of report cost. Setting records hold all six, so a runaway report hits a documented bound instead of an exhausted database, a full disk, or a surprise invoice.

Checka reviewer answers

Reviewed forEach report bound lives as a setting record beside the report.

Marked guidance: a reviewer answers this question.

Declare Export Paging#

ruleruleguidance
litespec/ext/jobs#declare-export-paging
Statement

An export query MUST state paging with mode of pages or none with its reason.

Applies toartifact: record:use-case

Why

An export reads many rows at once, so it states how the read is bounded: {"mode": "pages", ...} bounds each run, and {"mode": "none", "reason": "..."} states why the export reads all rows at once. Without the declaration, a reviewer cannot tell a bounded export from one that grows with the table.

Checka reviewer answers

Reviewed forWhich export queries lack paging with mode pages or mode none with its reason? Look at the export query beside the report.

Marked guidance: a reviewer answers this question.

Audit Export Reads#

ruleruleguidance
litespec/ext/jobs#audit-export-reads
Statement

An export query MUST state its audit reason and target.

Applies toartifact: record:use-case

Why

An export file concentrates sensitive rows into one downloadable object, so the use case states why the bulk read is audited and where the audit lands. Without the stated audit, the concentrated read leaves no trace a reviewer can follow.

Checka reviewer answers

Reviewed forWhich export queries lack a stated audit reason and target? Look at the export query beside the report.

Marked guidance: a reviewer answers this question.

Support Report Cancellation#

ruleruleguidance
litespec/ext/jobs#support-report-cancellation
Statement

A report implementation MUST support cancellation and remove expired outputs.

Applies toartifact: record:use-case

Why

A reader who closes the tab leaves the report running, and an export nobody downloads still occupies storage. Cancellation stops the abandoned work, and expiry removes the abandoned file, so neither consumes resources indefinitely.

Checka reviewer answers

Reviewed forReport work cancels on request and expired output clears in the cancellation tests.

Marked guidance: a reviewer answers this question.

Escape Formula-Leading Export Values#

ruleruleguidance
litespec/ext/jobs#escape-formula-leading-export-values
Statement

A CSV or spreadsheet export MUST escape formula-leading values unless the team intentionally fills a field with a formula.

Applies toartifact: record:use-case

Why

A value starting with =, +, -, or @ becomes a live formula when the export opens in a spreadsheet, running in the opener's application with the opener's data. Escaping those values keeps data as data, except in a field the team intentionally fills with a formula.

Checka reviewer answers

Reviewed forExported values starting a formula are escaped, except in formula fields the team fills intentionally.

Marked guidance: a reviewer answers this question.

Define Export Encoding and Columns#

ruleruleguidance
litespec/ext/jobs#define-export-encoding-and-columns
Statement

An export format MUST use explicit character encoding and a stable column contract.

Applies toartifact: record:use-case

Why

An export without a stated encoding arrives garbled on the first machine that guesses differently, and columns that shift per release break every parser built on them. Explicit encoding with a stable column contract lets consumers parse the file without guessing either.

Checka reviewer answers

Reviewed forEach export states its character encoding and stable columns in its format record.

Marked guidance: a reviewer answers this question.

Conventions#

Place Handlers with Owned Behavior#

conventiondefaultguidance
litespec/ext/jobs#place-handlers-with-owned-behavior
Default

Create one job handler under the module that owns its behavior.

ReplacementA project keeps job handlers in one shared jobs folder.

Applies toartifact: job

Why

One job handler lives under the module that owns its behavior. The module owns both the business action and what its schedule means. A handlers folder far from the behavior separates each schedule from its reason.

Checka reviewer answers

Reviewed forEach job handler sits under the module that owns its behavior, or the project keeps its handlers in one shared jobs folder.

Marked guidance: a reviewer answers this question.

Use Execution Scopes#

conventiondefaultguidance
litespec/ext/jobs#use-execution-scopes
Default

Resolve scoped dependencies inside an execution scope and dispose the scope after the Command completes.

ReplacementA project names another scope boundary for occurrence dependencies in its documentation.

Applies toartifact: job

Why

Each occurrence resolves its scoped dependencies inside one execution scope and disposes it after the Command completes. The scope isolates one occurrence's dependencies from the next. A shared scope would leak one occurrence's state into another.

Checka reviewer answers

Reviewed forEach occurrence resolves scoped dependencies in one disposed execution scope, or the project names another scope boundary in its documentation.

Marked guidance: a reviewer answers this question.

Separate Schedule Definitions#

conventiondefaultguidance
litespec/ext/jobs#separate-schedule-definitions
Default

Keep schedule definitions separate from Command behavior.

ReplacementA project keeps schedule definitions beside their Command behavior.

Applies toartifact: job

Why

Timing policy changes when operations needs a new rhythm, and Command behavior changes when the business needs a new outcome. Keeping schedule definitions separate from Command behavior lets each change alone. A schedule buried in the handler makes every timing change a code change.

Checka reviewer answers

Reviewed forSchedule definitions sit apart from Command behavior, or the project keeps them beside it.

Marked guidance: a reviewer answers this question.

Order Unscored Queues by Age#

conventiondefaultguidance
litespec/ext/jobs#order-unscored-queues-by-age
Default

A queue without a score lists its oldest item first.

ReplacementA project names its fallback queue order in its queue documentation.

Applies toartifact: record:use-case

Why

Age is the honest fallback: the item waiting longest goes first. Any other fallback works when the queue states it.

Checka reviewer answers

Reviewed forQueues without a score list oldest first, or the queue documentation names another fallback order.

Marked guidance: a reviewer answers this question.

Discard Poison Records After Three Attempts#

conventiondefaultguidance
litespec/ext/jobs#discard-poison-records-after-three-attempts
Default

A record that fails parsing three times is dropped with the poison reason.

ReplacementA project names its poison threshold in its pipeline documentation.

Applies toartifact: job

Why

One bad row must not stall the feed forever, and one strict parse must not drop rows a retry would save. Three attempts split the difference, and the poison reason keeps the casualty visible.

Checka reviewer answers

Reviewed forUnparsable rows drop as poison after three attempts, or the pipeline documentation names another threshold.

Marked guidance: a reviewer answers this question.

Name Engine Steps After Use Cases#

conventiondefaultguidance
litespec/ext/jobs#name-engine-steps-after-use-cases
Default

Each engine step carries the id of its mapped use case.

ReplacementA project records its engine step naming convention in its workflow documentation.

Applies toartifact: aggregate

Why

A step named after its use case needs no map to read: the name is the map. Any other naming works when the map stays beside the process.

Checka reviewer answers

Reviewed forEngine steps carry their use-case ids, or the workflow documentation records the naming convention.

Marked guidance: a reviewer answers this question.

Keep Report Definitions in Application#

conventiondefaultguidance
litespec/ext/jobs#keep-report-definitions-in-application
Default

Keep report definitions in Application under their module.

ReplacementA consumer records a report-definition convention naming where its report definitions live.

Applies toartifact: record:use-case

Why

What a report means and who may run it are business choices. Keeping the definition in Application under its module puts those choices beside the module's other rules instead of buried in query code.

Checka reviewer answers

Reviewed forReport definitions sit in their module, or the report-definition convention names where they live.

Marked guidance: a reviewer answers this question.

Keep Report Providers in Infrastructure#

conventiondefaultguidance
litespec/ext/jobs#keep-report-providers-in-infrastructure
Default

Keep SQL, row mappings, and storage providers in Infrastructure.

ReplacementA consumer records a report-provider convention naming where its SQL, mappings, and storage code live.

Applies toartifact: record:use-case

Why

SQL text, row mappings, and storage calls change per provider while the report's meaning stays fixed. Keeping them in Infrastructure swaps the provider without touching the report's business rules.

Checka reviewer answers

Reviewed forSQL and storage code sit in Infrastructure, or the report-provider convention names where they live.

Marked guidance: a reviewer answers this question.

Separate Worker Orchestration#

conventiondefaultguidance
litespec/ext/jobs#separate-worker-orchestration
Default

Keep Worker orchestration separate from report rules.

ReplacementA consumer records a report-orchestration convention naming where its scheduling and progress tracking live.

Applies toartifact: record:use-case

Why

Scheduling, chunking, and progress tracking run the report without defining it. Keeping Worker orchestration apart from the report's rules lets either change without dragging the other along.

Checka reviewer answers

Reviewed forWorker orchestration sits apart from report rules, or the report-orchestration convention names where it lives.

Marked guidance: a reviewer answers this question.

Dependencies#

Packages: This extension selects no report or object-storage package. Each provider the project uses needs a decision and a manifest pin.

Related extensions: outbox.

Guides: None.