Queues, cron schedules, and cancellation for AI agents
To run agent workflows on a durable queue with Tidebase, enqueue them — dedupe keys, delays, retries with backoff, and per-queue concurrency caps are built in, and a queued job IS a run, so its status lives in the same authoritative lifecycle as everything else.
// enqueue: at most one active run per dedupe key, 3 attempts with backoff
await tide.enqueue('generate-report', {
queue: 'reports',
input: { topic },
dedupeKey: `report:${topic}`,
maxAttempts: 3,
deadlineMs: 600_000
})
// pull-mode worker: claims ready runs, executes registered workflows
tide.workflow('generate-report', generateReport)
await tide.work({ queues: ['reports'] })
Tidebase is an open-source checkpoint layer for AI agents: wrap your steps, and failed runs resume from the last safe point — in your own Postgres, without moving execution into a new runtime. Since v0.5 it can also decide when your code runs — it still never executes it.
Two dispatch modes
- Pull —
tide.work()claims ready runs withSKIP LOCKEDsemantics: two workers can never receive the same job, and per-queue concurrency caps and rate limits hold under contention. Available in TypeScript and Python (incl. asyncio). - Push — configure a queue with an
invokeUrland Tidebase delivers signedrun.invokewebhooks to your app (same HMAC as recovery webhooks). At-least-once with a redelivery horizon; beginning the run by id makes redelivery safe.
Cron schedules
await tide.schedules.set('daily-digest', { cron: '0 9 * * *', workflowName: 'daily-digest' })
Five-field UTC cron. Each fire enqueues with a dedupe key derived from the fire time (sched:<name>:<time>), so a double-fire is structurally impossible — even with multiple server replicas.
Retries, worker death, and the reconciler
A failed run with attempts remaining transitions back to queued with exponential backoff; exhausting maxAttempts records failure_class: 'max_retries'. If a worker dies mid-run, the lease expires and the reconciler — one advisory-locked loop — requeues the run (completed steps replay from checkpoints, so the retry never re-pays for finished work). Stalled non-queue runs get their signed recovery webhook fired automatically.
Cancellation
await tide.runs.cancel(runId, { reason: 'customer asked', actor: 'support' })
Cancellation is authoritative, durable, and one-way: status flips to cancelled immediately, in-flight workers observe it at their next step or gate boundary (RunCancelledError in TS, RunCancelled in Python — including a worker blocked waiting on a gate), and a complete or fail arriving afterwards is refused. Deadlines (deadlineMs) cancel automatically with reason deadline. It is impossible to miss because user code skipped a cleanup branch — the server enforces it, not your finally block.
The lifecycle, in one place
pending/queued → running → completed | failed | cancelled, with failure_class on terminal failures. Don’t mirror it into your own status columns — query GET /runs/:id or subscribe to events. Every guarantee on this page is enforced by an invariant test with concurrency probes against real Postgres.
See also: The replay contract · How to resume a failed run