Files
deepseek-harness/docs/subsystems/workflow.md
T

14 KiB

Workflow

English | 中文

The workflow seam — an agent running a model-written orchestration SCRIPT that fans out subagents. Like subagent it is one optional capability, not part of the agent-loop spine, so its vocabulary lives here rather than in core.md. Unlike the subagent registry it takes the bash shape: ONE engine implementation per context provides ctx.workflows; there is no named-provider registry (a second engine is a plugin swap, not a co-resident).

Interface: dsh-workflow (ctx.workflows + the vocabulary below). The implementation is dsh-workflow-workerthread (a node:worker_threads engine — one worker per run, the script's vm context inside it); the model-facing consumer is dsh-tool-workflow. The proposal and rationale: the dynamic-workflows Agent Note.

Source: packages/workflow/workflow/src/types.ts

The start request

What a caller asks for when starting a run. The ordinary workflow tool builds this from the model's { script, meta, args } call plus the calling agent; specialized consumers may also select one engine-wide subagentProvider and lower maxTotalAgents for the run, but the script cannot observe or replace either policy. meta and args are plain JSON DATA (the engine shape-validates meta and rejects loud BEFORE anything runs — no script text is ever evaluated to obtain it). parent is REQUIRED — every child the script spawns is attributed to it (cwd, lineage, and depth flow through the subagent seam).

/**
 * What a caller asks for when starting a workflow run. `meta` and `args` are
 * plain JSON DATA by the seam contract (the tool builds both from the model's
 * schema-validated call; the engine validates `meta`'s shape and rejects loud
 * before anything runs) — an engine never evaluates script text to obtain
 * them. `parent` is REQUIRED — every `agent()` the script spawns is
 * attributed to it (cwd, lineage, depth flow through the subagent seam).
 */
interface WorkflowStartRequest {
  /** The plain-JS script body (top-level await allowed; ends with `return <json-value>`). */
  script: string
  /** The workflow's identity block, as plain JSON data (shape-validated by the engine). */
  meta: WorkflowMeta
  /** Optional input exposed verbatim to the script as the `args` global. */
  args?: unknown
  /**
   * Optional engine-wide child-provider override for this run. The workflow
   * script cannot observe or replace it; omission uses the engine's configured
   * provider.
   */
  subagentProvider?: string
  /**
   * Optional per-run total-child ceiling. Implementations reject values above
   * their deployment ceiling before publishing the run.
   */
  maxTotalAgents?: number
  /** The agent on whose behalf the run executes (parent of every child). */
  parent: Agent
  /** Cancels the run when aborted (the tool's `exec.signal`). */
  signal?: AbortSignal
}

The workflow's identity: WorkflowMeta

The identity block carried as data on the start request (the tool's meta parameter; the field vocabulary matches the Claude Code dynamic-workflows meta block). phases is progress vocabulary only: phase() calls match titles for observers; no execution structure is implied.

/**
 * The script's identity block, provided as plain JSON data alongside the
 * script body (the model-facing tool carries it as its `meta` parameter) and
 * validated by the engine before the body runs. `name`/`description` are
 * required; the rest is optional annotation. The field vocabulary matches the
 * Claude Code dynamic-workflows meta block.
 */
interface WorkflowMeta {
  /** Short kebab-case workflow name (display + persistence key). */
  name: string
  /** One-line description of what the workflow does. */
  description: string
  /** Optional guidance on when this workflow applies (shown in listings). */
  whenToUse?: string
  /** Optional phase declarations matched by `phase()` calls. */
  phases?: WorkflowPhase[]
}

The terminal result: WorkflowResult

The outcome of one run, resolved by WorkflowRun.result. value is the script's materialized return value — plain host-realm JSON data (null when the script returned nothing) — meaningful only for completed. stopReason is a CLOSED union (engine-owned; consumers may exhaust it): completed | cancelled | error. A non-completed reason carries the failure in error, and the consumer maps it to an isError tool result rather than reporting partial output as success.

/**
 * The outcome of one run, resolved by {@link WorkflowRun.result}. `value` is
 * the script's materialized return value (plain host-realm JSON data; `null`
 * when the script returned `undefined`) — meaningful only for `completed`.
 * A non-`completed` reason carries the failure in `error`; the consumer maps
 * it to an `isError` tool result rather than reporting partial output.
 */
interface WorkflowResult {
  /** The script's return value (host JSON data; `null` for no return). */
  value: unknown
  /** Why the run settled. */
  stopReason: WorkflowStopReason
  /** The failure message (present iff `stopReason` is not `completed`). */
  error?: string
  /**
   * How many `agent()` calls the run accepted over its whole lifetime. On a
   * graceful settlement this is the script-side count (calls still queued for
   * a concurrency slot included); on a termination path (grace force-settle,
   * worker death) it degrades to the host-observed count — calls queued
   * inside a terminated script are unknowable then.
   */
  agentsStarted: number
}

A live run: WorkflowRun

The handle the consumer holds while a script executes. The consumer awaits result, may cancel mid-flight, and MUST dispose on every path. result does NOT reject — a script failure resolves with stopReason: 'error' — and once the run is cancelled it SETTLES within the engine's bounded grace even if the script itself never settles (the engine force-settles cancelled; the worker-thread engine then terminates the script's worker), so a consumer awaiting result is never wedged past a cancellation. dispose() = cancel + that bounded settle + child quiescence; it never hangs on a stuck script.

/**
 * Holder-owned live workflow. `result` never rejects and settles within the
 * engine's cancellation grace; failures resolve through `stopReason`. Consumers
 * may cancel and must call idempotent `dispose()` on every path to await bounded
 * script settlement and child quiescence.
 */
interface WorkflowRun {
  readonly id: WorkflowRunId
  /** The validated meta block (available before the body runs). */
  readonly meta: WorkflowMeta
  readonly result: Promise<WorkflowResult>
  /** Cancel the run: children abort, pending hooks reject, the script dies at its next await (or is force-settled at the grace). */
  cancel(reason?: string): void
  /** Cancel + bounded-grace settle; safe to call on every path (idempotent). */
  dispose(): Promise<void>
}

Failure discipline: WorkflowError.fatal

Hook misuse inside a script — bad arguments, unknown/deferred agent() options, a schema outside the structured-output subset, a tripped cap, a seam start failure, cancellation — throws a WorkflowError with fatal: true. The parallel()/pipeline() combinators RE-THROW fatal errors instead of mapping the item to null: a typo'd option must kill the script loudly, never dissolve into something that reads as an ordinary child failure. The per-item null is reserved for child-run failures (a non-completed stop reason) and ordinary in-stage script errors.

Events

The workflow/* events (workflow/start, workflow/phase, workflow/log, workflow/agent-start, workflow/agent-end, workflow/end — see the events catalog) are observe-only emits carrying DATA SNAPSHOTS: every payload starts with WorkflowRunInfo (id + meta), never the live WorkflowRun, so a subscriber cannot gain cancel/dispose, and workflow/end deliberately omits the result value (a listener observing outcomes must not receive a mutable alias of the caller's result). Every emit is per-listener contained — a throwing subscriber is logged, never propagated, and cannot starve the listeners registered after it — and every listener receives its own payload clone, so mutating it corrupts neither the engine nor other listeners; the containment mirrors subagent/start/subagent/end.

Cordis surface

Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — this section is byte-identical in both language sides of the page. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx surface lives in cordis-api/inherited.md.

ctx.workflowsWorkflowService

Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and workflow/end fires exactly once as the result settles.

/**
 * Parse and execute a workflow script.
 * @param request - the script, its `args`, the parent agent, and an
 *   optional cancel signal.
 * @returns the live run; its `result` resolves when the script settles.
 */
abstract start(request: WorkflowStartRequest): WorkflowRun

Source: packages/workflow/workflow/src/index.ts:159

workflow/* events

workflow/agent-end — emit

One agent() call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by agent.seq, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome 'cancelled'.

/**
 * One `agent()` call settled (clean result, child failure, or run
 * cancellation). Paired with {@link Events['workflow/agent-start']} by
 * `agent.seq`, exactly once per started call on every stop path — on an
 * engine termination path (a worker killed past its grace) the end is
 * engine-synthesized with outcome `'cancelled'`.
 * @param info - the run's identity snapshot.
 * @param agent - the call identity plus its outcome.
 * @mode emit
 */
'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void

Source: packages/workflow/workflow/src/index.ts:81

workflow/agent-start — emit

One agent() call established a published child run. Paired with Events['workflow/agent-end'] by agent.seq. A call that never receives a published run from the provider emits neither event in this pair.

/**
 * One `agent()` call established a published child run. Paired with
 * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never
 * receives a published run from the provider emits neither
 * event in this pair.
 * @param info - the run's identity snapshot.
 * @param agent - the call's sequence number, label, phase, and child id.
 * @mode emit
 */
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void

Source: packages/workflow/workflow/src/index.ts:70

workflow/end — emit

A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start'].

/**
 * A workflow run settled (any stop reason). Fired when
 * {@link WorkflowRun.result} resolves. Paired with
 * {@link Events['workflow/start']}.
 * @param info - the run's identity snapshot.
 * @param result - the outcome data (stop reason, error, agent count) —
 *   deliberately WITHOUT the result value (see {@link WorkflowResultInfo}).
 * @mode emit
 */
'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void

Source: packages/workflow/workflow/src/index.ts:91

workflow/log — emit

The script emitted a narration line (a log(message) call).

/**
 * The script emitted a narration line (a `log(message)` call).
 * @param info - the run's identity snapshot.
 * @param message - the logged message, verbatim.
 * @mode emit
 */
'workflow/log'(info: WorkflowRunInfo, message: string): void

Source: packages/workflow/workflow/src/index.ts:60

workflow/phase — emit

The script entered a phase (a phase(title) call) — progress grouping for observers; no execution semantics.

/**
 * The script entered a phase (a `phase(title)` call) — progress grouping
 * for observers; no execution semantics.
 * @param info - the run's identity snapshot.
 * @param title - the phase title, verbatim.
 * @mode emit
 */
'workflow/phase'(info: WorkflowRunInfo, title: string): void

Source: packages/workflow/workflow/src/index.ts:53

workflow/start — emit

A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end'].

/**
 * A workflow run started — the script's meta block validated, the body
 * about to execute. Paired with {@link Events['workflow/end']}.
 * @param info - the run's identity snapshot (id + meta).
 * @mode emit
 */
'workflow/start'(info: WorkflowRunInfo): void

Source: packages/workflow/workflow/src/index.ts:45