10 KiB
Atomic Step Output Bindings Design
Status
Approved for implementation planning on 2026-07-23.
Problem
Canonical step outputs are ordered OutputBinding records that map one
node-local output path into one workflow state path. The focused authoring
surface currently reduces those records to a dict[str, str]. That map cannot
represent one local source feeding several state targets, so reading and later
merging a valid canonical list can silently discard fan-out.
The existing focused operation also patches only the binding list. Callers must separately declare matching state schema paths even when the capability output schema already describes the source values. This makes a common authoring edit multi-step and exposes an avoidable intermediate invalid revision.
Goals
- Replace one capability step's complete ordered output-binding list in one revision-checked operation.
- Preserve source fan-out and nested local/state paths without lowering through a dictionary.
- Project missing state schema paths from the capability output schema in the same atomic mutation.
- Expose the canonical operation through Python, JSON-RPC, MCP, and CLI.
- Keep the existing map operation for real compatibility callers while making its lossy behavior explicit.
- Reuse the existing schema path and projection helpers instead of adding a second JSON Schema implementation.
Non-Goals
- Changing the persisted
OutputBindingmodel or runtime output application. - Supporting literal step outputs. Step outputs read values produced by a node; literal bindings belong to workflow output projection, not this operation.
- Solving top-level workflow output literals or nested workflow output schema projection.
- Replacing every legacy output-map reader in this slice.
- Adding a generic input/output binding replacement framework.
- Generating TypeScript RPC contracts.
Canonical Operation
Add this operation to the public workflow API:
async def set_step_output_bindings(
*,
workspace_id: str,
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]:
"""Replace one capability step's canonical output bindings atomically."""
The operation performs these steps in order:
- Validate the request envelope before entering semantic authoring logic.
- Check the expected draft revision before reading the step or capability.
- Require the selected draft step to declare a capability through
use. - Resolve that capability's output schema contract.
- Validate every local
sourceagainst the capability output schema. - Reject duplicate or ancestor/descendant state
targetpaths. - Project each missing state target from its selected capability output schema
fragment, preserving local
$defsanddefinitionsreferences. - Accept an existing state target only when its schema is exactly equal to the selected capability output fragment.
- Replace the step output list and any changed
state_schemain one patch.
An identical binding list with an identical projected state schema is an exact no-op: return the current workspace summary without incrementing the revision.
Binding Semantics
The operation accepts the existing core model unchanged:
[
{"source": "report.title", "target": "state.report.title"},
{"source": "report.title", "target": "state.audit.title"},
{"source": ".", "target": "state.raw_result"}
]
sourceis a rootlessLocalPath..selects the complete capability output payload.targetis a non-rootStatePath, serialized with thestate.prefix.- Binding order is preserved exactly.
- Repeating a source is valid and represents fan-out.
- Repeating a target is invalid.
- Ancestor/descendant targets such as
state.reportandstate.report.titleare invalid because applying both is order-dependent. - Empty bindings are valid and clear the step output list; they do not remove previously declared state schema fields.
Schema Projection
wf_api.schema_projection remains the only implementation of JSON Schema path
selection and projection. Output authoring uses
schema_fragment_at_path(...), schema_path_exists(...), and
project_schema_path_to_schema_path(...) rather than introducing output-only
schema traversal.
For each binding:
- select
source.partsfrom the capability output schema; - if
target.partsis absent from the draft state schema, copy the selected fragment into that target path; - if the target exists with the exact same schema, leave it unchanged;
- if the target exists with a different schema, reject the operation rather than guessing JSON Schema compatibility;
- reject unresolved, remote, cyclic, or conflicting schema references through the existing projection errors.
Projection is monotonic. Replacing or clearing bindings never deletes state schema declarations because other steps or reducers may still depend on them.
Compatibility Boundary
Keep set_step_output_map(...) and
workflow.draft_workspaces.set_step_output_map unchanged for existing callers.
Its merge=True mode remains a compatibility adapter and remains inherently
lossy when canonical fan-out already exists.
New code and documentation prefer canonical replacement. The issue describing lossy compatibility maps remains open until all map readers and writers are removed or made incapable of rewriting canonical fan-out.
Transport Design
Add these surfaces:
- Python API:
set_step_output_bindings; - JSON-RPC:
workflow.draft_workspaces.set_step_output_bindings; - remote RPC client method:
set_step_output_bindings; - MCP tool:
wf.workflow.set_step_output_bindings.
RPC and MCP request models use list[OutputBinding] directly. They must preserve
the canonical list order and must not translate through DraftPathMap.
Malformed objects fail Pydantic/request-envelope validation before semantic
authoring logic runs.
CLI Design
Keep the existing command name:
wf draft set-output WORKSPACE --revision N --step STEP ...
Preferred canonical forms are:
wf draft set-output WS \
--revision 4 \
--step analyze \
--map report.title=state.report.title \
--map report.title=state.audit.title
and:
wf draft set-output WS \
--revision 4 \
--step analyze \
--bindings-file output-bindings.json
CLI behavior:
- repeatable
--map LOCAL_SOURCE=STATE_TARGETcreates canonical bindings in flag order; --bindings-fileaccepts the exact canonical JSON array and preserves order;--clearexplicitly replaces the list with[];- no bindings and no
--clearis an input error; --bindings-fileand--clearare mutually exclusive with--map;- canonical replacement does not support
--merge; --merge --map ...delegates to the legacy map operation and is documented as compatibility-only and potentially lossy.
Current bindings remain exportable without another endpoint:
wf draft inspect WS --include-draft |
jq '.draft.steps.analyze.output' > output-bindings.json
Error Contract
Semantic failures should identify the binding index and canonical path where possible. Representative errors are:
bindings[1].source 'missing' is not declared by capability 'report.analyze';bindings[0].target 'state.report' overlaps bindings[1].target 'state.report.title';bindings[0].target 'state.report.title' already has an incompatible schema;bindings[2].source '.' cannot be projected to 'state.raw_result': ....
After request-envelope validation, stale revision wins over missing steps, unknown capabilities, schema lookup, overlap, and projection errors. Every failure leaves the draft and revision unchanged.
Testing
Python Authoring
Add focused tests for:
- ordered replacement with nested local and state paths;
- one local source fanning out to several state targets;
- whole-payload
.projection; - missing nested state schema projection;
- exact-equivalent existing target schemas;
- incompatible existing target schemas;
- missing local sources;
- duplicate and ancestor/descendant state targets;
- stable binding order in the stored draft;
- exact no-op behavior;
- explicit clearing without state 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 to prove runtime output fan-out writes the expected values to both state paths.
Transport And CLI
Add tests proving:
- JSON-RPC, remote client, and MCP preserve canonical output-binding order and duplicate sources;
- malformed binding objects fail request validation;
- CLI repeated
--mapflags preserve order and fan-out; --bindings-fileround-trips an exported inspected list;--clearis explicit and an empty accidental invocation fails;- canonical replacement rejects
--mergecombinations; - compatibility
--merge --mapremains operational; - local and remote CLI targets use the same canonical operation.
Documentation And Issue State
After verification:
- keep the combined input/output map-loss issue open, but update it to state that both canonical replacements preserve fan-out while map compatibility operations remain lossy;
- leave workflow-output literals, nested workflow-output projection, focused step updates, and TypeScript parity open;
- update CLI help, user docs, and agent skills with canonical replacement, fan-out, clear, 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 a step's complete canonical output-binding list in one revision.
- Nested paths, source fan-out, and whole-payload output use the existing core binding model unchanged.
- Every local source is validated against the capability output schema before mutation.
- Missing state target schemas are projected atomically with the binding list.
- Existing map-only callers retain their current behavior through a clearly documented compatibility adapter.
- No new persisted binding type, template language, or JSON Schema traversal is introduced.
- Focused API, runtime, RPC, MCP, CLI, Ruff, formatting, and basedpyright checks pass.