Jobs
Overview
Section titled “Overview”Jobs add durable execution on top of Functions. A job is a function that survives crashes, retries on failure, checkpoints progress across steps, and can sleep for hours or days without holding a worker.
Use Jobs for order processing, nightly reports, webhook handlers that need retries, CRM sync orchestration, and any work that outlives a single HTTP request. Functions handle request/response; Jobs handle workflows.
Four trigger types ship at v0: cron (scheduled), webhook (external HTTP), event (internal pub-sub), and manual (API call). The TypeScript SDK (@reactor/jobs-sdk) provides ctx.step(), ctx.state, ctx.emit(), and ctx.sleep() for checkpointed execution.
Key features
Section titled “Key features”- Checkpointed steps —
ctx.step(name, fn)caches output; replays skip completed steps on retry. - Durable state — Per-run key-value store persists across steps and retries.
- Event pub-sub —
ctx.emit(topic, payload)triggers downstream jobs. - Durable sleep — Release the worker; wake up and resume from the same step.
- Retry with backoff — Exponential or linear backoff; failed runs land in a dead-letter queue.
- Concurrency control — Per-job semaphores prevent resource exhaustion.
- Built-in Data client —
ctx.dataqueries user tables with internal auth.
Quickstart
Section titled “Quickstart”Create a function with a job manifest, register the job, add a cron trigger, and trigger manually.
functions/process-order/code/index.ts:
import { JobContext } from '@reactor/jobs-sdk';
export default { async fetch(request: Request, _env: Env, ctx: JobContext): Promise<Response> { const payload = await request.json();
const order = await ctx.step('validate', async () => validateOrder(payload)); await ctx.step('charge', async () => chargeCustomer(order)); await ctx.emit('order.processed', { orderId: order.id });
return Response.json({ success: true, orderId: order.id }); },};reactor functions create process-order --runtime bunreactor functions deploy process-order --dir ./functions/process-orderreactor functions promote process-order
reactor jobs create process-order --function process-orderreactor jobs triggers create process-order --kind cron --schedule "0 * * * *"reactor jobs trigger process-order --payload '{"orderId":"ord_123"}'import { createClient } from '@reactor/client';
const reactor = createClient({ url: process.env.REACTOR_URL! });await reactor.auth.signInWithPassword({ email, password });reactor.auth.setOrg('acme');
await reactor.jobs.create({ name: 'process-order', functionName: 'process-order',});
await reactor.jobs.triggers.create('process-order', { kind: 'cron', schedule: '0 * * * *',});
const run = await reactor.jobs.trigger('process-order', { payload: { orderId: 'ord_123' },});console.log(run.id, run.status);# Create job (function must exist first)curl -s -X POST "$REACTOR_URL/jobs/v1/_admin/jobs" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"name":"process-order","function_name":"process-order"}'
# Manual triggercurl -s -X POST "$REACTOR_URL/jobs/v1/process-order/trigger" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"payload":{"orderId":"ord_123"}}'How-to guides
Section titled “How-to guides”Add a webhook trigger
Section titled “Add a webhook trigger”Webhook triggers accept unauthenticated HTTP POSTs to a stable token URL.
reactor jobs triggers create process-order --kind webhook# Returns ingress URL: /jobs/v1/webhooks/{token}# External caller (no JWT — token is auth)curl -s -X POST "$REACTOR_URL/jobs/v1/webhooks/whk_..." \ -H "Content-Type: application/json" \ -d '{"orderId":"ord_456","source":"stripe"}'Use durable sleep between steps
Section titled “Use durable sleep between steps”Sleep releases the worker and resumes execution after the duration.
await ctx.step('send-welcome', async () => sendWelcomeEmail(user));await ctx.sleep('cooling-off', '24h');await ctx.step('follow-up', async () => sendFollowUp(user));On wake, completed steps return cached output; execution continues after the sleep() call.
Retry failed runs and inspect the DLQ
Section titled “Retry failed runs and inspect the DLQ”reactor jobs runs list process-order --status failedreactor jobs dlq list process-orderreactor jobs dlq retry process-order dlq_01HZ...const failed = await reactor.jobs.runs.list('process-order', { status: 'failed' });const dlq = await reactor.jobs.dlq.list('process-order');await reactor.jobs.dlq.retry('process-order', dlq[0].id);curl -s "$REACTOR_URL/jobs/v1/_admin/jobs/process-order/runs?status=failed" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme"
curl -s -X POST "$REACTOR_URL/jobs/v1/_admin/jobs/process-order/dlq/dlq_01HZ.../retry" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme"Configuration
Section titled “Configuration”[jobs]worker_count = 4scheduler_interval_ms = 1000default_timeout_ms = 600000 # 10 minutesmax_timeout_ms = 3600000 # 1 hourwebhook_secret = "your-webhook-secret"
[jobs.functions]url = "http://localhost:8004"api_key = "internal-functions-api-key"
[jobs.data]url = "http://localhost:8002"api_key = "internal-data-api-key"Job manifest fields (inside function bundle):
{ "job": { "triggers": [ { "kind": "cron", "schedule": "0 */6 * * *" }, { "kind": "event", "topic": "order.created" }, { "kind": "webhook" } ], "retry": { "maxAttempts": 3, "backoff": "exponential", "initialDelayMs": 1000, "maxDelayMs": 60000 }, "maxConcurrency": 5, "timeoutMs": 600000 }}Limits and quotas
Section titled “Limits and quotas”| Limit | Default | Notes |
|---|---|---|
| Default job timeout | 10 minutes | Overridable per job |
| Max job timeout | 1 hour | Server cap |
| Default max attempts | 3 | Configurable per job |
| Worker count | 4 | Parallel run execution |
| Scheduler poll interval | 1 second | Cron, events, sleep wakeups |
| Max concurrency per job | 10 | Returns 429 when exceeded |
| Queue backend | Postgres (SKIP LOCKED) | Redis adapter in v0.2 |
Cron schedule examples:
| Schedule | Meaning |
|---|---|
0 * * * * | Every hour |
0 0 * * * | Daily at midnight UTC |
0 9 * * 1-5 | Weekdays at 09:00 UTC |
*/15 * * * * | Every 15 minutes |
API and SDK links
Section titled “API and SDK links”- HTTP base path:
/jobs/v1/ - Admin base path:
/jobs/v1/_admin/ - OpenAPI reference: Jobs API
- JavaScript SDK:
@reactor/jobs-sdk,reactor.jobs - CLI:
reactor jobs - Guide: Scheduled jobs
| Method | Path | Description |
|---|---|---|
POST | /jobs/v1/{name}/trigger | Manual trigger |
POST | /jobs/v1/webhooks/{token} | Webhook trigger |
GET | /jobs/v1/_admin/jobs/{name}/runs | List runs |
POST | /jobs/v1/_admin/jobs/{name}/runs/{id}/cancel | Cancel run |
GET | /jobs/v1/_admin/jobs/{name}/logs | Stream logs (SSE) |
Troubleshooting
Section titled “Troubleshooting”Run stuck in running
Section titled “Run stuck in running”The worker may have crashed mid-execution. Cancel and retry, or wait for timeout. Check function logs for the associated run_id.
reactor jobs runs get process-order run_01HZ...reactor jobs runs cancel process-order run_01HZ...reactor jobs runs retry process-order run_01HZ...Steps re-execute on every retry
Section titled “Steps re-execute on every retry”Ensure step names are stable strings. Renaming a step breaks the cache key. Step functions must be idempotent when they do run (cache miss).
429 concurrency_exceeded
Section titled “429 concurrency_exceeded”Too many concurrent runs for this job. Increase maxConcurrency or queue triggers across time.
Webhook returns 404
Section titled “Webhook returns 404”Token may be invalid or expired. Recreate the webhook trigger to get a new ingress URL.
Event trigger not firing
Section titled “Event trigger not firing”Verify the emitting job uses the exact topic string. Event matching is case-sensitive. Check _reactor_jobs.events for unconsumed events via admin API.