14 KiB
Capability Step Update Design
Status: Approved design
Date: 2026-07-26
Summary
Add one revision-checked operation for updating an existing capability-backed draft step without removing and recreating it or writing raw JSON Patch. The operation updates step metadata and, when requested, replaces the step's complete canonical input-binding list in the same atomic revision.
The same slice extends capability-step creation so callers can set metadata and canonical literal inputs when the step is first added.
Problem
DraftUseStep already supports:
desc;retry;timeout_seconds;- ordered canonical path and literal input bindings.
The focused authoring surfaces do not expose those capabilities coherently.
wf draft add capability accepts path inputs through a compatibility map but
cannot set literals or execution metadata. After creation, changing metadata
requires raw JSON Patch or removing and recreating the step. Removing and
recreating is especially poor for agents because it also risks disturbing
routes and output bindings that are unrelated to the intended edit.
The existing set_step_input_bindings operation solves canonical input
replacement, but it cannot combine that replacement with metadata changes in
one revision. A focused capability-step update should provide that atomic edit
without becoming a generic step-replacement mechanism.
Goals
- Update
desc,retry, andtimeout_secondson an existingDraftUseStep. - Distinguish omitted metadata from explicit clearing.
- Optionally replace the complete ordered canonical input-binding list in the same revision.
- Reuse existing capability-aware input validation and schema projection.
- Preserve
use, output bindings, routes, and all unrelated draft content. - Expose the operation through Python, JSON-RPC, MCP, and local/remote CLI.
- Let
wf draft add capabilityset the same metadata and canonical input shapes at creation. - Preserve stale-revision precedence, exact no-op behavior, and atomic failure.
Non-Goals
- Changing the step's
usecapability. - Updating routes or step output bindings.
- Updating non-capability step kinds.
- Replacing the complete
DraftUseStepdocument. - Removing existing compatibility map operations.
- Adding TypeScript RPC coverage in this slice.
Changing use is deliberately structural. A different capability can change
input/output schemas and declared outcomes, so callers must remove and add the
step explicitly rather than hiding that migration inside metadata repair.
Considered Interfaces
Typed Patch Model
Use one presence-aware CapabilityStepUpdate model and pass it through every
transport.
Advantages:
- omission and explicit
nullremain distinct; - one interface carries the same semantics across Python, RPC, MCP, and CLI;
- validation and mutation remain concentrated in the authoring module;
- future metadata fields can be added without another operation.
This is the selected approach.
Loose Optional Parameters
Pass one optional parameter per field plus internal sentinel values.
This makes the Python call superficially smaller, but every transport must reconstruct omission-versus-null semantics. It spreads one concept across several adapters and is rejected.
Complete Step Replacement
Require the caller to provide a complete DraftUseStep.
This simplifies mutation code but forces callers to preserve use, outputs,
and unrelated metadata. It creates avoidable lost-update risk and is rejected.
Domain Model
The transport-safe update model is:
class CapabilityStepUpdate(BaseModel):
desc: str | None = Field(default=None, min_length=1)
retry: int | None = Field(default=None, ge=0)
timeout_seconds: int | None = Field(default=None, gt=0)
input: list[InputBinding] | None = None
The model's interface includes Pydantic field presence:
| Field state | Meaning |
|---|---|
| Field omitted | Preserve the stored value |
desc: null |
Remove the description |
retry: null |
Remove the retry override |
timeout_seconds: null |
Remove the timeout override |
input omitted |
Preserve current canonical input bindings |
input: [] |
Clear canonical input bindings |
input: null |
Reject as ambiguous |
An update with no fields set is invalid. The model or operation must reject it before capability metadata is loaded.
model_fields_set is semantic input, not an implementation detail to discard
at a transport seam. RPC and MCP request models therefore carry the update as
a nested object rather than flattening nullable fields into the request
envelope.
Canonical Authoring Operation
Add:
async def update_capability_step(
*,
workspace_id: str,
revision: int,
step_id: str,
update: CapabilityStepUpdate,
) -> dict[str, Any]
to WorkflowDraftAuthoringApi, the public workflow surface, and WorkflowApi.
Validation And Mutation Order
The operation must:
- Validate the request envelope, including the non-empty update rule.
- Load the workspace and check
revision. - Require
stepsto be an object. - Require
step_idto exist. - Parse the selected step and require
DraftUseStep. - If
inputis omitted, skip capability resolution entirely. - If
inputis present, resolve the unchangedusecapability and reuse the same canonical input preflight used byset_step_input_bindings. - Build the complete changed step and any projected input/state schemas in memory.
- Return the current workspace summary without a revision increment when the resulting step and schemas are exactly unchanged.
- Apply one JSON Patch containing all changed schema and step fields.
Stale revision must win over missing-step, wrong-step-kind, unavailable
capability, path, overlap, literal, and schema errors. The final
patch_draft_workspace call remains the mutation-time race guard.
Input Replacement
When input is present, it replaces the complete ordered input-binding list.
The operation must reuse or extract the semantic implementation behind
set_step_input_bindings; it must not maintain a second version of:
- graph source validation;
- local-target overlap detection;
- input/state schema projection;
- literal validation;
- root binding behavior;
- context target handling;
- exact ordering and fan-out preservation.
Clearing input bindings does not delete workflow input/state schema fields projected by earlier edits. This matches existing canonical replacement semantics and avoids destructive schema inference.
Metadata Mutation
Only fields present in update.model_fields_set are changed. Explicit null
removes the corresponding optional field from the canonical step, which is
dumped with optional None fields excluded. Omitted fields retain their parsed
values.
retry=0 is valid. retry<0, timeout_seconds<=0, and an explicitly supplied
empty description fail request-model validation.
Capability-Step Creation Parity
Extend add_step_from_capability with:
desc;retry;timeout_seconds;- canonical
input_bindings.
The existing input_map remains a compatibility adapter for real callers.
Supplying both input_map and input_bindings is invalid. Canonical callers
and new transports should use input_bindings.
Creation uses the same binding preflight and schema projection as update and
set_step_input_bindings. It persists the metadata directly on the new
DraftUseStep, so a caller that already knows these fields does not need a
second revision.
The operation's route and output-binding behavior remains unchanged.
JSON-RPC
Add:
workflow.draft_workspaces.update_capability_step
with parameters:
{
"workspace_id": "report",
"revision": 4,
"step_id": "publish",
"update": {
"desc": "Publish the report",
"retry": 2,
"timeout_seconds": 30,
"input": [
{
"path": "state.report.title",
"target": "request.title"
},
{
"value": "markdown",
"target": "request.format"
}
]
}
}
The client must preserve omitted fields and exact input-binding order. It
serializes the nested update with exclude_unset=True; otherwise default
None values would become unintended clear operations. The existing
add_step_from_capability request gains optional metadata and canonical
input_bindings; its compatibility input_map remains accepted but cannot be
combined with the canonical field.
MCP
Add:
wf.workflow.update_capability_step
with a typed request carrying workspace_id, revision, step_id, and nested
update.
The tool delegates once to WorkflowApi.update_capability_step. Its
description must state that:
use, routes, and outputs are preserved;- omitted fields are preserved;
- explicit metadata
nullclears; - supplied
inputreplaces the complete canonical list.
The tool belongs in the stable workflow search allow-list. Extend the existing capability-add request with the creation-parity fields.
CLI
Add a dedicated update group in a focused module:
wf draft update capability
Example:
wf draft update capability report \
--revision 4 \
--step publish \
--description "Publish the report" \
--retry 2 \
--timeout-seconds 30 \
--input state.report.title=request.title \
--value request.format='"markdown"'
Clearing:
wf draft update capability report \
--revision 5 \
--step publish \
--clear-description \
--clear-retry \
--clear-timeout \
--clear-input
CLI Rules
--descriptionand--clear-descriptionare mutually exclusive.--retryand--clear-retryare mutually exclusive.--timeout-secondsand--clear-timeoutare mutually exclusive.--bindings-fileis mutually exclusive with--input,--value, and--clear-input.--clear-inputis mutually exclusive with--inputand--value.- At least one update field or input mode is required.
- All mode errors occur before CLI context loading.
--inputmeansGRAPH_SOURCE=LOCAL_TARGETand is repeatable.--valuemeansLOCAL_TARGET=JSONand is repeatable.- Convenience flags serialize path bindings first and literal bindings second,
matching
set-input. --bindings-fileis the lossless path/value interleaving form.
Extend wf draft add capability with:
--description;--retry;--timeout-seconds;--value;--bindings-file.
Its existing --input flag remains the path-binding convenience form.
--bindings-file is mutually exclusive with --input and --value.
The update and add commands must reuse the existing canonical input flag/file parsers. They must not add a third parser for the same binding union.
Error Contract
After request-envelope validation, focused errors include:
- workspace or step not found;
- stale revision conflict;
- selected step is not capability-backed;
- empty update;
- explicit
input: null; - capability unavailable when input replacement needs its schema;
- undeclared graph source;
- overlapping local targets;
- literal target absent from the capability input schema;
- incompatible source/target schema;
- invalid metadata constraints.
All semantic errors leave the complete workspace unchanged.
Metadata-only updates succeed even when the capability source is currently unavailable because they do not require capability schema inspection.
Testing
Authoring
- Preserve omitted fields.
- Set and explicitly clear each metadata field.
- Accept
retry=0. - Reject invalid metadata constraints.
- Replace, clear, and exactly no-op canonical input.
- Apply metadata and input replacement in one revision.
- Preserve
use, output bindings, and routes. - Skip capability resolution for metadata-only changes.
- Validate capability-aware paths, literals, roots, overlaps, and schemas by the shared input-binding implementation.
- Prove stale revision wins over every semantic error.
- Prove all failures leave the workspace unchanged.
- Compile and run a draft after a combined path/literal update.
Creation
- Persist metadata at creation.
- Preserve ordered path/literal bindings.
- Accept a lossless bindings file through CLI.
- Reject simultaneous compatibility and canonical input forms.
- Preserve existing route and output behavior.
Transports
- JSON-RPC model preserves omitted fields versus explicit
null. - JSON-RPC client preserves exact canonical input order.
- MCP request validation rejects malformed updates.
- Real MCP tool invocation delegates typed update data once.
- Local and remote CLI produce equivalent operation payloads.
CLI
- Pin every set/clear exclusivity rule.
- Pin bindings-file exclusivity.
- Reject empty update before loading context.
- Verify add and update help text.
- Verify explicit metadata clears serialize as present
null.
Documentation
Update:
ISSUES.md;docs/wf_cli.md;docs/workflow_drafts.md;docs/workflow_capabilities.md;docs/wf_mcp_operator_manual.md;skills/wf-cli/SKILL.md;skills/wf-workflowdraft/lifecycle references;docs/current_roadmap.md.
Document that capability update is a focused patch, not capability replacement, and that output bindings/routes remain separate operations.
Success Criteria
- One atomic operation updates capability-step metadata and optional canonical
inputs without changing
use, outputs, or routes. - Omission, explicit clearing, replacement, and no-op semantics are verified.
- Creation can set the same metadata and canonical literal inputs directly.
- Python, JSON-RPC, MCP, and local/remote CLI expose the same behavior.
- Existing compatibility callers remain functional.
- Focused tests, Ruff, formatting, and basedpyright pass.