← Back to API docs

Docs

Tasks API

A tasks instance holds queues. A task is one JSON payload. A push queue delivers each task to a function, a channel or a webhook, and retries it until the target accepts it or its attempts run out. A pull queue holds tasks for your own workers to lease.

Create a queue and add a task

Create a tasks instance — here jobs — in the console or with MCP's create_instance. Queues are created on its Queues tab, or over the API:

curl -X POST https://api.altengine.net/v1/tasks/jobs/queues \
  -H "Authorization: Bearer $ALTENGINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"emails","target":{"function":"app/send-receipt"},"ratePerSecond":5}'

curl -X POST https://api.altengine.net/v1/tasks/jobs/queues/emails/tasks \
  -H "Authorization: Bearer $ALTENGINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payload":{"orderId":"o-1041"},"name":"receipt-o-1041","delay":60}'

# → 201 { "tasks": [{ "ok": true, "id": "0mvpn7bai3f9c2e81d4a7", "name": "receipt-o-1041",
#                     "state": "scheduled", "runAt": 1793042520000 }] }

The function send-receipt in functions instance app is called a minute later. A task with no delay runs within about a second. Every instance has a queue named default.

From a function granted tasks, the same add is one call — answer the request now and do the slow work in the background:

await env.tasks.add({ instance: "jobs" }, "emails", { payload: { orderId }, name: `receipt-${orderId}` });
return Response.json({ ok: true });

Queues

POST /v1/tasks/{instance}/queues creates one: {name, …settings}. PATCH /v1/tasks/{instance}/queues/{queue} changes only the settings you send.

Queue settings
SettingDefaultMeaning
modepushpush delivers to a target. pull holds tasks for workers to lease, and has no target.
targetnoneExactly one of {"function": "<functions-instance>/<function>"}, {"channel": "<channel-instance>/<channel>"} or {"webhook": "https://…"}. Instances are named in your own organization, and must exist when you set it. Naming a channel needs a write grant on that channel instance. A task's own target overrides it.
ratePerSecond10Deliveries started per second, 0.01–100.
maxConcurrent5Deliveries in flight at once, 1–50.
deadlineSeconds600How long a function or webhook target is waited for, 15–840. Past it the attempt counts as failed and is retried.
retrysee RetriesThe retry policy for its tasks.
deadLetterQueuenoneAnother queue in this instance that gets a copy of every task that fails. See Dead-letter queues.
pausedfalseStops delivery and leasing. Tasks keep arriving and wait.
targetControltrueLets the target's answer steer its own retries.

Across all its queues, an instance has at most 50 deliveries in flight.

Adding tasks

POST /v1/tasks/{instance}/queues/{queue}/tasks takes one task, an array, or {"tasks": [...]} — up to 100.

Task fields
FieldMeaning
payloadRequired. Any JSON up to 128 KB; null for none. Put larger data in a blob and pass its key.
nameOptional. Makes the add idempotent. 1–500 characters of A–Z a–z 0–9 _ . : -.
delaySeconds from now.
runAtEpoch milliseconds or an ISO 8601 time. Give delay or runAt, not both; at most 30 days ahead.
targetOverrides the queue's. Not allowed on a pull queue. A channel target needs a write grant on that channel instance.
paramsA JSON object up to 4 KB, handed to the target as task.params.
deadlineSecondsOverrides the queue's deadline for this task, 15–840.
retryThis task's own retry policy. It replaces the queue's whole: fields it leaves out take the defaults, not the queue's values.

Each result in tasks is {ok: true, id, name, state, runAt} or {ok: false, code: "TASK_EXISTS", id, name, message}, in request order. The call answers 201 when at least one task was added, and 409 when none was — the same results, plus an error naming the first refusal. A push queue with no target refuses a task that has none of its own (400 NO_TARGET), and nothing in that call is added.

Names make an add idempotent

A name already used in the queue is refused with TASK_EXISTS and the existing task's id — for as long as that task's record is kept, finished or not: retentionDays after it finished, 7 by default. Name a task after the thing it does (receipt-o-1041) and a retried request cannot add it twice.

What a target receives

A function and a webhook both get a POST with this body:

{ "task": { "id": "0mvpn7bai3f9c2e81d4a7", "queue": "emails", "name": "receipt-o-1041",
            "attempt": 1, "maxAttempts": 10, "runAt": 1793042520000, "createdAt": 1793042460000,
            "deadlineAt": 1793043120000, "source": "api", "depth": 0, "params": null,
            "deadLetterOf": null },
  "payload": { "orderId": "o-1041" } }
What each kind of target receives
TargetReceives
FunctionThe body above, with x-ae-trigger: task, x-task-id, x-task-queue and x-task-attempt. Waited for until the deadline. Redirects are not followed.
WebhookThe body above and the same x-task-* headers, signed. Waited for until the deadline. Redirects are not followed.
ChannelA message {"type": "task", "task": {…}, "payload": …} on that channel. At most 32 KB as published.

source says where the task came from: api, console, mcp, function, datastore, blob, mailbox, deadletter or rerun.

Delivery is at least once. A delivery cut off mid-way — the queue restarted while it ran — is retried, so a target can see the same task twice. Make the handler idempotent on task.id.

The deadline

A function or webhook target has until task.deadlineAt (epoch ms) to answer: the queue's deadlineSeconds after the attempt starts, 600 by default, or the task's own. Past it the attempt counts as failed and is retried. The target is not stopped: its slot stays taken and the retry waits until it returns, so maxConcurrent is never exceeded and a task never runs beside itself. The timed-out attempt records what the target finally answered. For work longer than 14 minutes, use a pull queue, whose worker extends its lease.

What the answer means

How a target's answer decides the task's outcome
AnswerOutcome
2xx, or the channel message publishedDone. The response body is kept as the task's result, up to 16 KB.
408, 429, 5xx, no answer by the deadline, a network errorRetried with backoff.
Any other 4xxFailed at once. A payload the target refuses does not improve by retrying.
3xxFailed at once.
The function, its instance or the channel instance does not exist; a channel message over 32 KBFailed at once.

A target is looked up again on every attempt, so one deleted after the task was added fails it then.

export default {
  async fetch(request, env) {
    const { task, payload } = await request.json();
    if (await alreadySent(task.id)) return new Response("dup");   // at-least-once
    await sendReceipt(payload.orderId);
    return new Response("sent");                                  // 2xx: done
  },
};

Verifying a webhook

Every webhook delivery carries x-altengine-timestamp (ms) and x-altengine-signature: v1=<hex>, an HMAC-SHA256 keyed with the instance's signing key — the whole string, whsec_ prefix included — over <timestamp>.<raw body>. Owners reveal and rotate the key in the instance's Settings, or with MCP's tasks_reveal_webhook_secret and tasks_rotate_webhook_secret.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req, rawBody, secret) {
  const ts = req.headers["x-altengine-timestamp"];
  const want = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const got = req.headers["x-altengine-signature"] ?? "";
  return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want))
    && Math.abs(Date.now() - Number(ts)) < 5 * 60_000;   // reject replays
}

Rotating takes effect on the next delivery — update the receiver straight away. A webhook URL is https, with no username or password, at most 2,048 characters.

Retries

Retry policy fields
FieldDefaultRangeMeaning
maxAttempts101–100Attempts before the task fails, the first included.
minBackoffSeconds101–86,400The wait after the first failed attempt.
maxBackoffSeconds3,6001–604,800The longest wait. At least minBackoffSeconds.
maxDoublings80–16How many times the wait doubles before it grows linearly.
maxAgeSeconds604,800 (7 days)0–2,592,000A task whose next attempt would fall later than this after it was added fails instead. 0 is no limit.

After the nth failed attempt the wait is minBackoffSeconds × 2^(n−1). Past maxDoublings it grows by the last doubled step each time instead, and never exceeds maxBackoffSeconds. With the defaults: 10 s, 20 s, 40 s … 2,560 s before the tenth and last attempt.

Steering retries from the target

A function or webhook can change what happens next with headers on its answer:

// the record this task needs is not there yet: try again in a minute, without using an attempt
return new Response("waiting", { status: 503, headers: { "x-task-retry": "defer", "retry-after": "60" } });
Response headers a target can use to steer its retries
HeaderEffect
Retry-After or x-task-retry-afterSeconds, or an HTTP date: when the next attempt runs, instead of the backoff. At most 30 days out.
x-task-retry: neverFail now, whatever the status and the attempts left.
x-task-retry: retryCount this as a failed attempt and retry — even a 2xx, or a 4xx that would otherwise fail the task.
x-task-retry: deferRun again at Retry-After (or after minBackoffSeconds) without spending an attempt, whatever the status. A task can be deferred 1,000 times.
x-task-max-attempts: nThis task's attempt limit from now on, 1–100.

maxAgeSeconds still ends a task that is retried or deferred past it. Set the queue's targetControl to false and these headers are ignored.

Watching and managing tasks

In the console, a tasks instance's Queues tab shows each queue's counts by state; the Tasks tab lists them by queue and state, opens one with its attempts, and runs, retries or cancels it. The same operations over the API:

Task states
StateMeaning
scheduledWaiting for its time, or for its next attempt.
runningBeing delivered now.
leasedHeld by a pull worker.
done, failed, cancelledFinished. Kept for retentionDays (7 by default), then deleted.

GET …/queues/{queue}/tasks lists one page without payloads. Filter with state or name; page with limit (50 by default, at most 200) and the cursor from the previous page — null is the last page. Scheduled, running and leased tasks come soonest first, finished ones latest first, and with no state newest first. GET …/tasks/{id} returns one task with its payload, result, and every attempt with its status and error. A queue's counts by state stop at 10,000; a state listed in capped has at least that many.

Operations on tasks and queues
ToCallDoes
Run now, retry or re-runPOST …/tasks/{id}/runA scheduled task runs now. A failed or cancelled one is scheduled again with its attempts reset. A done one is run again as a new task with the same payload, params and target and no name; the call returns the new task. A running or leased task is refused (409).
CancelDELETE …/tasks/{id}, or POST …/tasks/cancel {"ids": [...]} (up to 200)Cancels scheduled and leased tasks. A running delivery cannot be stopped. The batch form returns {cancelled, skipped}, each skipped id with its state.
Pause, resumePOST …/queues/{queue}/pause, /resumeStops and restarts delivery. Nothing is lost.
PurgePOST …/queues/{queue}/purgeDeletes every task in the queue, in any state, and keeps the queue. A running delivery finishes and its outcome is dropped. Returns {purged, pending}; pending: true means the rest is deleted in the background.
Delete a queueDELETE …/queues/{queue}Deletes the queue and every task in it. default cannot be deleted, and a queue that is another's dead-letter queue is refused (409 QUEUE_IN_USE).

Pull queues

A queue with "mode": "pull" never delivers. Your worker leases tasks, does the work, and acknowledges each one:

POST /v1/tasks/jobs/queues/thumbnails/lease
{ "count": 10, "leaseSeconds": 120 }
→ { "tasks": [{ "id": "0mvpn7bai…", "payload": { "key": "blob:9f3c…:7a1b…" }, "attempts": 1,
                "leaseId": "c3a9…", … }] }

POST /v1/tasks/jobs/tasks/0mvpn7bai…/ack
{ "leaseId": "c3a9…", "result": { "width": 320 } }
Pull queue calls
CallBodyDoes
POST …/queues/{queue}/lease{count?, leaseSeconds?}Leases up to count (1–100, default 1) due tasks for leaseSeconds (1–3,600, default 300). Each lease is an attempt. A paused queue returns none.
POST …/tasks/{id}/ack{leaseId, result?}Done. result is kept up to 16 KB.
POST …/tasks/{id}/extend{leaseId, leaseSeconds}Holds the lease longer, from now.
POST …/tasks/{id}/release{leaseId, delaySeconds?, error?}Gives the task back to be leased again after delaySeconds. The attempt counts as failed; on the last one the task fails.

A lease that runs out without an ack counts as a failed attempt, and the task is retried with backoff. Every call after that with the old leaseId answers 409 LEASE_LOST, so a slow worker cannot finish a task another worker now holds.

Dead-letter queues

Set a queue's deadLetterQueue to another queue in the same instance, which must already exist. When a task fails, a copy is added there: the same payload and params, no name, and task.deadLetterOf set to the failed task's id. The dead-letter queue delivers it to its own target with its own retry policy, or, as a pull queue, holds it for you to lease.

Triggers

Other services can add tasks as things happen. A trigger names a queue as <tasks-instance>/<queue>; the queue's target runs the task, at the queue's rate and with its retries.

Trigger sources
WhenConfigured onPayload
A document is writtenA datastore instance's triggersdatastore.write
A file is uploaded or deletedA blob instance's triggersblob.create, blob.delete
Mail arrivesA mailbox route with a queue actionThe message, as a function receives it

The queue must exist when the rule is saved, and must have a target or be a pull queue. A rule's params reach the target as task.params. A chain of triggers more than 8 deep — a function writing the document that triggered it — is cut.

From a function

Grant a function tasks and it gets env.tasks. Methods return the value REST wraps — add the array of results, get the task — and take the same bodies and limits.

env.tasks methods and the grant each needs
MethodGrant
queues(t), getQueue(t, name), get(t, id), list(t, queue, {state, name, cursor, limit})read
add(t, queue, tasks), run(t, id), cancel(t, ids)write
lease(t, queue, {count, leaseSeconds}), ack(t, id, leaseId, result?), extend(t, id, leaseId, leaseSeconds), release(t, id, leaseId, {delaySeconds, error})write
createQueue(t, name, settings), updateQueue(t, name, settings), pauseQueue(t, name), resumeQueue(t, name), purgeQueue(t, name), deleteQueue(t, name)full

Endpoints and grants

Every call takes an organization API key with a grant on the instance. An end-user identity token is refused. Changing a queue needs full: its target runs every task with that target's own access.

Tasks endpoints and the grant each needs
EndpointGrantDoes
GET /v1/tasks/{instance}readThe instance's name, retentionDays and queues.
GET /v1/tasks/{instance}/queuesreadEvery queue, with its settings and counts by state.
POST /v1/tasks/{instance}/queuesfullCreate a queue.
GET /v1/tasks/{instance}/queues/{queue}readOne queue.
PATCH /v1/tasks/{instance}/queues/{queue}fullChange the settings sent.
DELETE /v1/tasks/{instance}/queues/{queue}fullDelete the queue and its tasks.
POST /v1/tasks/{instance}/queues/{queue}/pause, /resumefullStop or restart delivery.
POST /v1/tasks/{instance}/queues/{queue}/purgefullDelete every task in it.
POST /v1/tasks/{instance}/queues/{queue}/taskswriteAdd up to 100 tasks.
GET /v1/tasks/{instance}/queues/{queue}/tasksreadList, by state or name.
POST /v1/tasks/{instance}/queues/{queue}/leasewriteLease from a pull queue.
GET /v1/tasks/{instance}/tasks/{id}readOne task, with its payload, result and attempts.
DELETE /v1/tasks/{instance}/tasks/{id}writeCancel a scheduled or leased task.
POST /v1/tasks/{instance}/tasks/cancelwriteCancel up to 200: {"ids": [...]}.
POST /v1/tasks/{instance}/tasks/{id}/runwriteRun now, retry, or re-run.
POST /v1/tasks/{instance}/tasks/{id}/ack, /extend, /releasewriteFinish, hold or give back a leased task.

Limits and settings

Tasks limits and settings
Limit or settingValue
Queues50 per instance, default included. A name is 1–63 characters of a–z 0–9 _ -, starting with a letter or digit.
Tasks per add100, in a request body of at most 4 MB.
Payload128 KB as JSON. params: 4 KB.
Result kept16 KB of the target's answer.
Scheduled ahead30 days.
DeliveryUp to 100 a second and 50 at once per queue; 50 at once per instance.
Waiting for an answer (deadlineSeconds)600 seconds by default, 15–840, per queue or per task. See The deadline.
Leases1–100 tasks per call, held 1–3,600 seconds (300 by default).
Deferrals1,000 per task.
Trigger chain8 deep.
Keep finished tasks (retentionDays)7 days by default, 1–30. A task's name stays taken until then.
Rate limit (rateLimit)6,000 API calls a minute per instance by default; a value you set can only lower it. Delivery is not affected by it.
RegionChosen when you create the instance, and fixed after.

Change retentionDays and rateLimit in the instance's Settings, or with MCP's patch_instance_config.

What it costs

Tasks added, deliveries to a channel or webhook target (every attempt), and the data the instance keeps, averaged over the month. A delivery to a function bills as a function invocation instead, and one to a channel also bills as a channel message. See pricing for the numbers.

Testing locally

The emulator serves the same API, env.tasks, triggers and MCP tools, and delivers a task within about a second of it falling due, logging every attempt to the terminal.

altengine dev     # http://127.0.0.1:9191

Where it differs:

  • A tasks instance is created on first use.
  • Purging a queue and deleting one finish at once; pending is always false.
  • A trigger rule change applies immediately, not within about 30 seconds.
  • A trigger event that cannot be queued is logged to the terminal, not shown as a console notice.
  • Revealing and rotating the webhook key are not limited to owners, and MCP scopes are not checked.

From an AI agent

The MCP server exposes tasks directly: tasks_list_queues, tasks_set_queue (create or change), tasks_add, tasks_list, tasks_get, tasks_run, tasks_cancel, and an agent can be a pull worker itself with tasks_lease, tasks_ack and tasks_release. tasks_purge_queue and tasks_delete_queue need full plus confirm: true. The resource docs://tasks/guide covers targets, retries and triggers.