Queue
The Queue block is a container block in Mandala that processes work items from a durable, shared queue at a configured concurrency. Unlike Loop and Parallel, a Queue is not scoped to a single workflow run: it is a persistent resource that accumulates items across every separate trigger of the parent workflow, and its nested blocks process one queued item at a time, per available concurrency slot.
A Queue block is a container node, like Loop and Parallel -- but the queue itself lives in the database, not in memory for the duration of one run. Every workflow execution that reaches this block (every incoming email, every webhook call, every scheduled trigger) adds items into the SAME queue instance, and the configured concurrency limit applies across all of them combined, not per-run.
Overview
The Queue block enables you to:
Bound concurrency globally: Process a fixed number of items at once, across every execution of the workflow combined
Accept items from two sources: Its own collection expression at config time, and an Agent block's queue_push_item tool call at runtime
Retry safely: Failed items are retried with exponential backoff, up to a configurable ceiling, before landing in a dead-letter state
Persist durably: Queued items, and their outcomes, survive independently of any single workflow run -- undeploying pauses the queue rather than deleting it
How It Works
- Instance created at deploy - Deploying the workflow creates (or updates) one durable queue instance per Queue block. Running or testing the workflow before its first deploy has nowhere durable to enqueue into yet.
- Items added - Each time the Queue block's own node runs in the parent workflow's execution, it evaluates its
Itemsexpression and enqueues the resulting items. Separately, an Agent block anywhere can callqueue_push_itemat any time to add more, targeting the queue by its configured name. - Deduplication (optional) - If a
Dedup Keyexpression is configured, it's evaluated per item before insert; an item whose key already exists for this queue is skipped rather than queued again. - Claim and dispatch - Whenever a concurrency slot is free, the oldest eligible pending item is atomically claimed and dispatched for processing. Capacity is enforced with a single conditional update against the queue instance, so it holds correctly no matter how many separate executions or agent pushes are contributing at once.
- Independent processing - The blocks nested inside the Queue container run as their own completely independent workflow execution -- one per claimed item, with their own execution id -- not as part of the parent workflow's DAG walk that reached the Queue block.
- Completion or retry - A successful item is marked completed. A failed item is retried with exponential backoff until it either succeeds or exhausts
Max Attempts, at which point it becomes dead-lettered. Either way, a slot frees up and the next pending item is claimed immediately.
Configuration Options
Items (optional) - A JavaScript expression evaluating to an array of items to enqueue whenever this block runs, e.g. <start.input.items>. Leave it empty if this queue is only ever fed at runtime via queue_push_item. The expression can be a plain variable reference, a literal JSON array, or genuine JavaScript (e.g. return items.filter(...)) -- all three are evaluated correctly.
Dedup Key (optional) - A JavaScript expression evaluated once per item (e.g. <item.id>) to compute its dedup key. If an item with the same key already exists for this queue, it's skipped instead of enqueued again. Leave this empty and every item is always treated as new -- no deduplication happens without an explicit key.
Concurrency - Maximum number of items processed at once for this queue, across ALL contributing executions combined -- not per run. Defaults to 1 (strictly one item at a time) if left unset.
Max Attempts - How many times a failed item is retried before it's dead-lettered. Defaults to 1, meaning no retry at all unless you raise it.
Shared Context (optional) - A JSON object (e.g. { "tenant": "<variable.tenantId>" }) available to every queued item's nested blocks as <queue.context.*>. It's snapshotted per item at the moment that item is enqueued, so a later config change doesn't retroactively affect items already queued, and concurrently-processing items don't share a live, mutable copy.
How to Use
Creating a Queue
- Add a Queue block to your canvas
- Configure
Items,Concurrency,Dedup Key,Shared Context, andMax Attemptsas needed - Drag the blocks that should process each item inside the Queue container, and connect them
- Deploy the workflow
The queue's durable instance is created at deploy time. Running or testing the workflow beforehand skips enqueueing entirely -- the block reports enqueuedCount: 0 with an explicit error rather than silently dropping items into somewhere non-durable.
Adding Items at Runtime
An Agent block can call the queue_push_item tool to push a new item into a Queue block's queue at any time, in addition to whatever the block's own Items expression already enqueues each run. The tool identifies the target by the Queue block's configured name (not its internal id), so give a Queue a clear, distinct name if your workflow has more than one -- nothing currently stops two Queue blocks from sharing a name, and a collision resolves to whichever is active, then whichever was deployed first. queue_push_item also accepts an optional dedupKey, so an agent can avoid pushing the same logical item twice.
Checking Queue Status
The queue_get_status tool returns counts of items that are pending, active, paused, completed, failed (currently retrying), and dead-lettered for a named queue -- across every execution that has contributed to it. Since a Queue is fire-and-forget by construction, this is how an agent (or a human) finds out what happened to items pushed or enqueued earlier, when the result can't simply be handed back inline in the same run.
Referencing the Current Item
Inside the blocks nested in a Queue container, the item being processed and its shared context are available as:
<queue.item>/<queue.item.someField>: The current item's payload<queue.context.someKey>: The shared context snapshot taken when this item was enqueued
These are only resolvable inside a queued item's own independent execution -- referencing <queue.*> anywhere else resolves to nothing.
Example Use Cases
Serializing Calls to a Rate-Limited Dependency
Scenario: Process invoices uploaded via WhatsApp, but the downstream RPA automation can only run one request at a time
- WhatsApp trigger fires once per incoming message/attachment
- Queue block enqueues the attachment as an item (Concurrency set to 1)
- Inside the queue: an RPA block submits the invoice to the automation
Because the queue is shared and durable, every uploaded invoice lands in the same queue no matter how many arrive concurrently, across however many separate WhatsApp-trigger executions that is. With Concurrency at 1, exactly one item is ever being processed at a time, so the RPA dependency is never hit with more than one request at once -- regardless of upload volume.
Agent-Driven Background Work
Scenario: An Agent decides mid-conversation that some follow-up work should happen without blocking its response
- Agent block calls
queue_push_itemto enqueue the follow-up work - Queue block (elsewhere in the workflow) processes it at its configured concurrency
- Agent later calls
queue_get_statusto check whether it has completed
Advanced Features
Deduplication
// Dedup Key field -- skip re-queueing the same logical item twice
<item.id>Deduplication is scoped to the queue instance: matching is against items already in that specific queue, keyed on the expression's evaluated value. If the expression fails to evaluate, or is left empty, that item is never deduplicated.
Retries and Backoff
A failed item is automatically retried with exponential backoff -- an increasing delay between attempts, capped at a fixed maximum -- rather than being re-claimed instantly. This is deliberate: an immediate retry against a still-rate-limited or still-failing dependency just burns the attempt budget without giving it time to recover. Backoff timing itself isn't currently user-configurable; Max Attempts is the one retry parameter you control.
Shared Context Snapshot
// Shared Context field
{
"tenant": "<variable.tenantId>"
}This object is captured once per item, at the moment that item is enqueued -- not read live while the item is processing. A batch of items enqueued together all see the same consistent context, even if the block's configuration changes (via redeploy) before every item in that batch has finished.
Concurrency and Ordering
Items are claimed oldest-first as capacity becomes available, and capacity is enforced across every contributing execution, not per-run. A slot frees up the moment an item finishes (success, failure, or dead-letter), so the next pending item is claimed immediately rather than waiting for a periodic sweep.
Human-in-the-Loop Items
Because each queued item runs as its own independent workflow execution, it can contain a human-in-the-loop block like any other workflow. A paused item releases its concurrency slot immediately rather than holding it for as long as the human takes to respond, and resumes -- completing or failing -- once they act.
Nesting
Loop and Parallel blocks nested inside a Queue's own body run normally, self-contained within that single item's independent execution. Nesting a Queue block inside another Queue block is not supported.
A Queue's own body has nowhere durable to enqueue into before the workflow's first deploy -- test runs before that report zero items enqueued with an explicit error, rather than silently discarding them.
Undeploying a workflow pauses its queues (they stop accepting new items) rather than deleting them -- items already in the queue, and their history, remain in place. Redeploying reactivates them.
Queue vs Loop/Parallel
| Feature | Queue | Loop / Parallel |
|---|---|---|
| Scope | Durable, shared across every trigger of the workflow | Scoped to a single workflow run |
| Item processing | Own independent execution, once per queued item | Iterations/instances within the same run |
| Concurrency | Configurable, enforced globally across all contributing executions | Sequential (Loop), or bounded per-run (Parallel) |
| Persistence | Items and outcomes persist in the database; paused (not deleted) on undeploy | Exists only for the duration of the run |
| Adding work | Config-time Items expression AND runtime queue_push_item agent tool | Config-time only |
Inputs and Outputs
Items: Expression evaluating to an array of items to enqueue when this block runs
Concurrency: Maximum items processed at once, across all executions
Dedup Key: Per-item expression; matching items are skipped
Shared Context: JSON object snapshotted per item as
<queue.context.*>Max Attempts: Retry ceiling before an item is dead-lettered
queue.item: The current item's payload (inside the queue body only)
queue.context: The shared context snapshot for this item
enqueuedCount: How many items were newly enqueued by this run of the block
skippedDuplicates: How many items were skipped as duplicates
queueId: This queue block's own id
Best Practices
- Deploy before relying on it: The queue instance is created at deploy time, so any test run beforehand has nowhere durable to enqueue into.
- Set concurrency to match the downstream dependency, not your traffic: It's a global cap across every trigger of the workflow combined, not a per-run setting -- size it to what the thing you're calling can actually handle.
- Name queues distinctly:
queue_push_itemandqueue_get_statusresolve by the Queue block's configured name, and nothing currently prevents two Queue blocks from sharing one. - Add a Dedup Key whenever the same logical item could arrive twice: Without one, every item -- including retried triggers or duplicate uploads -- is always treated as brand new.
- Raise Max Attempts deliberately: It defaults to 1, i.e. no retry, so a transient failure dead-letters immediately unless you explicitly configure headroom.