JH

jacobhausler/hermes-workflows

Developer tools
1 stars Quality 61 Trend 25MIT

Agent-owned workflow graphs for Hermes Agent: nodes, fan-outs, gates, fingerprint-resume, desktop DAG view

Overview

Your agent authors a JSON graph of agent nodes, fan-outs and gates; a background runner executes it outside the caller process tree and hands results back through the same workflow tool it launched from. Hermes Desktop draws the live DAG. A single agent turn is one long, fragile thread of work: one context, one timeout, no resumable checkpoints. A splits the job into nodes with explicit dependencies — parallel recon lanes, a synthesis join, an adversarial review gate — each node a separate child session with its own model, budget, and typed failure. When something dies or you change your mind mid-flight, only the affected nodes re-run: completed work is replayed from its stored fingerprint, not re-paid. One tool, action-routed — the complete surface, with the flags that matter: Plus a /wf slash command: bare /wf lists the library; /wf [note] launches that graph with the note as its context seed.

README

Hermes Workflows

Agent-owned workflow graphs for Hermes Agent. Your agent authors a JSON graph of agent nodes, fan-outs and gates; a background runner executes it outside the caller process tree and hands results back through the same workflow tool it launched from. Hermes Desktop draws the live DAG.

Why this exists

A single agent turn is one long, fragile thread of work: one context, one timeout, no resumable checkpoints. A graph splits the job into nodes with explicit dependencies — parallel recon lanes, a synthesis join, an adversarial review gate — each node a separate child session with its own model, budget, and typed failure. When something dies or you change your mind mid-flight, only the affected nodes re-run: completed work is replayed from its stored fingerprint, not re-paid.

What you get

Capability In one line
Graph runs Agent nodes with goal, after edges, per-node model/provider/reasoning, and max_turns/timeout/run_budget caps; optional per-graph concurrency/item_concurrency, clamped to owner caps
Fan-out One node → N live children from fanout.items or items_from:".items"; per-item liveness; quorum cancels stragglers once N commit (they are cancelled, never a failure)
Human + machine gates A gate holds on a question until release; gate.wait holds on a timer or an argv probe at zero token cost; when predicates branch on upstream output; on_skip:"prune" retires the losing arm
Fingerprint resume Crash, restart, or amend the graph mid-flight — nodes whose effective fingerprint still matches replay-skip; only what actually changed re-runs. run/amend accept dry_run:true for a no-write preview
Cooperative steer steer queues text for a running node; the child pulls it at its next natural seam via the tool’s inbox action — prompts are never rewritten mid-flight
Profile delegation A node can run as another local Hermes profile (its instructions, memory, tools) behind a per-profile consent file — multi-personality graphs on one machine, no isolation claimed
Typed failures Every node.failed carries error_class + attempts from a closed set (timeout, cap_exhausted, provider_400, schema, precondition, cancelled, fatal_quota, route_unavailable, …) — you never infer a cause from prose; failed nodes ship node_facts (class, attempts log, final words, log path)
Route integrity A node that pins a model is fail-closed (require_route, on by default): a proven-dead pin refuses to launch rather than silently bill another model; with an owner-configured confidence_substrate ladder (#116) the substitution is engine-stamped and disclosed in the node’s result contract — never silent
Process-tree honesty An exit-0 child is believed only when its whole process tree is dead; a backgrounded worker is typed, never mistaken for done, and a fenced answer over a live tree commits partial with proof
Compact status Mid-run status/wait return output pointers and per-node metrics (missing evidence reads unknown, never a false zero); detail:"full" opts into everything
Wedged-lock recovery The runner is admitted by a kernel flock on runner.lock held for the process’s whole life — a dead holder’s lock is released by the kernel itself, the next spawn contends cleanly, and there is no stale lock to clear by hand
Desktop DAG view Live graph, fan-out stacks, timeline, and a ::workflow{id="…"} inline card in any reply; RUNNING / THE REST agent-first panes (every row carries its originating agent), and a session strip under the composer showing this chat’s runs (core ≥ v2026.7.30; older shells get the fallback slot)
Library save a proven graph (description + tags — flat or facet:value, e.g. use_case:code-review), library lists it richly and filters by tags (with a tag_vocab echo so agents reuse the live taxonomy), run from:"" replays it; optional study-only submit takes a why_not_library receipt and is quarantined for study, never auto-saved; inbox kind:"submissions" lists these submissions. Ordinary one-off run executes without lookup or curation
Composite graphs A graph-level include expands shelved library graphs into a run at launch — namespace-isolated ids, cycle/depth/size guards, model policy unions in and never relaxes (shipped in v1.3.0)
Authoring skill Bundled workflow skill: grammar, operations, and measured per-shape budget presets (recon/build/review/publish)

Offline checkout drift probe

Run python3 scripts/drift_check.py --repo /path/to/checkout on demand (the script’s checkout is the default). No fetch, network refresh, daemon, or cron. upstream_latest means the nearest reachable local release tag selected by git describe --tags, not proof of the remote’s newest release. The final stdout line reports installed_version, upstream_latest, local_only_commits, trailers_missing, and verdict; unknown error counts stay unknown.

Exit 0 means clean; exit 1 means drift (version_mismatch, trailers_missing, then local_only, in that precedence). Commits after the tag need (est-…) or #NN tracker references in their messages. Exit 2 means error, including an absent installed-version changelog heading, missing tag, or unreadable git state. The probe only reads plugin.yaml, CHANGELOG.md, and git describe/rev-list/log.

The workflow tool

One tool, action-routed — the complete surface, with the flags that matter:

Action What it does
run Launch a graph from graph (inline), graph_path (≤1 MiB local file), or from (library name). Optional name, run_context (seed string, or a binding map replacing {run.KEY} refs — malformed input is refused before anything is written; values land in prompts, so no secrets), team, lane_key (dedupe: a second run on an unfinished incumbent returns it instead of spawning), dry_run:true (full validation, zero writes).
status Read-only read model: per-node state, metrics, held gate, node_facts on failures, derived next steps — never spawns a runner. lane_key reads the incumbent instead; a lane entry whose claimed run dir no longer exists on disk reports state: "orphaned" (a claimed-then-lost run, not a forever-pending ghost).
wait The resume-and-watch verb: the only read action that respawns an idle runner; blocks to the next boundary (default 600 s, ceiling 1800 s), self-yielding before the host’s tool deadline with a “call wait again” note.
release Answer a held human gate (gate_id, answer); respawns the runner when idle. A human release pre-empts a machine wait park.
release_lock Operator escape hatch for a wedged runner.lock: renames the stranded inode aside under the admission heal-barrier and hands the lane a fresh lock path. REFUSES unless zero live holders are proven (kernel ledger + dead lease) — never heals on EACCES or a live holder.
steer Queue steering text for a running node (refused on gates and terminal nodes; delivery is cooperative via the child’s inbox pull — never a mid-prompt injection). cmd:"kill" (with node) is the lane-kill form, identical to kill; it never queues text.
inbox Child-side pull of steering lines baked for this spawn — exactly-once per spawn. Parent-side kind:"submissions" lists submit items newest-first.
amend Replace the graph mid-run with the whole new graph (dry_run:true previews will_rerun without writing). Fingerprint-valid unchanged nodes replay-skip.
diff Confirm an amend before applying it: the amend door with dry_run FORCED — accepts patch or a full graph and returns the preview plus a unified graph_diff; touches nothing.
stop Request a stop at the next boundary; in-flight children are killed; stop ≠ failure — cancelled work re-drives on resume, and a lane_key is freed for the next dispatch.
kill Node-targeted lane kill (run_id + node): SIGKILLs that node’s in-flight children and finishes the node failed with error_class=killed. Siblings and the run keep going (no stop.request). The kill marker stays for the run — a killed lane stays killed.
list All runs (capped page + full-census counts, per-run provenance rollup when present).
save Shelve a graph in the library under a name (overwrite = current best): description, tags (1–10, flat or facet:value; reuse library’s tag_vocab verbatim), optional source attribution writes provenance (owner, digest, timestamp) — attribution, never access control.
submit Quarantine a hand-rolled graph the library didn’t cover for human-gated study — requires a why_not_library receipt (≥80 chars); never joins the library.
library List shelved graphs richly (nodes, gates, fan-outs, description, tags, provenance), filterable by ALL-match tags; an empty filtered result says which tag starved.
validate Dry-run the door’s validation pipeline (defaults fill, defect collection, model/route policy) with no ping and no writes — {ok, errors:[{node,field,msg}], resolved_routes}.
doctor_version Read-only version truth for THIS install: {live_version, newest_packaged, source_commit, drift} — plugin.yaml vs the install.json provenance pack.py stamps at build time; one read, no network.

Plus a /wf slash command: bare /wf lists the library; /wf [note] launches that graph with the note as its context seed. /wf show (or workflow {"action":"library","name":""}) returns required inputs and an instantiate command. Library-sourced launches require nonempty string bindings for every {run.KEY} on the shared text surface, including echo output, gate options and machine argv; surviving refs refuse before any write. Inline graphs retain their existing behavior, including refusing unbound machine argv.

Optional save params:{KEY:{desc,default}} stores descriptive meta.params in the envelope. Required keys derive from graph refs, not this declaration; defaults are suggestions in the instantiate command, never automatic launch bindings. Omission retains params on overwrite; {} clears them deliberately. Missing or dead declarations on an envelope save produce non-fatal s12 save warnings naming the keys; a bare save’s contract derives wholly from refs. Bare, ref-free library rows and clean save responses keep their previous key sets.

Graph grammar in 30 seconds

Four node types — agent, gate, echo, join — and a closed key set per type (anything outside it is refused at submit with a per-defect error list, never silently ignored). The smallest useful graph:

{ "name": "check",
  "nodes": [{ "id": "inspect", "type": "agent",
              "goal": "Inspect the target. Return one fenced JSON object." }] }
workflow { "action": "run",  "graph":  }   → run_id
workflow { "action": "wait", "run_id": "" }          → repeat until terminal

Fan-out with quorum, gates with when/on_skip/wait, requires output preconditions, after_partial harvest release, inputs selection from upstream output, string-field enums, answer-schema floors minItems (on type:"array") and minLength (on type:"string", counted on stripped chars — a whitespace-only string is too short), graph-level defaults (precedence: explicit node key > shape preset > defaults) and model_policy — the authoritative vocabulary is references/grammar.md; the bundled authoring skill keeps a compressed working copy in SKILL.md. Runnable templates in examples/, grouped by role — basics/ (one idea each), build/ (fan-out and ledgers), review/ (independent judgment), release/ (issue→PR, release lifecycle, gates and watchers), ops/ (incidents). examples/README.md is the map: one line per template, what it teaches, and which arm its receipts proved.

What a run leaves behind

Each run is a directory (~/.hermes/workflows// by default): the committed graph.json, an append-only events.jsonl typed event log, per-node results under nodes/, prompts and logs under logs/, gate answers under gates/, and a summary.md. Resume, amend, and the desktop all read the same shared state. There is no hidden control plane and no daemon: exactly one runner process per run, admitted by a kernel lock, spawned outside the caller’s process tree. Run states (running, held, interrupted, done, failed, stopped), owner wake semantics, silent-runner reaping, and recovery procedures: references/operations.md.

Install

hermes plugins install hermes-workflows      # from the catalog (pinned SHA)
hermes plugins enable hermes-workflows

Restart the backend (hermes serve) so the tool and dashboard routes mount. Before disabling or removing the plugin, stop live runs: use workflow {"action":"list"} to find their run IDs, then workflow {"action":"stop","run_id":""} for each. Detached runners can outlive a session, gateway restart, or plugin disable until a boundary; disabling alone does not stop them. Desktop gate answers use the SDK to send a visible resume turn to the run owner’s chat; older Desktop builds insert text for you to send, or ask you to type workflow wait in that owner chat. If Hermes Desktop runs on a different machine than the backend, copy desktop/plugin.js to that machine’s ~/.hermes/desktop-plugins/hermes-workflows/plugin.js — the app hot-loads it. Manual/zip install and removal: INSTALL.md.

Two builds, one codebase

The plugin needs no patched Hermes. Every child spawn rides the stock quiet one-shot CLI contract, and every install runs the same code. Exactly one field differs, and it is optional:

stock Hermes (catalog install) with the optional core patch
graphs, fan-out, gates, steer, resume, desktop view, cards ✓ ✓
a child that dies on its max_turns cap error_class: unknown + preserved partial/log typed error_class: cap_exhausted + reason
how nothing to do one sha-stamped patch — docs/patched-core.md

The tier is self-reported: after a failed child, status shows turn_report: typed or untyped, so you always know which build you’re on. When upstream PR #121041 lands, the branch collapses and that doc deletes itself.

Requirements

  • Hermes Agent ≥ v2026.9.21 (package version 0.21.4) — measured stock -Q CLI and quiet turn-report contract; see docs/catalog/pr-body.md. Older 0.21.3 deployments are below this declared floor and will skip plugin admission.
  • Python 3 (stdlib only — the plugin imports nothing outside Hermes)
  • Node for the desktop half’s tests only; the app loads plugin.js uncompiled

Starter set: run something today (general/)

You don’t have to author a graph first. Five scrubbed, estate-neutral starter graphs ship with the plugin under library/general/, so a fresh install can run a real DAG out of the box:

{ "task": {"from": "general/report-and-verify"} }
Address Reach for it when
general/pr-review-light an open PR needs an advisory second read before a human looks
general/issue-triage a queue needs a standing rule applied unattended (the {run.KEY} seed-contract exemplar)
general/test-first-feature-slice one behavior is worth a red-green-proof slice
general/fan-out-audit-census a tree needs a per-file audit with an honest tally (the 8 KB per-parent input cap, taught by example)
general/report-and-verify a producer will drop a report later and you want zero tokens while waiting (machine wait gate exemplar)

Resolution order for from: general/: your runs-root library first (/library/general/.json), legacy launch root second, the packaged set last. Every graph routes agents through model tiers (worker/manager) — none names a vendor model — and each declares its seed params in meta.params.

Customize: copy a packaged JSON to your own shelf (or tune it and save name=general/); your copy shadows the packaged one at the same address, and the door never writes the packaged files. Share back: tuned graphs are welcome upstream — see the submit flow in issue #50 (“share your tuned graph back”): fork, drop the envelope under library/general/, run the scrub gate, open a PR. Per-graph author notes live in library/general/README.md.

For agents and contributors

AGENTS.md is the front door: repo map, operate/contribute procedures, and the rules that keep the tree publishable. Changes are gated by the serial suite (python3 scripts/suite.py . ci-out) and hermes plugins validate . — both run in CI.

graphify is an OPTIONAL local navigation aid — graphify query "" beats a grep dump. Its output is never committed and CI never builds it; source and tests are the spec.

License

MIT — see LICENSE.

View this README on GitHub

Recommended Tools

Try a different keyword or remove a filter.

Install

npx skillfish add jacobhausler/hermes-workflows