Node fields in detail
agent
prompt is required. profile names a host bot; model overrides that bot’s model — there is no provider field, because the runtime derives it from the host model catalog. retries is 0–3 and defaults to 0. Only agent steps retry: re-asking a human gate is not a retry, and a condition is deterministic.
condition
An expression has the form out.<node>.<field> <op> <value>. Operators are ==, !=, >, >=, <, <=. Anything not starting with out. is rejected at design time, so a typo fails in the canvas instead of mid-run. Parsing and comparison failures are step errors, never a silent false.
Node ids may contain dots. Both the validator and the runner resolve a reference by matching the longest known id prefix, so out.a.b.words > 2 resolves the node a.b and the path words rather than the node a. When the author compared against a number and the left side is numeric text, the text is coerced for the comparison; a quoted literal keeps it textual.
approval
question is required; options is the list of acceptable answers. Optional timeout_minutes (1–4320) is a deadline and on_timeout is reject or escalate. Once the deadline passes the run is failed with approval timed out, and a late answer gets that verdict, so a gate never waits forever. escalate records escalation requested and fails the same way — pretending to route to another reviewer would be a dead control.
output
Collects its parents’ outputs. Optional deliver is notification (a platform’s home channel) or channel (a specific chat), with a target such as telegram, telegram:123456 or slack:#ops. Leave both off to only collect. Never claim a delivery happened — read deliveries in the run result.
note
{id, type: "note", text, x, y, color} is a sticky label for the person reading the canvas. It has no ports, takes no edges, is dropped by the converter like a trigger, and never reaches the runner. Use it to annotate; never route work through it.
Limits
| Limit | Value | Why |
|---|---|---|
| Nodes per design | 50 | Keeps a canvas readable and a run bounded |
| Agent retries | 0–3 | A step failing four times is a design defect |
| Approval deadline | 1–4320 minutes | Three days; longer is a design error |
| Trigger modes | manual, schedule | A webhook is not a plugin-reachable mode |
Webhook mode is absent on purpose. Hermes inbound webhooks are operator-managed subscriptions with an HMAC secret that fire an existing cron job; a plugin cannot open a route of its own. A picker recording a URL nobody listens on would be a dead control.
Three canvases, one contract
The editor exists three times: the web dashboard tab (dashboard/dist/index.js), the Desktop page (desktop/plugin.js), and a dependency-free standalone file (widgets/designer.html) used by chat previews and manual checks. All three build the same payloads, and the tests drive every surface with the exact JSON the GUI produces.
widgets/designer.html is a single static file with no dependencies. Each node has an output port on its right and an input port on its left (a trigger has no input); drag from an output port to another node to add an edge, click an edge and press Delete to cut it.
The standalone canvas carries a fallback for every host theme variable it paints with, so it still draws legible nodes when it is opened outside Hermes, with no stylesheet to inherit from.
Schedules are real cron jobs
Nothing in the plugin runs on a timer. A recurring run is a host cron job named workflow:<design>, created through cronjob_manage — the same call hermes cron makes.
| Preset | Cron output |
|---|---|
hourly | <minute> * * * * |
daily | <minute> <hour> * * * |
weekdays | <minute> <hour> * * 1-5 |
weekly | <minute> <hour> * * <dow> |
custom | The trimmed expression, validated verbatim |
Times are 24-hour HH:MM. Validation requires exactly five fields and supports *, a-b, lists and */n. A human line is rendered when possible (“Daily at 09:00”), and anything it cannot phrase falls back to Cron <expression> rather than guessing.
A cron job has a schedule and a self-contained prompt; it has no field for “tool name plus structured arguments”. So the job’s prompt instructs a fresh session to call workflow_design_run with the design name, perform any awaiting_agent step and respond, report an approval question without answering it, and print the final outputs verbatim. The design name is embedded with json.dumps, so a quote or newline in it cannot break the prompt or smuggle instructions into the session.
Ask before registering anything recurring. A scheduled replay opens a fresh session and answers agent steps itself, but it never answers a human gate — that waits for a person until timeout_minutes. Inbound HTTP is not a mode; point people at hermes webhook subscribe, which can fire that cron job.