Work queue

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