Work queue
The work queue lets a trigger ACK HTTP immediately and still run the agent or harness in the background — including on another replica when a durable adapter is bound.
The host supplies the queue through IPlatformAdapter.queue(). Dev uses in-memory in the same process. Production can swap RabbitMQ (or similar) without changing triggers.
Why a queue
Slack (and similar webhooks) must receive HTTP 200 quickly or they retry. The agent run can take seconds. fork enqueues that run, waits until the queue accepts the job, then returns 200. A worker later calls next.activate (agent or harness, then the bound outcome).
Webhook POST
→ Trigger.activate
await next.enqueue(args) // job accepted
return 200
Worker
→ next.activate(args) // model + configured outcome
Every shuttl serve replica produces and consumes, so work is shared when the adapter is durable.
fork on triggers
async activate(args: ActivationArgs, next: ITriggerNext) {
const { parsed, prompt } = await this.parseArgs(args.request);
await this.fork(next, { request: args.request, parsed, prompt });
return new BufferedHttpResponse(200, "");
}
fork is await next.enqueue(args). If enqueue fails, activate throws and the host returns HTTP 500 so the webhook can retry. The HTTP response never waits for the model.
Handshake, 401, and skip paths return buffered HTTP and do not enqueue.
ApiTrigger still return next.activate(...) (synchronous for the caller). It does not use the queue.
Jobs
The queue stores data, not a live next object:
| Field | Meaning |
|---|---|
id |
Job id |
runnableName |
Agent or harness name |
triggerName |
Trigger that already ran activate |
request / parsed / prompt |
ActivationArgs |
attempt |
0 on first try |
threadId |
Optional conversation thread |
The worker looks up the runnable and trigger, rebuilds next, and calls next.activate. It does not run the trigger again (that would ACK Slack twice).
In-memory defaults
| Knob | Default |
|---|---|
| Concurrency | 4 workers in this process |
| Retries | 5 attempts |
| Backoff | retryBaseMs * 2^attempt (1s, cap 60s) |
| Poison | After max attempts: log and drop |
A process crash drops in-memory jobs. Durable adapters keep them.
import { InMemoryPlatformAdapter } from "@shuttl-io/core";
app.bindAdapter(new InMemoryPlatformAdapter({
concurrency: 8,
maxAttempts: 5,
}));
See Adapters for binding and writing a broker-backed adapter.
Next steps
- 📦 Adapters —
IPlatformAdapter,bindAdapter - ⚡ Triggers — custom
forkpaths - 📤 Outcomes — where queued work delivers