Workflows become properly composable: a workflow can now declare the arguments it takes and the result it returns, ship as one self-contained folder, and be simulated end to end without contacting a provider. Alongside that, a workflow node's capabilities become what its YAML declares rather than whatever happens to be configured on the operator's machine — see Breaking before upgrading.
Breaking
- Claude workflow nodes no longer inherit ambient skills and MCP servers. A Claude node now sees exactly the skills and MCP servers its YAML declares, plus the native tools Archon injects for the run. User, project, and plugin MCP configuration on the operator's machine no longer reaches workflow nodes: a node that relied on an ambiently configured server must declare it with
mcp:, and one that relied on an ambient skill must list it underskills:. Declared skills are now also checked before spend — a skill that is installed but unreachable from the node's enabledsettingSourcesfails up front instead of silently going missing, while built-in and plugin-qualified (plugin:skill) names warn and are left for the SDK to resolve. Direct Claude chat,CLAUDE.md, and built-in, filesystem, and inline agents are unchanged. (#2535) - An included command body must resolve, and may only reference its own workflow.
command:andloop.commandfiles inside aninclude:block are now resolved, validated, namespaced, and input-bound during load-time composition, recursing through nestedloop_groupbodies. Two previously tolerated shapes now fail: a body that references a node id outside the included workflow (it could bind whatever the caller happened to have) is rejected, and a body that cannot be resolved or read fails a fresh execution instead of warning and being skipped. Declare an input and pass it withwith:instead of reaching outward. A paused loop still resumes from its persisted prompt snapshot even if its command file is later deleted. Included command bodies gained the other half of this trade: they can now use$INPUTS.<name>, which Phase 1 rejected outright. (#2534, #2532)
Added
- Workflow signatures —
inputs:,returns:, andwith:on sub-runs. A workflow can declare the named arguments it accepts and which node's output is its result, and aworkflow:sub-run node can bind those arguments by name withwith:instead of passing a single opaqueinput:string. Values arrive as$INPUTS.<name>in prompts and command bodies, and asINPUTS_<UPPER_SNAKE>inbash:/script:node environments. Missing required inputs and undeclared arguments are rejected before any worktree, clone, or provider cost, andreturns:gives a stable author-chosen result channel in place of guessing the terminal sink. Workflows withoutinputs:keep their existing passthrough behavior byte for byte. (#2523) archon workflow run <name> --dry-runsimulates a workflow without spending anything. An in-memory simulator walks the DAG — routing,when:conditions, strict$node.outputreferences, gates, and loops — and prints a deterministic human or--jsontrace. No provider is contacted and nothing is persisted: no run row, events, sessions, or worktrees. Node outputs can be stubbed, approval behavior chosen, and code nodes actually executed with--exec-code(they are stubbed by default). (#2530)- Packaged workflows — one folder holds a workflow and everything it uses.
.archon/workflows/<pack>/<workflow>/is discovered as a package in both repo and home scope, and barecommand:/script:references in that workflow's YAML bind to its owncommands/andscripts/directories instead of the shared trees. Copying a workflow to another repo is now copying one folder rather than a scavenger hunt across three. Packaged resources are embedded in binary builds, and the Web API's workflow GET/PUT/DELETE lifecycle preserves the layout. Flat and one-level grouped YAML layouts are unaffected. (#2528)
Changed
- Codex workflow nodes no longer advertise ambient skills automatically. Workflow commands and prompts now invoke installed Codex skills explicitly with
$skill-name, an emptyskills: []declaration is valid, and unsupported YAML fields warn honestly instead of implying support. Direct Codex chat is unchanged. This is a behavioral guard rather than filesystem isolation, and Codex MCP configuration remains additive to ambient servers. (#2498, #2533) - A malformed
settingSourcesentry can only narrow Claude's scope, never widen it. An unrecognized entry previously left the key unset, falling through to the permissive['project', 'user']default — sosettingSources: [projct], written to lock a node to project scope, silently granted full user-scope ambient access. Unrecognized entries are now dropped and logged, and workflow validation shares one parser with execution so the two can no longer disagree about a node's effective sources. (#2535) - Agent instructions live in one file.
AGENTS.md— which Codex, Pi, and OpenCode read natively — had drifted about six months behindCLAUDE.md. It is now the canonical instruction file, withCLAUDE.mdreduced to a one-line pointer that Claude Code resolves as an import. No rule changed in the move. (#2497)
Fixed
- A schema upgrade can no longer crash-loop on an index that predates its column. An index or
COMMENT ON COLUMNplaced beside its table body names a column that does not exist yet on an upgrade, becauseCREATE TABLE IF NOT EXISTSis a no-op there and the column only arrives in the later additiveALTER TABLEblock. Since the schema applies in one transaction and re-throws, a single such statement aborted the whole apply and crash-looped every boot — on Postgres this broke startup and every CLI invocation for installs upgrading past #2414, and on SQLite it meantnew SqliteAdapter(...)simply could not open a database created beforeparent_conversation_idexisted. Every index and column comment now sits below the additive block. A newcheck:schema-upgradesCI job proves the current schema applies cleanly on top of every schema vintage ever released, re-applies idempotently, and lands on exactly the fresh-install shape. (#2513, #2552) - Workflow-level
modelReasoningEffort:andwebSearchMode:reach Codex. Both had been schema-declared, loader-validated, and documented as working workflow-level options since March, but the executor never read them off the workflow definition — a workflow declaring either parsed cleanly, warned about nothing, and ran at theconfig.yamlor SDK default. Declaring either on a non-Codex node now produces the same loud capability-mismatch warning every other unsupported field produces. (#2559) - The container write-back no longer mis-decodes hostile or merely unusual filenames. The two overlay walk scripts — the one place a container run writes the live root — decoded each entry by forking
basename,dirname, andsed, which produced three reachable defects: a whiteout marker under a--prefixed directory was planted as a literal.wh.*file instead of deleting its target (no adversary required, just a leading dash); a whiteout whose name ended in a newline deleted a different, innocent file and could write outside the live root while reporting success; and a TAB inside a filename or symlink target forged the positionally-decoded fields after it, presenting an escaping symlink to the approval gate as safe. Shell parameter expansion replaces the forks, closing all three and cutting forks per entry from 8 to 4 for a regular file and 6 to 1 for a whiteout. (#2561) - Archon no longer guesses the default branch.
getDefaultBranchended its fallback chain by checking whether<remote>/mainmerely existed and returning it if so. On a repository shaped like this one —devis the default andmainis the release branch — a run silently cut its worktree frommainand targeted its pull request atmain, with nothing erroring at the moment it happened. When<remote>/HEADis not a symbolic ref it now throws, naming--base,worktree.baseBranch, and the codebasedefault_branchfield. (#2503) WORKFLOW_IDis delivered tobash:andscript:node subprocesses. It substituted into the node body but was missing from the environment bag, unlikeARTIFACTS_DIR,STATE_DIR, andLOG_DIRsitting beside it — so a heredoc'dpython3ornodeblock readingos.environfailed inside the nested interpreter with a bareKeyErrorand no hint that its siblings worked fine. Thee2e-deterministicfixture that demonstrated the wrong way to consume an output reference was corrected too. (#2511)- A dry run of the state migration no longer creates a database.
scripts/migrate-state-dir.tsconsulted the codebase registry unconditionally, and the SQLite adapter connects lazily and applies the full schema on first use — so a read-only run wrote a 704 KB database while printingDry run — nothing was moved. (#2557)
Discussion thread — questions, or paste YAML that broke: #2574