Running workflows and answering gates

How a Hermes Workflow run reports status, parks at an approval, retries a transient failure, honours a deadline, and resumes exactly where it stopped.

Run statuses

workflow_design_run returns ok: true with a status, paused runs included. A paused run is a successful call that tells the caller what to do next. ok is false only for a failed run or a refused request.

StatusMeaningWhat to do
doneFinishedInspect outputs, skipped (nodes gated off by a false condition) and deliveries
awaiting_approvalA human gate is parkedAsk the user pending.question, then workflow_design_respond with run_id, node_id and answer
awaiting_agentAn agent step has no performerPerform pending.prompt yourself, then respond with answer (or structured for JSON conditions can read)
failedThe run stopped with an errorRead error, including kind and hint when diagnosed; retry deliberately

Never guess a run_id or a gate id — use the ones in the response.

Approval gates

An approval parks the run with {node_id, question, options, context, waiting_since} and returns control. Resuming validates the answer before mutating state: the answer must be one of the offered options, so a rejected answer leaves the run resumable instead of half-advanced.

The cursor is re-derived from the paused node’s id, not trusted from disk, so editing a design between pause and resume cannot silently skip or repeat the wrong step.

Deadlines live on the pending element rather than the plan, so a run reloaded after a process restart still knows when its gate expires. Nothing sweeps paused runs in the background — reading a run is the moment its deadline is honoured.

Approvals are answered only through the host’s release path. A scheduled run never answers a human gate.

Retries and rate limits

An agent step retries up to its retries budget (0–3) when the spawn seam reports a failure. A failure diagnosed as rate limited gets one extra automatic attempt even past the budget, after a 15-second backoff: a delay is the one thing that clears a 429, and exactly one extra attempt means a second rate limit is reported instead of napping forever.

attempts records how many tries each node took, so a result can explain itself.

Some failures are not retried: a gate that expired says approval timed out, and a step that ran out of retries says what the bot returned. Both are reported, not swallowed.

Retrying a failed run

When the user wants a run to go again, the deliberate path is the offered one: the canvas Retry button, workflow_design_run with retry=<run_id>, or /workflow run retry <run_id>. It re-enters the run at the failed step and reuses the outputs that already succeeded.

  • A failed run can always be retried.
  • A running run can be retried only when an earlier retry stamped it and that stamp is at least ten minutes old — otherwise the retry may still be in flight in another process, and taking it over would run steps twice.
  • A parked gate is answered, not retried. A finished run is not retried.
  • Never splice a run_id into a fresh Run.

Reading a result honestly

Read the run; do not narrate it. Everything a report needs is in the result.

outputs
Per-node output, keyed by node id
skipped
Nodes a false condition gated off
attempts
How many tries each agent step took
deliveries
Each send attempt as {node_id, target, ok, detail} — check ok before saying a message was sent
error
{node_id, msg, kind?, hint?}. The optional pair is the diagnosed-failure label and its one-sentence fix; relay both

The Runs view in a canvas lists the same runs from disk, newest first, with a chip for a diagnosed kind and Retry for failed ones.

Invariants worth knowing

  • Five node kinds plus note, never a sixth. Edges are after; x/y are widget-only.
  • Nothing runs on a timer. Recurring runs are real Hermes cron jobs.
  • Host seams are optional and lazy. profiles, models, spawn, delivery and the scheduler import host modules inside calls and degrade to “absent”.
  • A failed delivery does not fail the run; it is recorded in deliveries.
  • Untrusted input is re-validated: chat and webhook text goes through the schema before a write, and store paths stay confined under the store root.