Schedules, Jobs & Executions
Chronos has three primitives. Understanding how they relate is the key to using the API effectively.
The mental model
Section titled “The mental model”Schedule ──spawns──▶ Job ──creates──▶ Execution(pattern) (work) (attempt)- Schedule: A recurring pattern that produces jobs on a cadence. Think of it as a standing instruction: “run this every hour” or “run this at 9am on weekdays.”
- Job: A unit of work to execute. Created by a schedule (recurring) or directly via the API (one-off).
- Execution: A single attempt to run a job. If a job fails and has retries remaining, a new execution is created for the next attempt.
One schedule produces many jobs. One job may have multiple executions (if retries are needed).
Schedules
Section titled “Schedules”A schedule defines when and how work should happen.
Fields that matter
Section titled “Fields that matter”| Field | Purpose |
|---|---|
cron or interval | When to fire (exactly one required) |
handler | Routing key. Matches your worker’s registered handler name |
delivery | How to deliver: pull (worker) or push (HTTP) |
payload | Data passed to every job this schedule creates |
timeout | Seconds before an execution is considered timed out (default: 30) |
max_retries | How many times to retry on failure (default: 3) |
runs | Stop after this many jobs (optional) |
stop_at | Stop after this time (optional) |
Schedule lifecycle
Section titled “Schedule lifecycle”active ──────▶ completed (runs limit reached or stop_at passed) │ └──────────▶ archived (manual, via POST /v1/schedules/:id/archive)- active: Producing jobs on schedule.
- completed: Finished naturally. Reached its
runslimit or passedstop_at. - archived: Stopped manually. All pending jobs for this schedule are cancelled.
Archiving is the only manual lifecycle action. Completion happens automatically.
Interval anchoring
Section titled “Interval anchoring”Interval schedules compute the next run from the scheduled time, not from wall-clock:
Schedule: every 1 hourFirst run scheduled for: 10:00:00Job spawned at (actual): 10:00:03 (3s late due to system load)Next run computed from: 10:00:00 (not 10:00:03)Next run scheduled for: 11:00:00This prevents drift. Jobs don’t slide later over time.
A job is a discrete unit of work. It’s either spawned by a schedule or created directly as a one-off.
Job lifecycle
Section titled “Job lifecycle”pending → admitting → queued → ready → running → completed │ │ │ ├──▶ failed (retries exhausted) │ │ │ └──▶ retrying → (back to pending) │ └──▶ cancelled (schedule archived)What each status means:
| Status | Meaning |
|---|---|
pending | Created, waiting to be processed by the internal queue |
admitting | Being picked up by the internal dispatcher |
queued | In the dispatch queue, waiting for its run_at time |
ready | Due for execution and available to be claimed by a pull worker. Push jobs skip this state. |
running | Actively being executed |
completed | Finished successfully |
failed | Failed and retries exhausted |
retrying | Failed but has retries remaining. Waiting for next attempt |
cancelled | Cancelled because its parent schedule was archived |
From your perspective as a developer, the statuses you’ll interact with are: pending (just created), running (your handler has it), completed, failed, and retrying.
Recurring vs one-off
Section titled “Recurring vs one-off”| Recurring job | One-off job | |
|---|---|---|
| Created by | A schedule (automatically) | You (via POST /v1/jobs) |
schedule_id | UUID of parent schedule | null |
| Timing | Determined by schedule’s cron/interval | scheduled_for, delay, or immediate |
handler | Inherited from schedule | Set on the job directly |
The attempt_count field
Section titled “The attempt_count field”Each time a job produces a new execution (first attempt or retry), attempt_count increments. You don’t need to track this yourself. ctx.attempt in your handler tells you which attempt this is.
Executions
Section titled “Executions”An execution is a single attempt to run a job. It’s the lowest-level primitive: the record of what actually happened.
Execution lifecycle
Section titled “Execution lifecycle”pending → running → completed → failed → timeout| Status | Meaning |
|---|---|
pending | Created, not yet started |
running | In progress. Your handler is executing or push request is in-flight |
completed | Handler returned successfully (or endpoint returned 2xx) |
failed | Handler threw an error (or endpoint returned 4xx/5xx) |
timeout | Execution exceeded the job’s timeout without reporting a result |
Execution fields
Section titled “Execution fields”| Field | Description |
|---|---|
trigger | Why this execution exists: system (first attempt), system_retry (automatic retry), manual (reserved) |
duration_ms | How long the execution took (null if not completed) |
result | Return value from your handler (if completed with data) |
error | Error message (if failed, truncated to 4KB) |
response_code | HTTP status code (push delivery only) |
Retry creates a new execution
Section titled “Retry creates a new execution”When a job fails and retries remain:
- The current execution is marked
failed - The job transitions to
retrying - After the backoff delay, a new execution is created
- The new execution has
trigger: "system_retry"
Each retry is a distinct execution with its own execution_id. Use job_id for side effects that must happen once per job, and use execution_id for per-attempt logs, metrics, and correlation.
How they connect
Section titled “How they connect”Schedule "Hourly sync" (cron: 0 * * * *) │ ├── Job (10:00) ── Execution #1 ✓ completed │ ├── Job (11:00) ── Execution #1 ✗ failed │ └─ Execution #2 ✓ completed (retry succeeded) │ └── Job (12:00) ── Execution #1 ✓ completed
One-off Job "Send welcome email" │ └── Execution #1 ✓ completedEach execution attempt counts toward your plan’s monthly execution limit.