7.1 KiB
Shared Variables Context
Your container is running with the shared variables filesystem mounted at /variables/.
This is the only way you read a step's inputs and return its outputs to the control
plane — there is no API to call, no credential to hold, no env var to substitute.
You write plain files under your own step folder; the CP picks them up.
Normative source: spec/workflow-variables.md (WV-1..6a). This document summarises
the contract; when in doubt, the spec wins.
What is where
/variables/
workflow/ # run inputs, frozen at dispatch time (WV-8)
run-id
template
item.uuid
item.title
item.label
item.flow-state
artifacts.<key> # per WorkflowInput.required_artifacts
<step>/ # this step's OWN folder — write outputs here
<loop-id>_<n>/<step>/ # loop body node in iteration n (n is 1-based)
.self # path to your own step folder (read-only)
.done # completion marker, written by the entrypoint
You only write under your OWN folder. The pack sidecar walks that folder after you
exit and returns its contents to the CP. Earlier loop iterations' results and your
direct depends_on dependencies' outputs are also under /variables/, pre-filled
when your container started — you only READ those.
Two env vars are set for you
WORKFLOW_VARIABLES_ROOT— always/variables.WORKFLOW_VARIABLES_SELF— the path to your own step folder (e.g./variables/judgefor a top-level node namedjudge, or/variables/review-loop_2/judgefor loop iteration 2'sjudgenode).
Neither ever contains a value. Both are paths only. Treat both as read-only inputs.
Key naming (the contract you'll be checked against)
- A variable's key is the joined, lowercased path from
/variables/to the leaf file, with.as the separator./variables/judge/VERDICT↔judge.verdict./variables/review-loop_2/judge/findings/HIGH↔review-loop_2.judge.findings.high. - Leaf files are UPPERCASE on disk by convention; folders stay lowercase. Lookup via the helpers and the CP API is case-insensitive.
- Each path segment, after lowercasing, must match
^[a-z0-9][a-z0-9_-]{0,63}$. Maximum depth is 8 segments. Dotfiles, symlinks, FIFOs, sockets, and devices are skipped with a warning (never read or followed). - Two leaf files at the same level whose names lowercase to the same key → both
rejected at pack time as
invalid_output_key. - Reserved names that cannot be used as step or loop ids (case-insensitive):
workflow,while,for,foreach,loop,until,do,repeat,if,else,elif,elseif,then,switch,case,default,when,break,continue,return,goto,yield,try,catch,except,finally,raise,throw,and,or,not,xor,in,is,true,false,null,none,nil,yes,no,on,off. The full list lives incontrolplane/typed_workflows/reserved_ids.pyand may grow by spec amendment. - Step/loop ids are restricted to lowercase letters, digits,
-and_, starting with a letter; no trailing_<digits>(reserved for loop-iteration suffixes).
What you can see (visibility)
workflow.*— always. These are the run inputs, frozen for the life of the run (later item changes are not reflected here; pass them on through step outputs).- The outputs of each of your direct
depends_onnodes (the same set the manifest DAG wires you to) — not transitive. - Inside a loop body, every prior iteration's outputs for every step in the loop — not just the immediately preceding one. So iteration 2 sees what iterations 1, 2, … of every loop body node wrote. Iteration 1 sees only its direct dependencies.
- Nothing else. Your own folder is always created, even if empty.
Declared outputs
If your node's template declares outputs: [<key>, ...], those are the keys you
are expected to write. Any file actually present under your step folder is
accepted regardless — outputs: is for policy and tooling, not an allowlist.
A missing declared output produces a warning by default (configurable per-project
to fail the step with variables_missing_output).
Values
- Strings only. JSON is supported as a string value — you parse it yourself; the CP never interprets a value's structure.
- Per-value cap is configurable (
max_value_bytes, default 1 MiB). Per-step cap (max_step_bytes) and per-step file count (max_step_files) also apply. - Values are data — never substituted into prompts, commands, or environment variables by the CP. Don't put values in shell; read them through the helpers or the CLI.
Helpers (use these, not shell)
A Python package is pre-installed in your image at /opt/agent/variables/ and a
CLI at /usr/local/bin/wfvar. Both are stdlib-only.
from variables import load, save, keys
value = load("verdict") # load from your own folder
save("verdict", "CLEAN") # write into your own folder
keys() # list keys under your own folder
keys(prefix="judge.") # narrow by prefix (read-only access)
wfvar get VERDICT # print the value, NO trailing newline
wfvar set VERDICT CLEAN # write into your own folder
wfvar list # list your own keys
wfvar path # print your own step folder (same as $WORKFLOW_VARIABLES_SELF)
wfvar get judge.findings.high # read another step's output (visibility permitting)
Exit codes: 0 ok, 1 not found (key is valid but absent), 2 invalid key or
usage, 3 value exceeds per-value limit, 4 write attempted outside your own
step. Do not wrap wfvar get with extra shell that strips newlines — it prints
exactly the stored bytes.
Don't write plan-like content with wfvar set <key> <body> — argv breaks on
shell quoting and truncates at ARG_MAX. For multi-line / quoting-sensitive bodies
(e.g. the decompose plan files at /variables/<node>/items/<local_id>), use your
file-write tool or a heredoc with a terminator that cannot occur in the body,
writing directly into /variables/<node>/....
Safety
The pack sidecar reads your step folder with strict traversal protections
(O_NOFOLLOW, S_ISREG checks, size caps, 4 KiB marker cap). It does not follow
symlinks you might drop in. A symlink at any level is silently skipped with a
warning — never assume it will be read.
What you must NOT do
- Do not read or write outside your own step folder under
/variables/. - Do not call out to the CP API directly — the helpers and the pack sidecar handle all variable transport.
- Do not assume
/variables/exists in an unstamped container — it doesn't. - Do not write to
/variables/.done— the entrypoint owns that marker; it is written for you afterrun_finalize_phasereturns, whether you exited 0, non-zero, or via SIGTERM. - Do not store values in env vars, command lines, or prompts — values are data, and data doesn't go there.