# 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. # per WorkflowInput.required_artifacts / # this step's OWN folder — write outputs here _// # 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/judge` for a top-level node named `judge`, or `/variables/review-loop_2/judge` for loop iteration 2's `judge` node). 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 in `controlplane/typed_workflows/reserved_ids.py` and may grow by spec amendment. - Step/loop ids are restricted to lowercase letters, digits, `-` and `_`, starting with a letter; no trailing `_` (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_on` nodes (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: [, ...]`, 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. ```python 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) ``` ```bash 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 `** — argv breaks on shell quoting and truncates at ARG_MAX. For multi-line / quoting-sensitive bodies (e.g. the decompose plan files at `/variables//items/`), use your file-write tool or a heredoc with a terminator that cannot occur in the body, writing directly into `/variables//...`. ## 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 after `run_finalize_phase` returns, 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.