Authoring
Good workflow graphs make ownership visible. Start by deciding whether a node is a closed operation over known workflow state or an open-ended assignment that must leave that state to do its work.
Use
step.model()when everything needed to answer is already in the step input. Usestep.delegate()when the performer must go outside that input.
These paths can coexist in one workflow. Choose the owner separately for each piece of work; operating the workflow through a harness does not force every AI operation to become a delegation.
Decision tree
“Use a normal function” means the work is immediate and does not need durable workflow state. “Use an agent loop” means one autonomous performer should own the whole task; a chain of model calls with no meaningful durable boundaries is not improved merely by drawing it as a workflow.
Without an external harness
Use this path when application code and Workflow App surfaces operate the workflow directly. The application injects the model worker, starts runs, and presents durable questions or approvals through its SDK, CLI, TUI, GUI, HTTP, or other product surface.
Application-owned model work
step.model() resolves one injected model worker, makes exactly one structured
operation, supplies a JSON Schema from its declared output, and validates the
returned object before the graph advances. It does not itself own browsing,
tool use, conversation state, planning, subagents, or a hidden evaluator.
That boundary is an authoring rule, not a security sandbox: an injected custom worker can do arbitrary work. For new workflows, keep the model worker non-agentic and make open-ended work a visible delegation. Models and evaluation owns provider setup, worker behavior, and model lifecycle events.
Human boundaries remain available without a harness. Application code or a
Workflow App surface presents step.ask() and step.approval() to the human,
then submits the response so the durable graph can continue. No model router is
needed merely to handle a human interaction.
Do not use step.delegate() for application-owned work when no external
performer will service the resulting handoff. The run will correctly remain
waiting for that contracted external result.
With an external harness
Use this path when Codex, Claude, OpenCode, Shimex, or another harness should research, browse, use tools, manage context, or coordinate subagents. Register the workflow in a Workflow App and expose its control plane to the harness; the harness triggers or observes runs and services pending interactions.
Harness-owned delegated work
step.delegate() does not run an agent inside Dromio. It creates a durable
handoff_requested suspension containing the selected work context and output
contract. The external harness owns inference, tools, context management,
subagents, and its internal approvals; Dromio owns validation and graph
resumption. External harnesses owns the MCP
connection and lifecycle; the API reference
owns every option on the handoff contract.
Release status:
step.delegate()anddromio.resume_hookare public in@dromio/workflow0.2.5. See API status for compatibility and current limits.
import { step } from "@dromio/workflow";
import { z } from "zod";
export const revisePlan = step.delegate({
id: "fundraising.revise-plan",
input: {
gaps: z.array(z.string()),
plan: z.string(),
},
artifacts: ({ input }) => [{
artifactId: "fundraising-plan",
kind: "document",
title: "Current fundraising plan",
}],
capabilities: ["browser", "document-reading"],
context: ({ input }) => ({ gaps: input.gaps, plan: input.plan }),
instructions: "Research the identified gaps and revise the plan.",
output: {
revisedPlan: z.string(),
sources: z.array(z.string().url()),
},
title: "Revise the fundraising plan",
});
The harness reads the handoff, performs the work, and submits the typed result
through dromio.resume_hook. Dromio validates the result before advancing. A
validation failure preserves the pending handoff so the harness can correct and
retry it.
step.ask() and step.approval() still represent human boundaries. The
harness may present them to the human, but it must not infer the human's answer
or approval. An application-owned step.model() also remains valid inside a
harness-operated workflow when all evidence for that one operation is already
present in its input.
Choose the boundary
| The work needs | Use |
|---|---|
| Reasoning over complete, explicit input | step.model() |
| One schema-constrained classification, extraction, transformation, or score | step.model() |
| Browsing, external research, files not already supplied, or arbitrary tools | step.delegate() |
| Context management, multiple inference passes, or subagents | step.delegate() |
| A durable human answer or decision | step.ask() or step.approval() |
| A deterministic score-policy decision to route on | step.gate() or an ordinary deterministic step() |
Do not add tools to step.model() merely to avoid a delegation boundary. Once
the performer must discover context, choose tools, or manage its own work, it
is acting as an agent and should be visible as delegated work.
Judging results
A judge remains a good step.model() use when the plan, evidence, rubric, and
threshold inputs are all supplied to it. The model returns a typed assessment.
step.gate() then computes and emits a typed score-policy decision; workflow
composition or routing acts on that decision. The gate does not branch by
itself.
If judging requires browsing, independent verification, tool use, or multiple
specialist reviewers, make the judge another step.delegate() instead. The
score belongs in that delegate's output contract so Dromio can validate it and
route deterministically.
An execution kind controls how a node runs. A role such as assess, revise,
or judge explains what it means inside a larger workflow. The canonical
definition and table are in Composition.