DromioWorkflow SDK
Workflow SDKAuthoringAPIWorkflow Apps & Harnesses

Design the graph

Decision treeStepsWorkflowsCompositionModels and judgmentHuman boundaries
Design the graph
Decision tree
Steps
Workflows
Composition
Models and judgment
Human boundaries

Steps

A step is a visible executable unit with stable identity, input and output contracts, and an observable attempt lifecycle. Use one when progress, retry, failure, human judgment, model work, or a side effect should be visible.

Define a contracted step​

import { step } from "@dromio/workflow";
import { z } from "zod";

export const saveSummary = step({
id: "notes.save-summary",
input: {summary: z.string().min(1)},
output: {path: z.string().min(1)},
sideEffects: ["filesystem.write"],
maxRetries: 2,
async run({artifacts, input, step: attempt}) {
const path = `summaries/${attempt.idempotencyKey}.txt`;
await Bun.write(path, input.summary);
artifacts.file({mediaType: "text/plain", path});
return {path};
},
});

Returning the declared output completes the step. The SDK validates inputs and outputs, emits attempt events, and registers artifacts. Unexpected exceptions reject execution. Advanced steps may return done(), ask(), wait(), retry(), goto(), or fail() from @dromio/workflow/core.

Every step.* primitive​

APIUse it forSuspends
step()App-owned contracted behavior or side effectsOnly if its advanced result does
step.model()One visible structured model callNo
step.gate()Deterministic decision from evaluation and policyNo
step.delegate()External-harness inference, tools, browser use, or subagentsYes
step.ask()Human question with validated answerYes
step.approval()Typed approval request and decisionYes
step.waitFor()Typed external signalYes
step.sleep()Duration or absolute-time timerYes
step.workflow()One independently meaningful child workflowIf the child waits
step.router()Exactly one compatible child selected at runtimeIf the child waits
step.fork()Two or more independent child workflowsIf a branch waits
step.forEach()One child workflow per itemIf an iteration waits

step.evaluate() and step.promptedContract() remain supported compatibility wrappers, but they are bundled workflows rather than primitives. New code uses workflow.judge() and workflow.clarifyUntil(). There is no generic step.writeJson() root API.

Delegate work to a harness​

step.delegate() is a primitive with the same visible step identity and contracts as any other definition, but it suspends rather than performing the work in process. Choose it only when an external harness must leave the explicit input to gather context, use tools, or manage an agentic task. The Authoring decision rule explains when to use it; External harnesses owns MCP connection and resumption; the API reference owns its exact input fields.

Continue to Models and evaluation for model, gate, and prompted-contract examples; Human input and waits for ask, approval, signal, and timer steps; Composition for child primitives; and the Authoring API reference for exact option summaries. Runtime result helpers and session control are covered in Execution and durability.

Contracts and data flow​

input and output accept operation contracts or compatible sources such as Zod schemas. Keys are the default data-binding names used as state moves through document edges:

input: {
generatedResponse: generatedResponseSchema,
prompt: promptSchema,
},
output: {
assessment: assessmentSchema,
},

A step receives values for its declared input keys from accumulated workflow state. A completed output is merged into state. Use document node bindings when a placement intentionally renames an input or output key; do not wrap everything in an untyped payload to avoid explicit mappings.

Configuration and placement​

Reusable definition identity is separate from placement identity. A document node references catalogItemId and supplies its own id and optional config. The runtime calls config.resolve(defaults, placement) once when creating the placement:

const format = step({
id: "notes.format",
config: {
defaults: {prefix: "Note"},
resolve: (defaults, placement) => ({
prefix: typeof placement.prefix === "string"
? placement.prefix
: defaults.prefix,
}),
},
input: {summary: z.string()},
output: {formatted: z.string()},
run: ({config, input}) => ({formatted: `${config.prefix}: ${input.summary}`}),
});

Workflow configuration is merged first, then node configuration overrides it.

Catalog metadata​

The executable definition is also its catalog item. Optional metadata includes label, description, capabilities, intents, sideEffects, tags, verbs, runtime dependencies, configuration requirements, prompts, and examples. Do not maintain a second manifest that can drift from the code.

Retry and failure behavior​

maxRetries limits explicit retry(reason) results; the default is one retry. Each attempt has its own trace span and idempotency key. fail(message) returns a failed session and emits failure events. A thrown exception rejects the run with the step ID and attempt attached. The SDK does not automatically retry arbitrary exceptions or make external side effects idempotent.

Choose a step boundary​

Create a step when a user or operator would name it, its progress or failure matters, it invokes a model or human, it decides control flow, it has a reusable contract, or it owns an important side effect. Keep local parsing and formatting inside the owning step when they have no independent lifecycle.

A graph of model calls alone is usually a prompt chain. Step boundaries earn their keep when inference is punctuated by durable waits, human decisions, deterministic gates, parallel work, idempotent side effects, or other observable control boundaries.

On this page

  • Define a contracted step
  • Specialized primitives
  • Contracts
  • Catalog metadata
  • Step boundary