Skip to content

Channels, Pauses & HTTP Webhook Execution

Production workflows often start from incoming HTTP webhooks (Stripe events, GitHub webhooks, Slack slash commands, CRM triggers) and pause mid-run for human sign-off before executing high-stakes tool calls. AGBoss handles human-in-the-loop pauses out-of-process: when a tool’s @Channel returns ChannelOutcome.awaitReply(), the branch pauses while sibling parallel branches continue until nothing else can move, persisting a PendingPause record and releasing the worker’s 30-second execution lease.


Complete example: @ChannelRole, @Channel, @ReplyInterpreter, HTTP webhooks & test

Section titled “Complete example: @ChannelRole, @Channel, @ReplyInterpreter, HTTP webhooks & test”

How to run & resume workflows via HTTP webhooks in production

Section titled “How to run & resume workflows via HTTP webhooks in production”

When embedding AGBoss inside an HTTP server (Fastify, Express, Hono, Next.js route handlers, or NestJS), follow these five architectural rules:

  1. Boot createApp once at process startup — never per request: Call const app = await createApp(RefundApplication, { env: "prod" }) once when your HTTP server starts. createApp initializes your DI container (onInit -> onStart), opens the shared SQLite/Postgres connection pool, starts the 30-second orphan recovery sweep, and validates all @ModelProvider and environment contracts.
  2. Use deterministic threadId from the webhook’s domain entity: When starting a workflow from a webhook (app.execute(Start, dto, { threadId: "order:ord_99", ownerId, tenantId })), derive threadId from your business entity ID (or webhook event ID) so multi-turn follow-ups and app.cancel(threadId) address the exact conversation thread.
  3. Return 202 Accepted immediately when result.status === ExecutionStatus.Paused: When a tool requires approval, app.execute does not hold open a Promise or block a Node.js thread. It checkpoints the graph, writes a PendingPause row, calls your @Channel.send(ask) (which delivers ask.pauseId to Slack/email/webhook), releases the execution lease, and resolves app.execute with result.status === ExecutionStatus.Paused.
  4. Resume with two-phase app.resume() on the callback webhook: Your callback endpoint (POST /webhooks/slack-actions) can hit any replica in your cluster. Calling app.resume({ pauseId, decision: { approved: true } }, { tenantId, answeredBy }) (or { pauseId, reply: freeText }) atomically flips the pause Pending → Resolved in Phase 1 and acquires the 30s execution-tree lease in Phase 2.
  5. Handle webhook retries & ambiguous replies explicitly:
    • NotPausedError: External webhook providers (Slack, Stripe, GitHub) retry on network timeouts, and humans double-click buttons. Catch NotPausedError and return 200 OK idempotently.
    • UninterpretableReplyError: When a human sends a free-text reply that @ReplyInterpreter cannot parse (ReplyInterpretation.notUnderstood({ reason })), app.resume throws UninterpretableReplyError and keeps the pause Pending so your webhook can reply in the Slack thread with err.reason and wait for a clearer answer.
    • UnknownPauseError: The pauseId does not exist, belongs to another tenant, or was pruned after pauseRecordRetention (hours(24) default); return 404 Not Found.

Capability security (PauseAnswerer) & audit trail (result.record.pauses)

Section titled “Capability security (PauseAnswerer) & audit trail (result.record.pauses)”
  • PauseAnswerer.Channel (External Reviewer): result.pending intentionally omits pauseId so the caller of app.execute (e.g., an end user in chat) cannot approve their own tool call. Only your @Channel.send(ask) receives ask.pauseId, and app.resume requires { answeredBy: "<reviewer-id>" }.
  • PauseAnswerer.ThreadOwner (End-User Confirmation): result.pending includes pauseId so the user’s client UI can render an inline Confirm/Cancel prompt, and app.resume verifies { ownerId } matches the thread owner.
  • Automatic button cleanup (ChannelHandler.withdraw): If a parallel branch pause becomes obsolete (FailurePolicy.FailFast, quorum BranchVerdict.Surplus, or app.cancel), AGBoss calls withdraw(withdrawal) on your @Channel with PauseResolution.Withdrawn or PauseResolution.Cancelled so your Slack/UI integration can disable stale buttons immediately.