decree graph writes Markdown documents holding Mermaid stateDiagram-v2 diagrams, generated from the same arena the interpreter runs, so the picture can never drift from the behaviour. Saved as a .md file it renders as it is in VS Code, GitHub, GitLab and Obsidian. decree renders no images, so it stays a small single binary. Output is byte-for-byte deterministic.
decree graph writes one Markdown file per machine, .decree/graph/<machine name>.md:
# <machine name>
<machine description>
Machine: [machines/<machine name>.yml](../machines/<machine name>.yml)
```mermaid
<diagram>
```and .decree/graph/system.md, whose heading is # All machines, whose line under it is Every machine, the `emits` and `invokes` edges between them, and cron entry points., followed by one line per machine in name order, - [<name>](<name>.md): <description>, a blank line, and the <diagram> block. Every file ends with one newline after the closing fence. Files are deterministic, so committing them shows real changes only. The sections below define <diagram>.
Linking back. YAML has no link type, so a machine links to its graph with a comment, which decree ignores: # Graph: ../graph/<machine name>.md, on the line after the $schema comment (Schema). decree init writes both into the machines it creates.
For each container (the root, then each compound state recursively), with 4-space indent per level:
[*] --> <initial>- For each compound child, in name order:
state <name> {, the same 4 steps for its contents, then}. - Every transition whose transition domain is this container, sorted by (source state name, event name). Transitions declared on compound states are included, drawn from the compound state. Format:
<source> --> <declared target>: <label>. - For each final child:
<name> --> [*].
After the root's last line, emit notes, also at root level:
- For the root:
note left of <root initial>with linesmachine onentry: <scripts>,machine onexit: <scripts>andstore: <names>(thestore:names in name order, Store), thenend note. Omit empty lines and omit the note if all three are empty. - For each state with
onentry,onexitor a decision invoke, in name order:note right of <state>with, in this order and only when present,check: <condition>(as<subject> <name> <op> <value>, e.g.visits implement less_than data.max_rounds,data file matches '<regex>',confidence big_model at_least 0.4, oroutput read_text matches '<regex>'),model: <router machine>plus, min_confidence <n>,machine: <name>,person: <ask script>,onentry: <scripts>andonexit: <scripts>, thenend note.
Labels are the event name plus these suffixes, in this order:
(check)if the source state invokes acheck.(model)if it invokes amodel, or(model: <router>)when the invoke names a router. Not onerror.(machine: <name>)if it invokes a machine. Not onerror.(person)if it invokes aperson. Not onerror.(internal)for atype: internaltransition.(implicit)on unhandlederroredges tofailed. Draw one from every non-final atomic state that invokes a script, a machine, amodelor aperson, or has anonentry, when neither it nor any ancestor has a transition matchingerror.
Escape < as #lt; and > as #gt; in labels (Mermaid entity codes).
This is the <diagram> for the deploy machine: the fenced block in examples/project/.decree/graph/deploy.md.
stateDiagram-v2
[*] --> build
approval --> ship: approve (person)
approval --> failed: error (implicit)
approval --> rejected: reject (person)
build --> approval: done
build --> failed: error (implicit)
ship --> done: done
ship --> failed: error (implicit)
done --> [*]
failed --> [*]
rejected --> [*]
note right of approval
person: ask_person
end note
This is the <diagram> for a machine whose root holds the work state of a compound state, then done and failed. The compound state's contents come first, inside state work { … }; the transitions it declares, and the implicit error edges of its children, whose domain is the root, follow it.
stateDiagram-v2
[*] --> work
state work {
[*] --> implement
implement --> verify: done
verify --> implement: fail
verify --> verified: pass
verified --> [*]
}
implement --> failed: error (implicit)
verify --> failed: error (implicit)
work --> done: done.state.work
done --> [*]
failed --> [*]
note right of implement
onentry: snapshot
onexit: collect_logs
end note
The system graph shows how machines connect, one box per machine; each machine's own graph shows its states. It is a Mermaid flowchart LR, not a state diagram, because machines are not states of one machine. Flowchart nodes also accept Mermaid click directives, which a UI built on decree can add.
flowchart LR
<machine>["<machine>"] one per machine, in name order
cron__<id>[/"cron: <stem>"/] one per cron file, in filename order
cron__<id> -->|cron| <machine> one per cron file, in filename order
<machine> -->|emits| <target machine> one per distinct (machine, target) pair from `emits`, sorted
<machine> -->|invokes| <child machine> one per distinct pair from `machine` invokes and routers (named or default), sorted
<id> is the cron file's stem with every character outside [a-z0-9_] replaced by _; every cron file has machine: (M3). Indent 4 spaces.
decree graph --help and the README give the same instructions:
- Run
decree graph, then open.decree/graph/<machine>.md(orsystem.md). - In VS Code, press
Ctrl+Shift+V(Cmd+Shift+Von macOS) for the preview; VS Code 1.121 and later render Mermaid in Markdown without an extension. GitHub, GitLab and Obsidian render the committed files as they are. - Without any of those, copy the lines inside the
mermaidfence into https://mermaid.live.
Mermaid and its live editor are MIT-licensed, so a team can host its own editor.
Stable ids, for tools built on top. Node ids in the output are the state ids (single machine) or the machine names and cron__<id> (system graph), and events.jsonl carries machine, state and run_id. A UI outside decree can therefore link any node in a rendered graph to that state's logs (for example a Grafana Explore query on {job="decree", machine="develop"} | json | state="implement"). Building such a UI is outside decree; these ids and fields are the contract it relies on.
DOT, SCXML (XML) and image output are out of scope; the decision log records why.