Files
lda-wf/docs/superpowers/specs/2026-07-01-self-describing-interrupt-contracts.md
T

10 KiB

Self-Describing Interrupt Contracts

Date: 2026-07-01

Status: Implemented. This document is the live interrupt contract.

Related:

Purpose

Make human-in-the-loop workflow pauses self-describing. A client inspecting an interrupted run should know:

  • what kind of interrupt occurred;
  • what payload was sent to the user;
  • which resume outcomes are valid;
  • what JSON shape a resume payload must have;
  • how to validate the response before sending resume_run.

This is required for the Workflow Console and useful for CLI, JSON-RPC, MCP, and agent clients. It prevents every client from needing workflow-specific code just to render and answer an approval step.

Implemented Contract

The core interrupt model now exposes a complete public pause/resume contract.

  • InterruptNode stores kind, request bindings, resume bindings, and resume outcomes, plus request_schema and resume_schema.
  • Runtime builds an InterruptRequest with id, frame id, node id, kind, payload, route, resumability, outcomes, request schema, resume schema, and a typed marker.
  • workflow.runs.inspect serializes that persisted interrupt request.
  • Runtime validates request payloads before pausing and validates resume payloads against the persisted pause-time schema before mutating state.

This is close to the LangGraph-style interrupt(value) and Command(resume=...) pattern: flexible and simple, but the response contract is mostly app convention. wf should keep the flexibility while making the contract explicit.

Design Summary

JSON Schema contracts are carried from interrupt nodes through persisted run inspection and resume validation.

sequenceDiagram
  participant Runtime
  participant Store
  participant Client

  Runtime->>Runtime: Build request payload from interrupt.request bindings
  Runtime->>Runtime: Validate payload against request_schema
  Runtime->>Store: Persist interrupted run checkpoint
  Client->>Store: inspect_run(run_id)
  Store-->>Client: payload, outcomes, request_schema, resume_schema
  Client->>Client: Render form from resume_schema
  Client->>Store: resume_run(run_id, payload, outcome)
  Store->>Runtime: Validate resume payload against resume_schema
  Runtime->>Runtime: Apply interrupt.resume bindings

Core Model

InterruptNode carries two schema fields:

class InterruptNode(BaseModel):
    id: str
    type: Literal["interrupt"]
    kind: str
    request: list[InputBinding] = Field(default_factory=list)
    resume: list[OutputBinding] = Field(default_factory=list)
    outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
    request_schema: JsonSchemaObject = Field(default_factory=_object_schema)
    resume_schema: JsonSchemaObject = Field(default_factory=_object_schema)

The default schema is a permissive object schema:

{
  "type": "object",
  "additionalProperties": true
}

This preserves existing persisted workflow documents and current examples. New authoring helpers should emit explicit schemas whenever they can.

Runtime Request Contract

When an interrupt node executes:

  1. Build the request payload using existing request bindings.
  2. Validate the built payload against request_schema.
  3. If validation fails, fail the run with a structured execution error. This is a workflow contract bug, not a human pause.
  4. Persist the interrupted run with an InterruptRequest that includes:
    • interrupt id;
    • frame id;
    • node id;
    • kind;
    • payload;
    • resumable flag;
    • child interrupt route when present;
    • outcomes;
    • request schema;
    • resume schema;
    • typed=true when the schema was explicitly declared.

The runtime should not infer request schemas from payload values. Inference would make the public contract depend on one execution instance rather than the workflow definition.

Resume Contract

Before mutating run state, resume_run validates:

  1. the run is currently interrupted;
  2. the selected resume outcome is declared by the interrupt node;
  3. the resume payload satisfies the interrupt node's resume_schema.

Only after those checks pass should the runtime apply resume bindings and advance the graph. Invalid resume payloads must not consume the interruption or write partial state changes.

Resume validation should use the same JSON Schema validation helper used by workflow validation where possible. Do not hand-roll schema validation.

Public Inspect Shape

workflow.runs.inspect should expose the interrupt contract directly:

{
  "status": "interrupted",
  "interrupt": {
    "id": "interrupt:review",
    "frame_id": "frame_1",
    "node_id": "review",
    "kind": "issue_review",
    "payload": {
      "report_markdown": "# lda.chat Thesis And Project Readiness Report",
      "proposed_issues": []
    },
    "outcomes": ["submitted", "cancelled"],
    "request_schema": {
      "type": "object",
      "properties": {
        "report_markdown": {"type": "string"},
        "proposed_issues": {"type": "array"}
      },
      "required": ["report_markdown", "proposed_issues"]
    },
    "resume_schema": {
      "type": "object",
      "properties": {
        "approved": {"type": "boolean"},
        "selected_issue_ids": {
          "type": "array",
          "items": {"type": "string"}
        },
        "comment": {"type": "string"}
      },
      "required": ["approved", "selected_issue_ids"],
      "additionalProperties": false
    },
    "typed": true,
    "resumable": true
  }
}

The schema snapshot is part of the interrupted run's public contract. A client should not need to reload the mutable artifact or inspect Python source code to answer the interrupt.

Authoring Contract

Raw workflow plans may specify request_schema and resume_schema directly on interrupt nodes.

Authoring helpers may provide convenience wrappers:

  • direct JSON Schema dictionaries;
  • Pydantic models converted through model_json_schema();
  • a small built-in helper for common approval forms.

The stored workflow should still contain ordinary JSON Schema dictionaries. Python type objects must not leak into persisted artifacts.

The first planned demo uses:

  • kind="issue_review";
  • a request schema containing rendered report markdown and proposed issues;
  • a resume schema containing approval, selected issue ids, and optional comment;
  • outcomes submitted and cancelled.

Validation Contract

Workflow validation should catch static interrupt contract errors:

  • request_schema and resume_schema must be valid JSON Schema documents;
  • schemas must describe JSON objects for V1;
  • request binding targets must be valid local payload paths;
  • resume binding sources must be valid local payload paths;
  • resume binding destinations must write to declared state fields;
  • every declared resume outcome that is routed must be known by the node.

V1 does not need full static proof that every request binding output satisfies request_schema. Runtime validation still protects execution. Static validation can become stricter later if it remains useful and maintainable.

CLI And RPC Behavior

CLI and RPC should surface schema failures as ordinary structured product errors, not Python tracebacks.

Recommended behavior:

  • wf run inspect <run_id> includes the interrupt schemas in JSON output.
  • Compact/text output summarizes kind, valid outcomes, and required resume fields.
  • wf run resume <run_id> --input ... validates before mutation and reports missing/invalid resume fields.
  • wf explain gains cards for interrupt request-schema and resume-schema validation failures if new diagnostic codes are added.

Compatibility

Existing workflows without explicit schemas remain valid.

  • Missing request_schema or resume_schema is treated as a permissive object schema.
  • Public inspect can include typed=false for legacy interrupts.
  • New authoring helpers and examples should emit explicit schemas.
  • No migration is required for stored artifacts.

Compatibility here is justified because raw workflow plans and saved artifacts are a documented external/persisted contract.

Workflow Console Usage

The Workflow Console uses this contract to render generic human approval forms:

  1. inspect the interrupted run;
  2. read interrupt.kind and interrupt.resume_schema;
  3. choose a kind-specific renderer when available;
  4. fall back to a generic JSON Schema form;
  5. validate locally for user feedback;
  6. send the resume payload through JSON-RPC;
  7. display server-side validation errors if the schema check still fails.

The console may provide a custom renderer for issue_review, but the custom renderer must still emit a payload accepted by resume_schema.

Non-Goals

  • New interrupt execution semantics.
  • Multi-user approval workflow.
  • Authentication, authorization, or audit identity.
  • Scheduling or event-triggered resumes.
  • A full JSON Schema UI standard.
  • Semantic compatibility analysis between schema revisions.
  • LangGraph API compatibility.

Implementation Slices

  1. Extend core models and persistence-safe serialization.
  2. Validate request and resume schemas using the existing schema validation library path.
  3. Carry interrupt schemas and outcomes into InterruptRequest and workflow.runs.inspect.
  4. Validate resume payloads before state mutation.
  5. Add authoring/builder helpers and schema discovery output.
  6. Update CLI docs, skills, and examples.
  7. Add the issue_review interrupt to the prepared lda.chat report workflow.

Test Plan

  • Core model accepts explicit schemas and defaults legacy nodes.
  • Invalid interrupt schemas are rejected by workflow validation.
  • Runtime validates a request payload before persisting interruption.
  • Resume rejects invalid payloads without mutating run state.
  • Resume rejects unknown outcomes before mutating run state.
  • workflow.runs.inspect includes schemas, outcomes, typed flag, and payload.
  • JSON-RPC and CLI resume return structured errors for invalid payloads.
  • Draft/raw-plan compilation preserves interrupt schemas.
  • Builder/Pydantic convenience emits plain JSON Schema in saved workflows.

Success Criteria

The contract is complete when an external client can inspect an interrupted run, render a correct form, submit a valid resume payload, and explain invalid responses without reading workflow source code or hard-coding that workflow's interrupt shape.