# Atomic Workflow Output Bindings Design ## Status Approved for implementation planning on 2026-07-23. ## Problem `WorkflowDraft.output` already stores the canonical ordered union of `InputPathBinding | InputValueBinding`. These bindings construct the public workflow output from workflow input, state, context, or literal JSON values. The focused `set_workflow_output_map(...)` authoring surface narrows that model to `dict[str, str]`. It cannot add literal outputs, cannot preserve every canonical list shape, and only projects output schemas for single-field top-level `input.*` and `state.*` sources. Nested sources such as `state.report.title` therefore require separate schema patches even though the source schema already declares the selected value. The runtime behavior is already sufficient. The missing concept is one atomic authoring operation over the existing canonical workflow-output binding list. ## Goals - Replace the complete ordered workflow-output binding list in one revision-checked operation. - Support path and literal bindings through the existing `InputBinding` union. - Project missing nested public output schemas from declared workflow input or state source schemas. - Validate literal outputs against already-declared public output schemas. - Preserve the current implicit same-name state fallback when the explicit binding list is empty. - Expose the canonical operation through Python, JSON-RPC, MCP, and CLI. - Keep the existing map operation for compatibility callers while documenting its limitations. - Reuse `wf_api.schema_projection` rather than adding another JSON Schema traversal or inference system. ## Non-Goals - Changing `WorkflowDraft.output`, `Workflow.output`, or runtime output projection. - Moving output projection into end nodes. End nodes remain control-flow nodes; workflow output is still projected after execution finishes. - Inferring JSON Schema from literal values. - Deleting output schema declarations when bindings are replaced or cleared. - Removing the implicit output fallback for workflows with no explicit output bindings. - Replacing every compatibility map reader or writer in this slice. - Adding a generic binding editor shared by step inputs, step outputs, and workflow outputs. - Generating TypeScript RPC contracts. ## Existing Runtime Semantics The runtime distinction remains explicit: - when `workflow.output` contains bindings, the runtime applies those bindings in order to construct the public output object; - when `workflow.output` is empty, the runtime retains its current fallback and reads same-named top-level state fields declared by `output_schema`. `--clear` therefore restores the existing implicit fallback. It does not mean "produce an empty output" and does not change end-node behavior. ## Canonical Operation Add this operation to the public workflow API: ```python async def set_workflow_output_bindings( *, workspace_id: str, revision: int, bindings: Sequence[InputBinding], ) -> dict[str, Any]: """Replace canonical workflow output bindings atomically.""" ``` The operation performs these steps in order: 1. Validate the request envelope and canonical binding union. 2. Check the expected draft revision before semantic inspection. 3. Validate every path source against its available workflow schema. 4. Reject duplicate or ancestor/descendant output targets. 5. Stage output-schema projection and literal validation. 6. Replace `/output` and any changed `/output_schema` through one patch. An identical binding list with an identical projected output schema is an exact no-op and does not advance the draft revision. ## Binding Semantics The operation accepts the existing model unchanged: ```json [ {"path": "state.report.title", "target": "report.title"}, {"path": "state.report.title", "target": "audit.title"}, {"value": "markdown", "target": "format"} ] ``` - `path` is a `GraphSourcePath` rooted at `input`, `state`, or `context`. - `target` is a rootless `LocalPath` inside the public output payload. - `value` is any JSON-compatible literal, including explicit `null`, arrays, and objects. - Binding order is preserved exactly. - Repeating a path source is valid and represents public-output fan-out. - Repeating a target is invalid. - Ancestor/descendant targets such as `report` and `report.title` are invalid because applying both is order-dependent. - Empty bindings clear the explicit list and restore implicit fallback. - Replacing or clearing bindings never removes existing output-schema fields. The root target `.` remains valid because it is part of the canonical `LocalPath` model. It overlaps every other target and therefore must be the only binding. A root path binding requires its selected source schema to be exactly equivalent to the complete declared `output_schema`; a root literal must be a mapping that validates against the complete declared `output_schema`. The operation does not replace or infer the root output schema. `context.*` path sources cannot target `.`. Context values do not have a statically declared schema, so the operation cannot perform the exact whole-schema comparison required for a root binding. They remain valid for declared non-root output targets. ## Path Source And Schema Rules For `input.*` and `state.*` path bindings: 1. Select the complete nested source fragment from `input_schema` or `state_schema` with `schema_fragment_at_path(...)`. 2. Reject an undeclared source path. Output authoring cannot infer a graph source that the workflow contract does not declare. 3. If the non-root output target is missing, project the selected fragment into that target with `project_schema_path_to_schema_path(...)`. 4. If the target exists, accept it only when its schema is exactly equal to the selected source fragment. 5. Reject incompatible targets rather than guessing JSON Schema assignability. For `context.*` path bindings, no workflow-owned context schema exists. The source remains valid under the existing model, but its output target must already be declared. The operation cannot project or statically compare a context source schema. Runtime resolution and final output validation remain authoritative. Whole-source paths `state` and `input` use the complete corresponding schema. They can target a non-root nested output field through ordinary projection. A whole-source-to-root binding follows the stricter exact-root rule above. ## Literal Rules Literal bindings never infer output schemas. - The output target must already exist in `output_schema`. - Validate the literal with `validate_json_value_at_schema_path(...)` against the selected target schema. - A root literal must be a mapping because the runtime public output payload is an object. - Explicit `null` is accepted only when the declared schema accepts null. - Invalid literals fail before mutation and identify their binding index and target. This keeps `output_schema` authoritative. Authors can use `set-contract` first when introducing a new literal output field. ## Shared JSON Schema Operations `wf_api.schema_projection` remains the only JSON Schema traversal and projection implementation. This slice reuses: - `schema_fragment_at_path(...)` for nested input/state sources; - `schema_path_exists(...)` for declared output targets; - `project_schema_path_to_schema_path(...)` for missing output paths; - `validate_json_value_at_schema_path(...)` for literals. The literal-validation helper currently uses capability-input wording in one internal diagnostic label. Generalize that label parameter rather than adding a workflow-output-specific validator. Local `$defs` and legacy `definitions` references must remain self-contained when projected. Target-side local references must be traversed through their definition objects. Remote, unresolved, cyclic, non-object, and conflicting references fail through the shared helper errors. Projection is monotonic. A replacement can add missing output schema paths but cannot delete or silently rewrite incompatible declarations. ## Revision And Error Semantics The operation follows established semantic-authoring precedence: 1. malformed request shapes fail transport validation; 2. stale revision returns the canonical `revision_conflict` result; 3. source-path errors are reported in binding order; 4. target overlap is rejected; 5. schema projection and literal validation errors are reported in binding order; 6. mutation-time revision checking remains the final race guard. Every semantic failure leaves the draft and revision unchanged. Representative errors are: - `bindings[0].path 'state.report.missing' is not declared`; - `bindings[0].target 'report' overlaps bindings[1].target 'report.title'`; - `bindings[1].target 'report.title' already has an incompatible schema`; - `bindings[2].value does not satisfy output schema at 'format'`; - `bindings[0].path 'context.user' requires a declared output target`. ## Compatibility Boundary Keep `set_workflow_output_map(...)` and `workflow.draft_workspaces.set_workflow_output_map` for existing callers. `merge=True` remains a compatibility adapter. It preserves existing literal records where possible but cannot add literals or faithfully represent every ordered canonical list. New code and documentation prefer canonical replacement. The combined map-loss issue remains open until compatibility map readers and writers can no longer rewrite canonical fan-out or ordering. ## Transport Design Add these surfaces: - Python API: `set_workflow_output_bindings`; - JSON-RPC: `workflow.draft_workspaces.set_workflow_output_bindings`; - remote RPC client method: `set_workflow_output_bindings`; - MCP tool: `wf.workflow.set_workflow_output_bindings`. RPC and MCP request models use `list[InputBinding]` directly. They must preserve path/value union shapes and caller order without translating through a map. Malformed or ambiguous binding objects fail request-envelope validation before semantic authoring logic runs. ## CLI Design Keep the existing command name: ```text wf draft set-workflow-output WORKSPACE --revision N ... ``` Preferred canonical forms are: ```bash wf draft set-workflow-output WS \ --revision 4 \ --map state.report.title=report.title \ --map state.report.markdown=report.markdown \ --value format='"markdown"' ``` and: ```bash wf draft set-workflow-output WS \ --revision 4 \ --bindings-file output-bindings.json ``` CLI behavior: - repeatable `--map GRAPH_SOURCE=LOCAL_TARGET` creates path bindings; - repeatable `--value LOCAL_TARGET=JSON` creates literal bindings; - convenience flags serialize all `--map` bindings in flag order followed by all `--value` bindings in flag order; - `--bindings-file` accepts the exact canonical JSON array and preserves order; - `--bindings-file` is mutually exclusive with `--map` and `--value`; - `--clear` explicitly replaces the list with `[]`; - no bindings and no `--clear` is an input error; - canonical replacement does not support `--merge`; - `--merge --map ...` delegates to the compatibility map operation; - `--merge` rejects `--value`, `--bindings-file`, and `--clear`. Current bindings remain exportable without another endpoint: ```bash wf draft inspect WS --include-draft | jq '.draft.output' > output-bindings.json ``` ## Testing ### Python Authoring And Runtime Add focused tests for: - ordered nested input/state path replacement; - one source fanning out to several output targets; - path and literal bindings in one replacement; - nested output-schema projection from nested input and state sources; - whole-source projection to a nested output target; - exact-equivalent and incompatible existing target schemas; - context paths with declared and missing output targets; - literal scalars, objects, arrays, explicit null, and invalid values; - root target exact-schema, mapping-literal, and overlap behavior; - duplicate and ancestor/descendant targets; - exact no-op behavior; - explicit clearing without output-schema deletion; - stale revision precedence and no mutation for every failure class; - compatibility of the existing map-only merge operation. Compile and execute one representative draft proving nested path projection, literal output, source fan-out, and the empty-list fallback where practical. ### Transport And CLI Add tests proving: - JSON-RPC, remote client, and MCP preserve path/value unions and order; - malformed binding objects fail request validation; - CLI repeated `--map` and `--value` flags produce one canonical request; - `--bindings-file` round-trips an exported inspected list; - `--clear` is explicit and an empty accidental invocation fails; - canonical replacement rejects `--merge` combinations; - compatibility `--merge --map` remains operational; - local and remote CLI targets invoke the same canonical operation. ## Documentation And Issue State After verification: - check the focused workflow-output literal authoring issue; - check the nested workflow-output schema projection issue; - keep compatibility input/output map loss open; - leave focused step metadata updates and TypeScript parity open; - update CLI help, user docs, MCP inventories, and agent skills with canonical replacement, literal, nested projection, clear, fallback, compatibility, and export examples; - add a completed roadmap entry linking to the archived implementation plan; - archive the implementation plan under `docs/historical/superpowers/plans/`. ## Success Criteria - One focused operation replaces the complete canonical workflow-output binding list in one revision. - Nested input/state sources, fan-out, literals, explicit null, context paths, and whole-source bindings use the existing runtime model unchanged. - Missing nested output schemas are projected atomically from declared source schemas. - Literal and context bindings never trigger schema inference. - Clearing explicit bindings restores the existing same-name state fallback. - Existing compatibility map callers retain their current behavior. - No new persisted binding type, end-node behavior, template language, or JSON Schema implementation is introduced. - Focused API, runtime, RPC, MCP, CLI, Ruff, formatting, and basedpyright checks pass.