Cron and workflows
Five-field crontabs that POST to your own endpoints on the minute — plus durable workflows, queues and a dead-letter replay.
A cron job is a timer that calls your app. You give it a schedule and a path; every minute the scheduler checks which jobs are due and POSTs the ones that are. No worker process, no extra service to keep alive.
The schedule
Expressions are a classic five-field crontab, evaluated in UTC:
┌───────────── minute 0–59
│ ┌───────────── hour 0–23
│ │ ┌───────────── day 1–31
│ │ │ ┌───────────── month 1–12
│ │ │ │ ┌───────────── weekday 0–6 (0 is Sunday)
* * * * *
Each field takes *, a number, a list (1,15), a range (9-17), or a step (*/5, 9-17/2). 0 9 * * 1 is 09:00 UTC every Monday; */15 * * * * is every fifteen minutes.
Unsure what an expression does? POST /v1/projects/<slug>/cron/preview with { "expression": "…" } returns the next five fire times — the console's planner calls the same route as you type.
What your endpoint receives
A job's endpoint is a path on your own app — /jobs/digest, not a URL. Absolute URLs are rejected, so a job can only ever call https://<slug>.camplax.app:
POST https://<slug>.camplax.app/jobs/digest
User-Agent: Camplax-Cron/1.0
Camplax-Cron-Job: crn_…
Camplax-Cron-Run: run_…
Content-Type: application/json
{ "jobId": "…", "runId": "…", "name": "Daily digest", "expression": "0 9 * * *", "firedAt": "…" }
A scheduled run gives your endpoint 30 seconds before it is marked failed; a 2xx is a success and anything else is a failure with the HTTP status recorded.
One thing to know: the request is not signed. The headers tell you which job fired, but anyone on the internet can POST to your path. If the endpoint does something sensitive, protect it yourself — a secret path, or a token in the path or a header your code checks.
Managing jobs
In the console's Cron page you can create, edit, pause and delete jobs. The same thing over the API, with your console session:
| Method | Path | What it does |
|---|---|---|
GET | /v1/projects/<slug>/cron/jobs | List jobs with last and next run times |
POST | /v1/projects/<slug>/cron/jobs | Create — { name, expression, endpoint, enabled? } |
PATCH | /v1/projects/<slug>/cron/jobs/:id | Edit, or pause with { "enabled": false } |
DELETE | /v1/projects/<slug>/cron/jobs/:id | Delete |
POST | /v1/projects/<slug>/cron/preview | Next five fire times for an expression |
GET | /v1/projects/<slug>/cron/runs | Run history — ?status=ok|fail|running, up to 200 rows |
Every run is recorded with its status, duration and error, and the runs list rolls up today's counts and median duration. A failed cron run is not retried — the schedule fires it again next time. For retries you want a workflow.
Workflows
A workflow is an ordered list of steps, each one an endpoint on your app with its own reliability settings:
- Attempts — up to 10 tries per step.
- Backoff — none, fixed 2s, exponential 2s→32s, or a longer exponential 5s→5m.
- Timeout — per step, 5s to 5m.
- Dead-letter — a step that exhausts its attempts can park on the dead-letter queue instead of vanishing.
Create one with POST /v1/projects/<slug>/cron/workflows, start a run with POST /cron/workflows/:id/run, and tune a step later with PATCH /cron/workflows/steps/:id. Workflow runs appear in the same runs list — and those can be retried with POST /cron/runs/:id/retry.
Queues
The Queues tab shows named queues with waiting, in-flight and per-minute throughput. Messages that exhaust their retries land on the dead-letter queue; once you have fixed the cause, POST /v1/projects/<slug>/cron/queues/dead-letter/replay moves every waiting dead-letter message back onto a live queue. GET /cron/queues and GET /cron/queues/:id/messages let you inspect what is parked.
Runs also show up in the project's log stream, next to deploy output and runtime lines.
Under the hood: a Durable Object ticks every minute, due jobs go through a Cloudflare Queue, and every run lands in the cron_runs table.