18 KiB
Workflow Draft Surface 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: Replace the disposable draft prototype with the first real MCP-facing workflow draft surface: keyed, patchable JSON that adapts into wf_authoring.WorkflowBuilder instead of rebuilding graph semantics itself.
Architecture: The new draft layer remains a typed JSON seam in wf_artifacts, but delegates graph construction to wf_authoring. Draft parsing owns keyed presentation, validation, and stable patch paths; WorkflowBuilder owns graph construction. The MCP workflow surface keeps the same tool family while accepting the new draft shape.
Tech Stack: Python 3.14, Pydantic v2, wf_authoring, wf_core, jsonpatch, pytest, basedpyright, Ruff.
File Structure
Create
src/wf_artifacts/drafts/models.py- concrete Pydantic draft document models
src/wf_artifacts/drafts/adapter.py- thin JSON-draft-to-
WorkflowBuilderadapter
- thin JSON-draft-to-
src/wf_artifacts/drafts/api.py- public compile/validate/patch functions and diagnostics
tests/artifacts/test_draft_models.py- draft document validation
tests/artifacts/test_draft_adapter.py- keyed step/routes lowering through
WorkflowBuilder
- keyed step/routes lowering through
tests/artifacts/test_draft_api.py- public compile/validate/patch behavior
Modify
src/wf_artifacts/drafts.py- replace module body with compatibility re-exports or remove once imports are updated
src/wf_artifacts/__init__.py- export public draft API/models from the package
src/wf_authoring/builder/core.py- add
use_ref(...)for named external capabilities without localNodeSpecs
- add
src/wf_authoring/ops/*- only if route helpers need a reusable public lowering entrypoint
src/wf_mcp/workflow_surface/handlers.py- keep using public draft API; no semantic duplication
tests/wf_mcp/test_workflow_surface.py- update draft fixtures to the new keyed document shape
tests/wf_mcp/test_server.py- confirm MCP schemas remain plain-object friendly
docs/workflow_drafts.md- replace prototype examples with the first real draft surface
docs/wf_mcp_end_to_end_runbook.md- update draft example
docs/wf_mcp_operator_manual.md- keep draft-first guidance accurate
docs/wf_mcp_troubleshooting.md- update patch-path examples
Task 1: Split Draft Code Into Focused Modules
Files:
-
Create:
src/wf_artifacts/drafts/models.py -
Create:
src/wf_artifacts/drafts/api.py -
Create:
src/wf_artifacts/drafts/adapter.py -
Modify:
src/wf_artifacts/drafts.py -
Modify:
src/wf_artifacts/__init__.py -
Test:
tests/artifacts/test_draft_models.py -
Step 1: Write the failing model tests
from wf_artifacts.drafts import DraftUseStep, WorkflowDraft
def test_workflow_draft_uses_keyed_steps() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {
"echo": {
"use": "demo.echo",
"in": {"input.text": "text"},
"out": {"echoed": "state.echoed"},
}
},
"routes": {"echo": {"ok": "__end__"}},
}
)
assert isinstance(draft.steps["echo"], DraftUseStep)
assert draft.steps["echo"].use == "demo.echo"
def test_draft_step_requires_exactly_one_kind_key() -> None:
result = WorkflowDraft.model_validate(
{
"name": "bad",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "bad",
"steps": {
"bad": {
"use": "demo.echo",
"join": {},
}
},
"routes": {},
}
)
The second test should be written with pytest.raises(ValidationError) and assert the authoring path identifies steps.bad.
- Step 2: Run tests to verify they fail
Run:
uv run --with pytest pytest tests/artifacts/test_draft_models.py -q
Expected: import errors or validation failures because keyed draft models do not exist yet.
- Step 3: Implement minimal concrete draft models
Create:
# src/wf_artifacts/drafts/models.py
from __future__ import annotations
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, model_validator
JsonObject = dict[str, Any]
STEP_KIND_KEYS = frozenset({"use", "foreach", "interrupt", "join"})
class DraftUseStep(BaseModel):
use: str
in_: dict[str, str] = Field(default_factory=dict, alias="in")
out: dict[str, str] = Field(default_factory=dict)
desc: str | None = None
retry: int | None = Field(default=None, ge=0)
timeout_seconds: int | None = Field(default=None, gt=0)
class DraftForeachPayload(BaseModel):
over: str
as_: str = Field(alias="as")
mode: Literal["serial", "parallel"] = "serial"
on_item_error: Literal["fail", "collect", "skip"] = "fail"
class DraftForeachStep(BaseModel):
foreach: DraftForeachPayload
class DraftInterruptPayload(BaseModel):
kind: str
request: dict[str, str] = Field(default_factory=dict)
resume: dict[str, str] = Field(default_factory=dict)
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
class DraftInterruptStep(BaseModel):
interrupt: DraftInterruptPayload
class DraftJoinStep(BaseModel):
join: JsonObject = Field(default_factory=dict)
DraftStep = Annotated[
DraftUseStep
| DraftForeachStep
| DraftInterruptStep
| DraftJoinStep,
Field(discriminator=None),
]
class WorkflowDraft(BaseModel):
name: str
input_schema: JsonObject
state_schema: JsonObject
output_schema: JsonObject
start: str
steps: dict[str, DraftStep]
routes: dict[str, dict[str, str]] = Field(default_factory=dict)
@model_validator(mode="before")
@classmethod
def _validate_step_kinds(cls, value: object) -> object:
if not isinstance(value, dict):
return value
steps = value.get("steps")
if not isinstance(steps, dict):
return value
for step_id, payload in steps.items():
if not isinstance(payload, dict):
continue
present = STEP_KIND_KEYS.intersection(payload)
if len(present) != 1:
raise ValueError(
f"steps.{step_id} must contain exactly one step kind key"
)
return value
Keep the public import path stable by re-exporting through src/wf_artifacts/drafts.py during the transition.
- Step 4: Run tests to verify they pass
Run:
uv run --with pytest pytest tests/artifacts/test_draft_models.py -q
Expected: pass.
- Step 5: Commit
git add src/wf_artifacts tests/artifacts/test_draft_models.py
git commit -m "refactor: add keyed workflow draft models"
Task 2: Add use_ref And Thin Adapter Over WorkflowBuilder
Files:
-
Create:
src/wf_artifacts/drafts/adapter.py -
Modify only if needed:
src/wf_authoring/builder/core.py -
Modify only if needed:
src/wf_authoring/ops/* -
Test:
tests/artifacts/test_draft_adapter.py -
Step 1: Write failing adapter tests
from wf_artifacts.drafts import WorkflowDraft
from wf_artifacts.drafts.adapter import build_workflow_from_draft
def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {"echo": {"use": "demo.echo"}},
"routes": {"echo": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
assert workflow.nodes[0].id == "echo"
assert workflow.nodes[0].node == "demo.echo"
assert workflow.edges[0].from_ == "echo"
assert workflow.edges[0].outcome == "ok"
assert workflow.edges[0].to == "__end__"
def test_builder_use_ref_creates_external_node_use_without_node_def() -> None:
builder = WorkflowBuilder(
"echo",
input_schema={},
state_schema={"fields": {}},
output_schema={},
)
step = builder.use_ref("demo.echo", id="echo")
builder.set_entry_point(step)
builder.connect(step, "ok", "__end__")
workflow = builder.compile()
assert step.node == "demo.echo"
assert workflow.node_defs == []
- Step 2: Run tests to verify they fail
Run:
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py -q
Expected: import error because build_workflow_from_draft does not exist.
- Step 3: Implement the thin adapter
First add:
def use_ref(
self,
name: str,
*,
id: str | None = None,
in_map: MapArg | None = None,
out_map: MapArg | None = None,
desc: str | None = None,
) -> NodeUse:
...
use_ref creates a NodeUse for an already named external capability and does
not add a local NodeDef.
Then implement build_workflow_from_draft(draft: WorkflowDraft) -> Workflow so
it:
- constructs a
WorkflowBuilder - registers each draft step by stable id
- uses existing
WorkflowBuilderpublic methods for:use_refforeachinterruptjoin
- applies
routes - calls explicit
start(...) - returns
builder.build(...)
Do not invent draft route sugar in this pass.
- Step 4: Run tests to verify they pass
Run:
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py -q
Expected: pass.
- Step 5: Commit
git add src/wf_artifacts src/wf_authoring tests/artifacts/test_draft_adapter.py
git commit -m "feat: adapt workflow drafts through workflow builder"
Task 3: Replace Prototype Public API
Files:
-
Create:
src/wf_artifacts/drafts/api.py -
Modify:
src/wf_artifacts/drafts.py -
Modify:
src/wf_artifacts/__init__.py -
Test:
tests/artifacts/test_draft_api.py -
Step 1: Write failing API tests
from wf_artifacts.drafts import compile_workflow_draft, patch_workflow_draft
def test_compile_workflow_draft_returns_raw_core_shape() -> None:
plan = compile_workflow_draft(_keyed_echo_draft())
assert plan["nodes"][0]["id"] == "echo"
assert plan["nodes"][0]["node"] == "demo.echo"
assert plan["edges"][0]["outcome"] == "ok"
def test_patch_workflow_draft_uses_stable_step_paths() -> None:
result = patch_workflow_draft(
_keyed_echo_draft(),
[
{
"op": "replace",
"path": "/steps/echo/in/input.text",
"value": "message",
}
],
)
assert result["status"] == "valid"
assert result["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
- Step 2: Run tests to verify they fail
Run:
uv run --with pytest pytest tests/artifacts/test_draft_api.py -q
Expected: failures because the old prototype API still expects array steps.
- Step 3: Implement the API
Create:
# src/wf_artifacts/drafts/api.py
def compile_workflow_draft(draft: JsonObject) -> JsonObject:
parsed = WorkflowDraft.model_validate(draft)
workflow = build_workflow_from_draft(parsed)
return workflow.model_dump(mode="json", by_alias=True, exclude={"node_defs"})
Keep:
validate_workflow_draftpatch_workflow_draft- structured
DraftDiagnostic
Update diagnostics to use keyed paths such as:
steps.echo.in
routes.echo.error
Delete the old array-step prototype code after public tests are green.
- Step 4: Run tests to verify they pass
Run:
uv run --with pytest pytest tests/artifacts/test_draft_api.py -q
Expected: pass.
- Step 5: Commit
git add src/wf_artifacts tests/artifacts/test_draft_api.py
git commit -m "feat: replace draft prototype with keyed public api"
Task 4: Update MCP Workflow Surface
Files:
-
Modify:
tests/wf_mcp/test_workflow_surface.py -
Modify:
tests/wf_mcp/test_server.py -
Modify only if needed:
src/wf_mcp/workflow_surface/handlers.py -
Step 1: Update the failing MCP tests
Replace old fixtures like:
"steps": [{"id": "echo", "kind": "use", ...}]
with:
"steps": {"echo": {"use": "demo.echo", ...}},
"routes": {"echo": {"ok": "__end__"}},
Keep assertions that:
-
draft tools still expose plain object schemas to MCP clients
-
create_artifact_from_draftstill saves artifacts -
source binding normalization still works
-
missing
wf.stdself-binding diagnostics still work -
Step 2: Run tests to verify failures
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
Expected: failures wherever MCP handlers still assume the old prototype shape.
- Step 3: Make minimal MCP adjustments
Keep handlers thin:
plan = compile_workflow_draft(draft)
No duplicate draft interpretation should appear in wf_mcp.
- Step 4: Run tests to verify they pass
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
Expected: pass.
- Step 5: Commit
git add src/wf_mcp tests/wf_mcp
git commit -m "feat: accept keyed workflow drafts over mcp"
Task 5: Add Outcome Validation When Capability Contracts Are Available
Files:
-
Modify:
src/wf_artifacts/drafts/api.py -
Modify:
src/wf_mcp/workflow_surface/handlers.py -
Test:
tests/wf_mcp/test_workflow_surface.py -
Step 1: Write failing outcome validation test
def test_draft_validation_rejects_unknown_capability_outcome_when_spec_is_known() -> None:
handlers = _handlers_with_demo_echo_spec()
draft = _keyed_echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"}
result = asyncio.run(handlers.validate_draft(draft=draft))
assert result["status"] == "invalid"
assert result["diagnostics"][0]["path"] == "routes.echo.typo"
- Step 2: Run test to verify it fails
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py -q
Expected: validation currently accepts the typo.
- Step 3: Implement capability-aware outcome validation
Pass an optional capability lookup into draft validation from MCP handlers.
Rules:
- validate outcome keys for
usesteps only when the capability is resolvable - if the capability is unknown/unavailable, leave dependency validation to the later artifact/deployment stages
- diagnostic path must identify the keyed route entry
Do not make wf_artifacts depend on wf_mcp; define a tiny callable/protocol
interface for lookup instead.
- Step 4: Run tests to verify they pass
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py -q
Expected: pass.
- Step 5: Commit
git add src/wf_artifacts src/wf_mcp tests/wf_mcp/test_workflow_surface.py
git commit -m "feat: validate draft routes against known outcomes"
Task 6: Update Documentation
Files:
-
Modify:
docs/workflow_drafts.md -
Modify:
docs/wf_mcp_end_to_end_runbook.md -
Modify:
docs/wf_mcp_operator_manual.md -
Modify:
docs/wf_mcp_troubleshooting.md -
Step 1: Update docs to the real draft surface
Replace prototype array examples with keyed examples:
"steps": {
"echo": {
"use": "demo.echo_tool",
"in": {"input.text": "text"},
"out": {"echoed": "state.echoed"}
}
},
"routes": {
"echo": {
"ok": "__end__"
}
}
Document:
-
exactly-one-kind-key rule
-
stable keyed patch paths
-
routeas repeated condition-chain sugar -
WorkflowBuilderas the semantic owner beneath the JSON adapter -
outcome strings validated against capability contracts when available
-
Step 2: Run a targeted docs scan
Run:
rg -n '\"steps\": \\[|\"kind\": \"use\"|/steps/0|create_artifact_from_draft' docs
Expected:
-
no stale prototype examples in current docs
-
create_artifact_from_draftstill documented as the preferred path -
Step 3: Commit
git add docs
git commit -m "docs: describe keyed workflow draft surface"
Task 7: Full Verification
Files:
-
No new files
-
Step 1: Run focused verification
uv run --with pytest pytest tests/artifacts tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
Expected: pass.
- Step 2: Run full project tests
uv run --with pytest pytest -q
Expected: pass.
- Step 3: Run type checking
uv run basedpyright --level error
Expected: 0 errors.
- Step 4: Run lint
uvx ruff check src tests
Expected: pass.
- Step 5: Commit final cleanup
git add .
git commit -m "feat: ship keyed workflow draft authoring surface"
Self-Review
Spec Coverage
- keyed
steps: Tasks 1-4 - compact
routes: Tasks 1-4 - verb-keyed explicit step kinds: Task 1
- saved capability/workflow refs in
use: Task 2, existing capability refs pass through unchanged - stable patch paths: Tasks 3 and 6
wf_authoringas semantic owner: Tasks 2 and 6- outcome validation against declared contracts: Task 5
- prototype replacement rather than migration: Tasks 3, 4, 6
Placeholder Scan
- no
TBD - no unspecified "add validation" placeholders
- every task has exact files, tests, commands, and expected behavior
Type Consistency
WorkflowDraft,DraftUseStep,build_workflow_from_draft, and public API names stay consistent across all tasks- patch examples use keyed paths consistently
routesstays the only authored outcome-routing section