Files
lda-wf/docs/historical/superpowers/plans/2026-05-31-draft-output-binding-docs.md
T

8.5 KiB

Draft Output Binding Docs Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Clarify the two different output binding shapes in workflow drafts so MCP/LLM clients stop applying step-level source/target bindings to top-level workflow output projection.

Architecture: This is docs-only for now. The core model is internally coherent: step-level output uses OutputBinding (source local -> target state), while top-level workflow output uses input-binding shape (path graph -> target local output payload). Update the main draft docs, runbook, and schema-facing descriptions to teach this explicitly without changing runtime behavior.

Tech Stack: Markdown docs, Pydantic field descriptions, pytest schema/docs tests, ruff, basedpyright.


Scope

Do:

  • Add a clear “Two Outputs, Different Shapes” docs section.
  • Show exact JSON for both step-level output and top-level workflow output.
  • Explain the legacy fallback: empty top-level output projects same-name top-level state fields.
  • Update MCP model field descriptions so schema viewers see “uses path, not source” for top-level output.
  • Add docs/test assertions that this guidance is exported.

Do not:

  • Rename fields to writes / returns.
  • Change validation behavior.
  • Remove legacy same-name output fallback.
  • Add automatic MCP content block extraction.

Files

  • Modify: docs/workflow_drafts.md
    • Add the primary explanation and examples.
  • Modify: docs/wf_mcp_end_to_end_runbook.md
    • Add a short warning in the draft patching section.
  • Modify: docs/workflow_capabilities.md
    • Mention that next_actions.patch_examples may include top-level output projection examples.
  • Modify: src/wf_artifacts/drafts/models.py
    • Improve WorkflowDraft.output field description.
  • Modify: src/wf_core/models/workflow.py
    • Improve Workflow.output field description.
  • Modify: tests/wf_mcp/server/test_docs.py
    • Assert exported docs include the new guidance.
  • Modify if needed: tests/wf_mcp/server/test_config.py
    • Assert schema descriptions include “path, not source” if exposed in tool schema.

Task 1: Document The Two Output Shapes

Files:

  • Modify: docs/workflow_drafts.md

  • Step 1: Add docs section

After the “Important details” list or before “Explicit Outputs And Error Outcomes”, add:

## Two Outputs, Different Shapes

Drafts have two fields named `output`, but they do different jobs.

### Step-Level `steps.<id>.output`

Step output writes a node's local return payload into workflow state. It uses
`source` / `target`:

```json
{
  "source": { "root": "local", "parts": ["text"] },
  "target": { "root": "state", "parts": ["result_text"] }
}

Read this as:

node output.text -> state.result_text

Top-Level output

Top-level workflow output projects graph values into the final public workflow output payload. It uses input-binding shape: path / target, not source / target.

{
  "path": { "root": "state", "parts": ["result_text"] },
  "target": { "root": "local", "parts": ["result_text"] }
}

Read this as:

state.result_text -> workflow output.result_text

If top-level output is empty, the runtime keeps the legacy same-name fallback: for every field in output_schema, it copies the top-level state field with the same name when present. That fallback is convenient, but explicit output projection is clearer for new workflows.


- [ ] **Step 2: Run grep check**

Run:

```powershell
rg -n "Two Outputs, Different Shapes|path.*not.*source|state.result_text -> workflow output.result_text" docs/workflow_drafts.md

Expected: all terms appear.

Task 2: Update Runbook Warning And Example

Files:

  • Modify: docs/wf_mcp_end_to_end_runbook.md

  • Step 1: Add warning near draft patching section

Near “Patch Or Validate The Workspace”, add:

When patching output bindings, keep the two levels separate:

- Step-level `steps.<id>.output` uses `source` local -> `target` state.
- Top-level `output` uses `path` graph -> `target` local output payload.

For explicit final output projection from state, use:

```json
{
  "path": { "root": "state", "parts": ["result_text"] },
  "target": { "root": "local", "parts": ["result_text"] }
}

Do not use source at top level. source belongs to step output bindings.


- [ ] **Step 2: Run grep check**

Run:

```powershell
rg -n "Do not use `source` at top level|steps.<id>.output|result_text" docs/wf_mcp_end_to_end_runbook.md

Expected: all terms appear.

Task 3: Update Field Descriptions

Files:

  • Modify: src/wf_artifacts/drafts/models.py

  • Modify: src/wf_core/models/workflow.py

  • Step 1: Update WorkflowDraft.output field description

Change:

output: list[InputBinding] = Field(default_factory=list)

to:

output: list[InputBinding] = Field(
    default_factory=list,
    description=(
        "Top-level workflow output projection. Uses input-binding shape: "
        "`path` reads from input/state/context and `target` writes to the "
        "local public output payload. Do not use step output `source` here."
    ),
)
  • Step 2: Update Workflow.output field description

In src/wf_core/models/workflow.py, extend the output description to include:

"Use `path`, not `source`; `source` belongs to step-level node output bindings."

Keep the existing legacy fallback explanation.

  • Step 3: Run focused schema/type checks

Run:

uv run basedpyright --level error src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py
uv run ruff check src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py
uv run ruff format --check src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py

Expected: pass.

Task 4: Add Exported Docs Test

Files:

  • Modify: tests/wf_mcp/server/test_docs.py

  • Step 1: Add docs resource assertion

In the docs resource test that reads workflow authoring/draft docs, assert:

assert "Two Outputs, Different Shapes" in text
assert "Do not use step output `source` here" in text or "Do not use `source` at top level" in text

Use the existing variable names in the file. Do not assert whole payload dict equality.

  • Step 2: Run focused docs test

Run:

uv run pytest tests/wf_mcp/server/test_docs.py -q

Expected: pass.

Task 5: Optional Schema Description Test

Files:

  • Modify if needed: tests/wf_mcp/server/test_config.py

  • Step 1: Inspect whether draft output field description is exposed

Run:

uv run pytest tests/wf_mcp/server/test_config.py -q

If this test already inspects create_draft_workspace request schemas, add:

output_description = minimal_request["properties"]["output"]["description"]
assert "path" in output_description
assert "source" in output_description

If the schema nests the description differently, skip this test change and rely on test_docs.py.

Task 6: Final Verification

Files:

  • All touched docs and Python files.

  • Step 1: Run focused tests

Run:

uv run pytest tests/wf_mcp/server/test_docs.py tests/wf_mcp/server/test_config.py -q

Expected: pass.

  • Step 2: Run touched-file lint/type checks

Run:

uv run ruff check src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py tests/wf_mcp/server/test_docs.py tests/wf_mcp/server/test_config.py
uv run ruff format --check src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py tests/wf_mcp/server/test_docs.py tests/wf_mcp/server/test_config.py
uv run basedpyright --level error src/wf_artifacts/drafts/models.py src/wf_core/models/workflow.py tests/wf_mcp/server/test_docs.py tests/wf_mcp/server/test_config.py

Expected: pass.

  • Step 3: Optional full suite

Run when time allows:

uv run pytest -q

Expected current baseline: full suite passes with the existing skip/xfail count.

Notes For Opencode

  • This is docs/description work only.
  • Do not rename model fields.
  • Do not remove fallback same-name projection.
  • Do not auto-extract MCP content blocks.
  • The exact mental model to teach is:
steps.call.output: local node output -> workflow state
workflow output: input/state/context graph path -> public output payload