jobs.tasks and workflows in jobs.workflows, then use frogbot.jobs to enqueue and run them. For dedicated processes, see Job workers.
Queue a job
With async-user task registered that accepts userId, enqueue it from a hook or endpoint:
workflow instead of task for a registered workflow. Pass waitUntil: new Date(...) to delay eligibility. An explicit queue takes precedence over the workflow’s configured queue; otherwise jobs use default.
Reserve a jobId
jobId is an optional string, unique among unfinished jobs across all tasks, workflows, and queues. It stays reserved while the job is waiting (including a future waitUntil), processing, failed with retries left, or paused at waitFor. A paused workflow keeps it through delays, resumable waits, expiry, and the resumed run.
A duplicate jobs.queue with a reserved jobId creates nothing, does not throw, and resolves with the existing job record: the same id, original task or workflow, input, queue, waitUntil, and meta. The new call’s arguments are ignored, so use an ID that identifies the work, not just the caller. Concurrent calls from many instances end with one job on every supported database, subject to the MongoDB transaction exception below.
- The returned
job.idremains the generated primary key. Use it forrunByIDandcancelByID. - Omitting
jobIdpreserves ordinary enqueue behavior; multiple jobs can omit it. - Completion, failure for good, cancellation, or deletion frees the ID. Completion frees it whether the record is kept or deleted, with either
jobs.deleteJobOnCompletesetting.
jobId means “first live job wins; later duplicates are absorbed.” concurrency.supersedes instead deletes older pending jobs with the same concurrencyKey first, including paused workflows, freeing their IDs before inserting the replacement. A job that is already processing is not superseded; the duplicate resolves with it.
jobId is not an “only ever once” key and does not give exactly-once execution. After a job finishes, the same ID queues new work; recovery can also repeat an external side effect. Check your own records for side effects that must happen once.
Transactions
A duplicate inside your transaction resolves with the existing job and leaves the transaction usable on Postgres and SQLite. On Postgres, a duplicate of a job inserted by another still-open transaction waits until that transaction ends. On MongoDB, if two uncommitted transactions enqueue the same newjobId at the same moment, the server aborts the losing transaction with its retryable transient transaction error. FrogBot passes that error through unchanged; retry the whole transaction. Duplicates of committed jobs and calls outside a transaction do not error because of a duplicate. Unrelated database errors still reject.
Run and manage jobs
Access the API throughreq.frogbot.jobs or your initialized frogbot.jobs instance.
For example, a maintenance loop can schedule and run across every queue:
run also accepts where, processingOrder, sequential, and silent. Jobs run in parallel unless sequential: true. Queue, run, and cancel operations run the matching jobs.access rule unless overrideAccess: true is passed. The default rule allows a signed-in user, so workers, crons and other calls without a user pass overrideAccess: true.
An exceptional completion or database-write failure can reject a batch before already-started sibling work finishes. The batch promise does not guarantee that siblings have drained after such a failure.
Local API
run({ limit: 0 }) means unlimited. Only the worker CLI treats
--limit 0 as “run no jobs.”Method options
Every method that acceptsreq takes a FrogBotRequest. overrideAccess defaults to false, which runs the matching jobs.access rule against req.user; pass true to skip it.
queue accepts:
run accepts:
Task callbacks and retries
Task definitions supportonSuccess and onFail. Both callbacks are awaited: onSuccess runs after a successful handler, before its success checkpoint is saved; onFail runs when a task attempt fails, including attempts that will retry. It is not a final-failure-only callback.
Configured retries and checkpoint restoration remain supported. By default, a previously completed task restores its saved output without rerunning its handler or onSuccess; retry settings can disable restoration. Work without a durable checkpoint can run again, including callback side effects.
Job execution uses atomic claims and direct job-record updates at depth 0. It does not run jobs collection hooks. Ordinary application collection hooks, including those invoked by collection operations inside a task, remain supported.
Durable workflow waits
UsewaitFor from a jobs.workflows[].handler to wait for an approval, an external reply, or a date. FrogBot saves the wait and pauses the same job in its original queue; there is no separate continuation job. It stays pending with a future waitUntil: until for a delay, or the expiry for a resumable wait. Waiting holds no worker and consumes no retry. waitFor is a workflow-handler argument; it is not available to task handlers, piece actions, agent tools, or chat handlers.
Choose either
onWait or until; there is no type field. Call and await waitFor directly in the workflow handler, outside an inlineTask. Let its internal suspension signal reach FrogBot’s wrapper; do not swallow it in a catch-all error handler.
Runners pick up a paused job only once its waitUntil is strictly earlier than now. runByID runs it early; if the wait is still unresolved, it pauses again with the same waitUntil. A paused scheduled workflow counts as the running instance, so the default schedule check does not start an overlapping run.
Approval links
ConfigureserverURL with your app’s public origin so workers can generate reachable resume URLs. Register a workflow with an input field named requestId and this handler. In this example, sendApproval sends the supplied link by email or Slack, and recordApproval updates your application; both are application helpers that return serializable task results.
onWait({ resumeUrl }). Once the callback finishes, the handler suspends. On resume, the same named call returns its saved result without calling onWait again.
The generated URL has the shape /api/jobs/:token/resume under your configured API prefix. Opening it with GET shows confirmation without consuming the token; HEAD also leaves it unchanged. Submitting the empty confirmation form uses POST with application/x-www-form-urlencoded and resumes with data: {}. Link previews and scanners that only fetch the URL cannot approve the workflow.
Resume tokens are one-time bearer secrets: anyone holding one can respond to that wait. Share the generated URL only with intended recipients and keep it out of logs. The URL contains no source job ID. Expired or already-consumed tokens cannot resume again.
Resume from code
An email-reply trigger or webhook handler can use the same operation as the confirmation endpoint:req is optional; supply it when you have a request. In the workflow, waitFor<{ messageId: string }>(name, { onWait }) returns { expired: false, data: { messageId } } for that response. Check expired before reading data.
data must be a JSON value with finite numbers. Object keys named __proto__, constructor, or prototype are rejected at any depth before the token is consumed, preventing storage from silently removing them. A successful local resume returns { jobId }, the original job’s generated record ID, not its external jobId string, including when the response is accepted before onWait finishes. A resume call accepts input; it does not wait for the workflow to finish or return its final output.
Resume over HTTP
SendPOST to the generated URL with Content-Type: application/json. The entire JSON body becomes the wait’s data; for example, send {"messageId":"reply-42"} to receive { expired: false, data: { messageId: 'reply-42' } }. Do not wrap the body in a data property unless you want that property in the result.
JSON success returns HTTP 200 with { "ok": true }, including early acceptance. It does not include a job ID. Form success returns an HTML confirmation page. Nonempty forms are rejected with 400; other content types are rejected with 415.
JSON errors use { error: { code, message } }: malformed JSON returns 400 (INVALID_JSON), invalid values or reserved keys return 400 (INVALID_DATA), unknown tokens 404 (WAITPOINT_NOT_FOUND), consumed tokens 409 (WAITPOINT_CONSUMED), expired tokens 410 (WAITPOINT_EXPIRED), and unsupported content types 415 (UNSUPPORTED_MEDIA_TYPE). Internal failures return 500 (INTERNAL_ERROR); a dispatch failure can occur after the response was already accepted.
Dispatch and recovery limits
FrogBot atomically consumes each token once. Ordinary concurrent resume attempts have one winner. If a response arrives whileonWait or the pausing run’s write is still finishing, FrogBot buffers it durably and the sweep retries waking the job. Dispatch claims guard overlapping workers.
Dispatch wakes the original job instead of enqueueing another one. Recovery never recreates a missing or finished job. If a woken run crashes, lease recovery reruns that same job; external side effects still need at-least-once protection.
cancel and cancelByID end paused workflows, and concurrency.supersedes deletes older paused jobs with the same key. These operations free the jobId and end the wait; deleting a paused job also closes its wait. Its resume link then returns 409 (WAITPOINT_CONSUMED): “This FrogBot waitpoint cannot be resumed.”
Failed dispatches remain retryable by the scheduled sweep, even if local resume rejected or HTTP returned 500 after token consumption. Resubmitting a consumed token returns a conflict; keep the scheduler and recovery worker running to retry dispatch.
Replay and side effects
The resumed job reruns the workflow handler from the beginning. Arbitrary handler code runs again; FrogBot restores successfulinlineTask outputs from the prior log and returns saved results for earlier waits.
- Give each wait a nonempty, stable name unique within the workflow run. For loops, use a stable item identity such as
approval:${invoice.id}. Duplicate names and the reserved names__proto__,constructor, andprototypeare rejected. - Keep wait names, wait kinds, inline-task IDs, and branching consistent across retries and resumed runs. Use job input and saved task outputs for decisions rather than fresh randomness or changing external state.
- Put external calls and mutations in named
inlineTasks, including side effects insideonWait. Successful saved tasks restore their output by default instead of repeating the work. onWaitcan retry after callback failure or a process crash before readiness is saved. A side effect can also succeed before its task checkpoint is saved. NeitheronWaitnorinlineTaskguarantees exactly-once external effects; use the destination service’s idempotency key when available.
Durable delays
Use an absoluteDate or date string for until, derived from stable workflow input or a saved task result:
waitUntil to until; ordinary runners wait until that date has passed. An early runByID pauses again if the delay is not due. The call returns void when the workflow continues. A delay does not use onWait, a resume URL, or the resumable-wait expiry settings. Dates after 9999-12-31T23:59:59.999Z are rejected. Actual execution depends on the next worker batch and queue backlog.
Expiry
Configure instance-wide limits infrogbot.config.ts:
All durations are positive safe-integer milliseconds. The default must not exceed the maximum; per-wait
expiresIn must not exceed it either and must produce a valid date on or before 9999-12-31T23:59:59.999Z. The deadline starts when the wait is first created, including time spent in onWait.
Expiry continues the same job with { expired: true }, even if nobody attempts to resume it. Its jobId stays reserved if it waits again, until the workflow completes, fails for good, is cancelled, or is deleted. Runners can pick it up after its expiry waitUntil passes; the scheduled frogbot-sweep-waitpoints task also expires waits and wakes their jobs. A racing response and expiry have one winner. Keep scheduling and execution running for default, plus workers for the workflow’s original queue; see Workers for durable waits. A stopped worker does not extend token validity, but it delays expiry handling and execution.
Leases and crash recovery
Each run claims jobs with a freshprocessingToken and a processingUntil lease, and renews the lease while the jobs run. Task and workflow handlers need no heartbeat code. A job whose lease has passed can be claimed by any worker; an old worker can’t update a job once a successor holds it. FrogBot’s SQL adapters claim with FOR UPDATE SKIP LOCKED on Postgres, so concurrent workers take different jobs.
Choose a duration with headroom for event-loop pauses and database delays; it is not a maximum job runtime.
Keep a scheduler and worker running that cover the
default queue, or use allQueues: true for both scheduling and execution. They can share one process. The default queue runs the frogbot-sweep-waitpoints task, which expires waits and wakes their jobs.
Recovery is at least once. A crash, database outage, or event-loop stall can lose the lease after an external action has already happened. The lease protects job updates, not external effects. Make external operations idempotent, for example with the destination service’s idempotency key.
Configuration reference
Jobs options
Access options
Each rule receives{ req } and returns a boolean or a promise of one.
Autorun options
Task options
Workflow options
Retry options
retries takes a number of attempts or an object:
Concurrency options
concurrency takes a key function or an object:
Schedule options
Database requirements
Use the FrogBot adapters:@frogbotai/db-postgres, @frogbotai/db-vercel-postgres, @frogbotai/db-sqlite, @frogbotai/db-d1-sqlite, or @frogbotai/db-mongodb. They provide atomic claims and conditional lease updates without Redis.
The jobs collection includes a jobId field that is unique while its job is unfinished. New SQL projects get the partial unique index through their generated database migrations; apply them before starting workers. MongoDB creates its index on connect. See SQLite and D1 requirements for RETURNING support and local development.