universelabSCI · CLI · BIO
ESEspañol

Canopy

Template Variables

Last updated

Agent node prompts support these placeholders:

Variable Expands to
{{graph_name}} The graph’s name
{{workdir}} The graph’s working directory
{{spec_id}} The spec’s unique ID
{{spec_name}} The spec’s name
{{spec_content}} The spec’s description (falls back to name)
{{spec_code}} Engine-generated quick reference (e.g. FIX-7) — empty on legacy specs without a type
{{spec_type}} Conventional Commits type (feat, fix, etc.) — empty on legacy specs without a type
{{spec_scope}} Optional scope (e.g. tui) — empty when absent
{{spec_commit_message}} Prebuilt commit message (fix(tui): open F1) — empty on legacy specs without a type
{{node_id}} The node’s unique ID
{{previous_feedback}} JSON output of the previous node (truncated if oversized)
{{output:<node>}} The recorded output of another node in the same graph, by node name

Outputs are read from nodes, never from ensembles: {{output:<name>}} naming an ensemble is refused, and the message names the ensemble’s quorum node when one exists — that node is the source meant instead. Any other placeholder nothing can bind is refused when the template is written — graph_node add, graph_node update, graph_ensemble add and graph_ensemble update (the shared prompt and every member prompt_override), and graph_edit import (all-or-nothing) — naming the node or ensemble and the placeholder, plus the forms it accepts when the fix is a different marker, instead of failing member by member at run time. A marker meant as literal text stays accepted when escaped as \{{...}}. A prompt_preset is a file name, not text, so it keeps its spawn-time check. A router’s prompt_template binds only {{previous_feedback}}, {{previous_output}}, {{spec_content}} and {{spec_name}}: it reads no node output, so an {{output:…}} marker there is refused as well.

A template stored before this validation is reported by graph_control preflight in graph_warnings, one entry per offending placeholder naming the node or ensemble — advisory only, since the spawn-time refusal still applies. The exception is an unknown {{output:…}} target in a graph node’s prompt_template, which preflight has always refused as an error.

Check node commands

Check node command strings have their own marker vocabulary, separate from the agent-prompt table above — a check command cannot use {{spec_content}}, {{node_id}}, {{previous_feedback}} or {{output:…}}. Every marker a check command supports:

Marker Expands to Quoting
{{graph_name}} The graph’s name shell-quoted — safe for values with spaces or quotes
{{workdir}} The directory the command runs in (the graph’s workdir, or the run’s sandbox worktree / workdir_override when one applies) shell-quoted
{{spec_id}} The spec’s unique ID shell-quoted
{{spec_name}} The spec’s name (empty on an idea run) shell-quoted
{{spec_start_head}} The git HEAD commit at the start of the spec run, empty when the workdir is not a Git repository or on a legacy run raw — a hex SHA
{{spec_start_dirty}} The number of non-ignored entries reported by git status --porcelain at the spec boundary, empty when unknown raw — a number
{{spec_committed_head}} The git HEAD left behind by the last commit_rights: true node’s own execution during this spec run, empty until one actually commits (see Commit rights) raw — a hex SHA

The shell-quoted markers expand to a single POSIX-quoted shell word, so a name containing spaces or quotes can never break or inject into the command. Write them bare ({{spec_name}}) or already single-quoted ('{{spec_name}}' — the engine strips that exact wrapper so the quoting is not doubled), never inside other quotes; a \{{marker}} escape stays literal text. The raw markers expand unquoted exactly as before.

Any other {{…}} marker fails the check node before the command runs: the run records unknown marker {{x}} in check command and nothing reaches the shell. graph_control preflight reports the same defect as an error before spending any probe quota.

Every check command also runs with these environment variables (there is no CANOPY_WORKDIR — the command’s working directory already is the workdir):

Variable Value
CANOPY_SPEC_ID The spec’s unique ID
CANOPY_SPEC_NAME The spec’s name; the empty string on an idea run
CANOPY_GRAPH_ID The graph’s unique ID
CANOPY_GRAPH_NAME The graph’s name
CANOPY_SPEC_START_HEAD The same value as {{spec_start_head}}; the empty string when unknown
CANOPY_SPEC_FILE Absolute path of a file holding the spec’s full description — the idea text on an idea run, an empty file for a spec with no description. The engine writes it just before the command runs and removes it once the node finishes, so read it inside the command (quote the expansion: "$CANOPY_SPEC_FILE"). There is deliberately no {{spec_content}} marker in check commands: a file has no quoting problem and no command-length limit
CANOPY_SPEC_CODE Engine-generated quick reference (e.g. FIX-7) — empty on legacy specs without a type
CANOPY_SPEC_TYPE Conventional Commits type (feat, fix, etc.) — empty on legacy specs without a type
CANOPY_SPEC_SCOPE Optional scope (e.g. tui) — empty when absent
CANOPY_SPEC_COMMIT_MESSAGE Prebuilt commit message (fix(tui): open F1) — empty on legacy specs without a type

{{spec_start_head}} only proves some commit landed since the spec began; it’s satisfied just as well by a commit from outside this run sharing the same worktree (another agent, a human, a cherry-pick) as by this spec’s own work. A check node that exists to gate “did this spec’s work actually get committed” should use {{spec_committed_head}} instead:

{{spec_start_dirty}} is the number of non-ignored entries reported by git status --porcelain at the spec boundary. It is empty when the workdir is not a Git repository or when reading a legacy run, rather than being reported as clean.

spec_end_dirty/spec_end_dirty_paths (readable via graph get/ graph node_runs, not a template placeholder) record the same count and the first 20 paths at the moment the spec’s attempt actually ends — Completed, Failed, or Interrupted. A spec that ends Failed or Interrupted with a dirty tree folds a one-line note onto its own failure summary/blocker text and (for a plain Failed ending) onto the on_failed hook’s {{node}} value.

graph_control run and the autorun scheduler refuse to launch the next spec when the workdir is currently dirty and a different spec of the same graph — or a member of its active queue run — ended Failed or Interrupted with recorded dirty paths that overlap the ones dirty now. Records from other graphs, and from standalone backlog specs outside the active queue run, are ignored, so a stale row elsewhere on the workdir never blocks a spec resuming its own work. Completed specs never block, and neither do Skipped ones: a skipped spec never ran, so its stale record must not make a later spec’s own dirt look ambiguous. The comparison uses the (first 20) paths each spec recorded at its end, so a record naming files that are no longer dirty never blocks. A spec resuming interrupted work of its own proceeds when the dirty paths are exactly the ones its own latest run recorded. The warning names the spec whose last recorded run in this graph left the workdir dirty (graph_runs joined to spec_end_dirty > 0); when no run of this graph recorded dirt the warning says dirty before this graph ran and names no spec. A resuming spec is still refused while another spec of the same graph (or active queue run) left overlapping dirt on the workdir: the refusal names the blocking predecessor — its name (id when the name is empty), the graph it ran in, its end status and end time — and, when the next spec is resuming its own dirt, says so in the same line. Finish, reset or remove that blocking spec (or set allow_dirty_start) to proceed. When the launch proceeds, the warning says why — allow_dirty_start is set on the graph, or the next spec is resuming the dirty work it left itself — instead of telling you to set a flag. When it blocks, the message says not launched before the Set allow_dirty_start hint. Set allow_dirty_start: true via graph_edit update to downgrade a refusal to a warning. graph_control preflight separately reports a dirty workdir as a warning unconditionally, with no “previous spec” logic — call it before graph_control run to see this regardless of history. A queue run applies the same guard between its specs, not only at launch — see Spec queues.

test -z "$(git status --porcelain -- src/)" \
  && test -n "{{spec_committed_head}}" \
  && test "$(git rev-parse HEAD)" = "{{spec_committed_head}}"

This requires a node in the graph to declare commit_rights: true — without one, nothing ever populates {{spec_committed_head}} and the check above always fails. {{spec_start_head}} remains useful on its own for anything that only needs “did the tree change at all”, where a concurrent commit from outside this run isn’t a concern.