Route Sume run webhooks by event: format, action and agent terminal

Run webhooks use one terminal event per family: action.run.terminal, format.run.terminal, agent.run.terminal. Route on event, then branch on outcome.

5 min readSume
All posts

A run webhook has exactly one terminal event for each run family, and the event name never carries the outcome. Use action.run.terminal for Action runs, format.run.terminal for Format runs and agent.run.terminal for Agent Completions. Route on event, then read status and outcome to learn what happened. A service that embeds only Formats can match event === "format.run.terminal" and ignore every other delivery.

The three events

Surface`event`Receipt `object`
Action runsaction.run.terminalaction.run
Format runsformat.run.terminalformat.run
Agent Completionsagent.run.terminalagent.run

Generation-job webhooks are a different surface. Their events start with job., such as job.completed, and have their own event set. The signature scheme is the same, so one verifier covers both, but your router needs a separate branch for the job.* names.

Branch on outcome, not on the name

On a finished run, status is OK or ERROR, and outcome refines it. ok means the run completed with output. degraded means the run completed and was billed, with real media in artifacts[], but output is null because the projection did not match your schema, and output_error gives the cause. error means the run did not complete. Cancel is not in this list: a canceled run never delivers a webhook.

`outcome``status`Usable media
okOKYes, output is filled
degradedOKYes, in artifacts[]; output is null
errorERRORNo. See error.code

A small router

The function maps the event to a family and reports whether media is usable. It performs no verification, so call your signature check first, on the raw body. It returns unknown for events it does not know, which lets you acknowledge and ignore new event names without failing the delivery.

// One terminal event per run family. The outcome lives in status and outcome.
const FAMILIES = {
  "action.run.terminal": "action",
  "format.run.terminal": "format",
  "agent.run.terminal": "agent",
};

export function route(envelope) {
  const family = FAMILIES[envelope.event];
  if (!family) return { family: "unknown" };
  const usable = envelope.outcome === "ok" || envelope.outcome === "degraded";
  return { family, runId: envelope.run_id, usable, partial: envelope.outcome === "degraded" };
}

console.log(route({ event: "format.run.terminal", run_id: "arun_demo", status: "OK", outcome: "degraded" }));
console.log(route({ event: "job.completed" }));

Handler order

Do the work in this order. Read the raw body and verify the signature, rejecting timestamps outside the five-minute window. Parse the JSON. Look up run_id in your own table to skip duplicates. Route on event. Then branch on outcome. Acknowledge quickly: each attempt has a 10 second timeout, and ten failed attempts end in failed or exhausted on the receipt's webhook_delivery. Do the slow work, such as downloading media, after you have answered.

Other fields to rely on

  • run_id and request_id are equal and stable across retries. Use either as your dedupe key.
  • created_at is when Sume built this delivery body, so use it, not request_id, to order deliveries.
  • payload is byte-identical to data from the receipt route, so one parser can serve a webhook and a poll.
  • payload is null when the receipt was over 1 MiB. Then error.code is payload_too_large and error.result_url is where to fetch it. status still shows the real outcome.
  • A continued run is a new run with a new id, and it delivers its own single terminal event. The original run's webhook does not fire again.

One event per run, not per clip

A run is one agent turn, so its terminal event fires once, whatever the number of clips or images it produced. There is no per-artifact event. For progress inside a turn, use the generation-job layer, which fires one event per job on completion. See Run webhooks for the payload and Webhooks for the job events.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume