Skip to content

Variable Reference

Archon substitutes variables in command files, inline prompts, bash scripts, and script: node bodies before execution. There are two categories of variables: workflow variables (substituted by the workflow engine) and node output references (DAG workflows only).

These variables are substituted by the workflow executor in all node types (command:, prompt:, bash:, script:, loop:, loop_group:, and a workflow: node’s input: field — which behaves like a prompt: body, not a bash-escaped one).

VariableResolves toNotes
$ARGUMENTSThe user’s input message that triggered the workflowPrimary way to pass user input to commands
$USER_MESSAGESame as $ARGUMENTSAlias
$WORKFLOW_IDUnique ID for the current workflow runUseful for artifact naming and log correlation
$ARTIFACTS_DIRPre-created external artifacts directory (~/.archon/workspaces/<owner>/<repo>/artifacts/runs/<id>/)Always exists before node execution; stored outside the repo to avoid polluting the working tree. Container runs (--container): this host path is not mounted into the container, so a node that writes directly to $ARTIFACTS_DIR from inside the container will fail — write to the workspace instead. Engine-written typed-output sidecars still work (they are written on the host from captured stdout).
$STATE_DIRPre-created external cross-run state directory (~/.archon/workspaces/<project>/state/)Scoped per project — shared across every workflow, every conversation, and every invocation surface, so cooperating workflows can share memory. Namespace inside it yourself ($STATE_DIR/<name>/) if you want isolation. Survives worktree teardown, and never appears in git status. Throws if referenced but unresolved, exactly like $BASE_BRANCH. Container runs (--container): same caveat as $ARTIFACTS_DIR — the host path is not mounted into the container, so a node writing there from inside the container writes to the container’s ephemeral layer.
$BASE_BRANCHBase branch for git operationsResolved in order: the --base <branch> flag on archon workflow run (per dispatch), then worktree.baseBranch in .archon/config.yaml, then the registered codebase’s stored default branch, then git auto-detection. --base sets the worktree cut-from too, so this variable always names the branch the worktree was actually cut from — unless --from was also passed, which overrides only the cut-from. See Base branch precedence. Throws an error if referenced in a prompt but cannot be resolved
$DOCS_DIRDocumentation directory pathConfigured via docs.path in .archon/config.yaml. Defaults to docs/ when not set. Never throws
$CONTEXTGitHub issue or PR context, if availablePopulated when the workflow is triggered from a GitHub issue/PR. Replaced with empty string when unavailable
$EXTERNAL_CONTEXTSame as $CONTEXTAlias
$ISSUE_CONTEXTSame as $CONTEXTAlias
$LOOP_USER_INPUTUser feedback from an interactive loop approval gateOnly populated on the first iteration of a resumed interactive loop. Empty string on all other iterations. On a signal-bearing gate, a bare approve (no feedback) finalizes the node without a new iteration, so the variable is never read
$REJECTION_REASONReviewer feedback from an approval node rejectionOnly available in on_reject prompts. Empty string elsewhere
$LOOP_PREV_OUTPUTCleaned output of the previous loop iteration (loop nodes only)Empty string on the first iteration. Useful for fresh_context: true loops that need to reference the prior pass without carrying the full session history
$LOOP_PREV.<nodeId>.outputA body node’s output from the previous iteration (loop_group body nodes only)Empty string on iteration 1. $LOOP_PREV.<nodeId>.output.<field> accesses structured-output fields with the same strict semantics as $nodeId.output.field. See Cross-Node Loops

The three context aliases ($CONTEXT, $EXTERNAL_CONTEXT, $ISSUE_CONTEXT) all resolve to the same value. When no issue context is available, they are replaced with an empty string to avoid sending the literal $CONTEXT text to the AI.

If issue context is present but no context variable appears in the prompt, the context is appended to the end of the prompt automatically. This prevents duplicate context when a command explicitly uses $CONTEXT.

Unlike other variables, $BASE_BRANCH will cause the workflow to fail immediately if:

  • The variable is referenced in a prompt, AND
  • worktree.baseBranch is not set in .archon/config.yaml, AND
  • The registered codebase has no stored default branch, AND
  • Auto-detection from git fails

If the variable is not referenced, no error occurs even if the base branch cannot be determined.

$STATE_DIR is the external home for state a workflow needs to remember between runs: a dedup ledger, a “last processed” cursor, a nudge log. It is created before the first node runs and lives at ~/.archon/workspaces/<project>/state/, a sibling of artifacts/ and logs/.

Two properties matter:

  • It is per project, not per workflow. Two cooperating workflows in one project see the same directory, which is what lets a pair of related workflows share one ledger. If you want isolation, namespace it yourself: $STATE_DIR/my-workflow/.
  • It is outside the repository and outside the worktree. State written here survives worktree teardown and can never be staged into git — which is exactly what the older .archon/state/ convention could not promise (inside an isolated run that path is the worktree, so it was deleted at cleanup).

Like $BASE_BRANCH, referencing $STATE_DIR where no state directory could be resolved throws rather than substituting an empty string.

If Archon finds a legacy <repo>/.archon/state/ directory when a run starts, it logs one warning with the exact mv command and moves nothing.

Concurrency. The engine does no locking on $STATE_DIR. See Authoring Workflows for the read-modify-write hazard and how to avoid it.

Name collisions. The project segment is derived from the project’s identity, and distinct projects can derive the same one — two no-remote local repos both called api, or two folder projects whose display names slugify identically. Artifacts and logs are keyed by run id, so a collision is harmless there. $STATE_DIR has no run-id segment, so colliding projects genuinely share their state files. If that matters, register one of them under a distinct name, or namespace inside $STATE_DIR.

Archon does not support positional arguments ($1, $2, $3, … $9). Command files and workflow prompts receive the user’s whole trigger message via $ARGUMENTS / $USER_MESSAGE only — there is no whitespace-splitting into numbered slots, in either direct command invocation or workflow nodes. If you need structured inputs, parse them out of $ARGUMENTS inside the command or prompt body.

In DAG workflows, nodes can reference the output of any completed upstream node. These are substituted after workflow variables.

PatternResolves toNotes
$nodeId.outputFull output string of the referenced nodeThe node must be a declared dependency (in depends_on)
$nodeId.output.fieldA specific JSON field from the node’s outputWorks on any JSON-object output; output_format adds stricter validation — see notes below

A .field reference fails the consuming node when the producer’s output is not a JSON object — whether or not the producer declared an output_format. Declaring a schema buys you a stricter check on the field name (an undeclared field fails the consuming node with a named error rather than resolving to a silent empty), and lets a declared-but-absent field resolve to ''; it never makes a broken producer quieter. This matters most for workflow: sub-run nodes, where output_format populates the accessible field names but is not validated against what the child actually returns.

During the current run, downstream interpolation and when: conditions see the full returned node output. Successful bash events retain only a 32 KiB UTF-8 audit preview, so after a process boundary a resumed run rehydrates that persisted preview rather than the full output. If a large gate verdict must survive a restart intact, store it through a deliberately managed artifact contract instead of relying on the event preview.

$nodeId.output values are auto shell-quoted when substituted into bash: scripts, so the value is always safe to embed in a shell command. For small outputs, values are single-quoted inline. For outputs exceeding 32 KB, Archon spills to a temp file and substitutes $(cat '/tmp/path') instead — the unquoted assignment form is correct in both cases. They are not shell-quoted when substituted into script: bodies — the raw value is embedded as-is. For script nodes, treat substituted values as untrusted input and parse them with language features (e.g. JSON.parse), not by interpolating into shell syntax.

User-controlled variables ($ARGUMENTS, $USER_MESSAGE, $LOOP_USER_INPUT, $LOOP_PREV_OUTPUT, $REJECTION_REASON, $CONTEXT and its aliases) are delivered to bash: and script: nodes as subprocess environment variables (ARGUMENTS, USER_MESSAGE, LOOP_USER_INPUT, LOOP_PREV_OUTPUT, REJECTION_REASON, CONTEXT/EXTERNAL_CONTEXT/ISSUE_CONTEXT), never spliced as raw text into executable code — so attacker-influenced input can’t inject. In bash: read them as "$ARGUMENTS"; in script: read them via process.env.ARGUMENTS (bun) or os.environ['ARGUMENTS'] (uv/python). A literal $ARGUMENTS/$USER_MESSAGE/$CONTEXT left in a script: body no longer resolves and logs a one-release migration warning.

Because bash: substitutions arrive pre-quoted, wrapping them in double quotes is a silent footgun for small (inline) values:

Terminal window
# WRONG — for a small value, $emit.output.status is injected as 'ok' (single-quoted),
# so status="$emit.output.status" becomes status="'ok'" — the quotes become data.
status="$emit.output.status"
[ "$status" = "ok" ] && echo pass # → silently fails ($status is 'ok', not ok)
# CORRECT — leave the substitution unquoted; Archon's quoting is the quoting.
status=$emit.output.status # → status='ok' → bash assigns: ok
[ "$status" = "ok" ] && echo pass # → passes

For large outputs (>32 KB) the substitution is $(cat '/path'), where var="$(cat ...)" is correct bash — but you can’t know the size at author time, so the rule is unconditional. Numeric and boolean fields are injected raw (no quotes), so double-quoting accidentally “works” for them — which makes the bug intermittent. Always use var=$node.output.field, never var="$node.output.field".

nodes:
- id: classify
command: classify-issue
output_format:
type: object
properties:
type: { type: string, enum: [BUG, FEATURE] }
required: [type]
- id: fix
prompt: |
The issue was classified as: $classify.output.type
Full classification: $classify.output
User's original request: $USER_MESSAGE
depends_on: [classify]

Variables are substituted in a defined order:

  1. Workflow variables$WORKFLOW_ID, $USER_MESSAGE, $ARGUMENTS, $ARTIFACTS_DIR, $STATE_DIR, $BASE_BRANCH, $DOCS_DIR, $LOOP_USER_INPUT, $REJECTION_REASON, $LOOP_PREV_OUTPUT
  2. Context variables$CONTEXT, $EXTERNAL_CONTEXT, $ISSUE_CONTEXT
  3. Node output references$nodeId.output, $nodeId.output.field

Inside a loop_group body, $LOOP_PREV.<nodeId>.output refs are resolved first (before $LOOP_USER_INPUT is spliced in, so user-provided text is never re-processed as a workflow ref), then the node’s normal substitution runs.

Positional arguments ($1 through $9) are not supported in any context — $ARGUMENTS / $USER_MESSAGE deliver the whole trigger message instead.

VariableWorkflow nodesDirect command invocationwhen: conditions
$ARGUMENTS / $USER_MESSAGEYesYes (both aliases)No
$WORKFLOW_IDYesNoNo
$ARTIFACTS_DIRYesNoNo
$STATE_DIRYesNoNo
$BASE_BRANCHYesNoNo
$DOCS_DIRYesNoNo
$CONTEXT / aliasesYesNoNo
$LOOP_USER_INPUTYes (loop nodes)NoNo
$REJECTION_REASONYes (on_reject only)NoNo
$LOOP_PREV_OUTPUTYes (loop nodes)NoNo
$LOOP_PREV.<nodeId>.outputYes (loop_group body nodes)NoNo
$nodeId.outputYes (DAG nodes)NoYes

These are standard environment variables read from process.env at clone time. They are not workflow-substituted variables — they must be set in your shell environment or .env file before Archon starts.

VariableDescription
GH_TOKENGitHub personal access token for authenticated clone operations
GITLAB_TOKENGitLab personal or project access token (glpat-*) for authenticated GitLab clones
GITEA_TOKENGitea API token for authenticated Gitea/Forgejo clones