Scheduled Jobs
Reactor Jobs runs background work on a schedule or in response to events. This guide covers cron jobs, webhook-triggered runs, job handlers, and monitoring — integrated with Data and Functions.
Prerequisites: Running Reactor server with [jobs] configured, deployed function or script handler.
1. Configure Jobs
Section titled “1. Configure Jobs”[jobs]worker_count = 4scheduler_interval_ms = 1000default_timeout_ms = 600000 # 10 minuteswebhook_secret = "{{ env REACTOR_JOBS_WEBHOOK_SECRET }}"max_org_concurrent_runs = 50Verify the scheduler is running:
curl http://localhost:8000/health | jq '.capabilities.jobs'# "ok"2. Define a job handler
Section titled “2. Define a job handler”Job handlers are functions deployed to the Functions capability:
import { ReactorClient } from "@reactor/client";
interface JobPayload { org_id: string; report_type: "daily" | "weekly";}
export default async function handler(req: Request): Promise<Response> { const payload: JobPayload = await req.json(); const reactor = ReactorClient.service(); // service role — bypasses RLS
// Aggregate data const yesterday = new Date(Date.now() - 86400_000).toISOString(); const { data: orders } = await reactor.data .from("orders") .select("total, status") .eq("org_id", payload.org_id) .gte("created_at", yesterday);
const summary = { total_orders: orders?.length ?? 0, revenue: orders?.reduce((sum, o) => sum + o.total, 0) ?? 0, completed: orders?.filter(o => o.status === "completed").length ?? 0, };
// Store report await reactor.data.from("reports").insert({ org_id: payload.org_id, type: payload.report_type, data: summary, generated_at: new Date().toISOString(), });
console.log(`Report generated for org ${payload.org_id}:`, summary); return Response.json({ ok: true, summary });}Deploy:
reactor functions deploy daily-report3. Register jobs
Section titled “3. Register jobs”Jobs are registered via manifest in your deploy bundle or the Jobs API:
{ "jobs": [ { "id": "daily-sales-report", "name": "Daily Sales Report", "handler": "daily-report", "timeout_ms": 300000, "retries": 3, "triggers": [ { "type": "cron", "schedule": "0 6 * * *", "timezone": "America/New_York", "payload": { "org_id": "00000000-0000-0000-0000-000000000001", "report_type": "daily" } } ] }, { "id": "cleanup-expired-sessions", "name": "Cleanup Expired Sessions", "handler": "cleanup-sessions", "timeout_ms": 60000, "retries": 1, "triggers": [ { "type": "cron", "schedule": "*/15 * * * *", "payload": {} } ] } ]}Deploy the manifest:
reactor deploy # includes jobs/manifest.json in bundleOr register via API:
curl -X POST \ -H "Authorization: Bearer $USER_JWT" \ -H "Content-Type: application/json" \ http://localhost:8000/jobs/v1/jobs \ -d @jobs/manifest.json4. Cron schedule reference
Section titled “4. Cron schedule reference”Reactor uses standard 5-field cron syntax:
┌───────────── minute (0-59)│ ┌───────────── hour (0-23)│ │ ┌───────────── day of month (1-31)│ │ │ ┌───────────── month (1-12)│ │ │ │ ┌───────────── day of week (0-6, Sun=0)│ │ │ │ │* * * * *| Schedule | Expression |
|---|---|
| Every minute | * * * * * |
| Every 15 minutes | */15 * * * * |
| Every hour | 0 * * * * |
| Daily at 6 AM | 0 6 * * * |
| Weekdays at 9 AM | 0 9 * * 1-5 |
| First of month | 0 0 1 * * |
{ "type": "cron", "schedule": "0 9 * * 1-5", "timezone": "Europe/London", "payload": { "action": "send_digest" }}Always specify timezone for user-facing schedules. UTC is the default if omitted.
5. Webhook triggers
Section titled “5. Webhook triggers”Trigger jobs from external systems (Stripe, GitHub, custom apps):
// Register webhook trigger in manifest{ "id": "stripe-payment-processor", "handler": "process-payment", "triggers": [ { "type": "webhook", "path": "/hooks/stripe", "method": "POST", "verify": { "type": "hmac", "header": "stripe-signature", "secret": "vault:jobs/stripe_webhook_secret" } } ]}External caller:
curl -X POST \ -H "Content-Type: application/json" \ -H "stripe-signature: t=123,v1=abc..." \ https://api.myapp.com/jobs/v1/hooks/stripe \ -d '{"type":"payment_intent.succeeded","data":{...}}'Manual trigger from your app:
await reactor.jobs.trigger("daily-sales-report", { org_id: org.id, report_type: "weekly",});6. Job with Data + Storage
Section titled “6. Job with Data + Storage”A cleanup job that archives old files and deletes database records:
export default async function handler(req: Request) { const reactor = ReactorClient.service(); const cutoff = new Date(Date.now() - 90 * 86400_000).toISOString();
const { data: files } = await reactor.data .from("uploads") .select("id, storage_path") .lt("created_at", cutoff) .eq("archived", false) .limit(100);
for (const file of files ?? []) { // Move to archive bucket await reactor.storage .from("uploads") .move(file.storage_path, `archive/${file.storage_path}`);
await reactor.data .from("uploads") .update({ archived: true, archived_at: new Date().toISOString() }) .eq("id", file.id); }
return Response.json({ archived: files?.length ?? 0 });}7. Monitoring and debugging
Section titled “7. Monitoring and debugging”List recent runs
Section titled “List recent runs”curl -H "Authorization: Bearer $JWT" \ "http://localhost:8000/jobs/v1/runs?limit=10" | jq[ { "id": "run_01HZ...", "job_id": "daily-sales-report", "status": "completed", "started_at": "2026-05-29T06:00:01Z", "completed_at": "2026-05-29T06:00:04Z", "duration_ms": 3200 }]Prometheus metrics
Section titled “Prometheus metrics”# Job failure raterate(reactor_jobs_runs_total{status="failed"}[1h])
# Queue depthreactor_jobs_queue_depth
# Run duration P95histogram_quantile(0.95, rate(reactor_jobs_run_duration_seconds_bucket[1h]))reactor logs --capability jobs --follow8. Error handling and retries
Section titled “8. Error handling and retries”{ "id": "sync-external-api", "handler": "sync-api", "timeout_ms": 120000, "retries": 5, "retry_backoff": { "type": "exponential", "initial_ms": 1000, "max_ms": 60000, "multiplier": 2 }, "triggers": [{ "type": "cron", "schedule": "0 */4 * * *" }]}In the handler, return structured errors for retryable vs fatal failures:
try { await syncData(); return Response.json({ ok: true });} catch (err) { if (err.code === "RATE_LIMITED") { // Will retry with backoff return new Response(JSON.stringify({ error: err.message }), { status: 503 }); } // Fatal — won't retry return new Response(JSON.stringify({ error: err.message }), { status: 422 });}Summary
Section titled “Summary”| Trigger type | Use case |
|---|---|
| Cron | Reports, cleanup, periodic sync |
| Webhook | External events (payments, CI, webhooks) |
| Manual | On-demand via SDK or API |
| Data event | React to inserts/updates (via function trigger) |
Related
Section titled “Related”- File uploads — storage cleanup jobs
- Multi-tenant app — per-org job scheduling
- Configuration —
[jobs]reference