Skip to main content
Subflow is a top-level export, unlike the typed components createSmithers(...) returns (Workflow, Task, Approval, Sandbox, Signal, Branch, Loop, Parallel): import it directly from "smthrs" for a child workflow boundary. The workflow prop takes a SmithersWorkflow value: a workflow file’s default export. Since smithers((ctx) => ...) returns one, a child workflow is just another createSmithers module:
For a workflow module generated at runtime, pass a WorkflowFileRef instead: it must default-export a SmithersWorkflow, and its resolved path (including symlinks) must stay within approvedRoot (default: the parent run’s rootDir):
The second argument to smithers(build, { output }) declares which schema becomes the child’s RunResult.output, the value a parent <Subflow> persists: pass createSmithers(...).outputs.<key> (a string key or Drizzle table also works). Without it, the child result reads only from a schema key literally named output; a child writing elsewhere (like childResult here) yields an empty subflow result. Import that value into the parent and pass it to <Subflow workflow={...}>; the parent declares the schema it persists the result under (key need not match the child’s, but the shape must):
The child reads the input you pass via its own ctx.input, typed by giving the child’s createSmithers an input schema (above); without one, ctx.input is untyped and needs a guard per field (ctx.input?.repo ?? "."). Read the persisted subflow result in the parent like a task output: ctx.outputMaybe(parent.outputs.childSummary, { nodeId: "run-child" }), where nodeId is the <Subflow id>.

What childRun persists

With mode="childRun" (the default), <Subflow output> stores the child’s normalized RunResult.output: its last task result, not a table-keyed snapshot. When the child run finishes, the engine reads the rows written to its declared result schema (the smithers(build, { output }) argument above), strips the system columns (runId, nodeId, iteration), and normalizes:
  • One row (the common case: the child’s final task is the sole writer) unwraps to that plain object, like { summary: "..." } above.
  • Zero rows normalize to null.
  • Multiple rows (several tasks or loop iterations writing to the schema) persist as an array of stripped rows.
The parent gets one result value, not a { schemaKey: rows } map, validated against <Subflow output> like a task payload, so adding or changing the child’s final task changes the parent’s expected schema: renaming output columns, pointing the result at a different schema, or adding a writer (one row becomes an array) all change the shape the parent receives. Update <Subflow output> in the same change, or the subflow task fails output validation.

Single-file parent and child

For small examples and eval fixtures, define both factories in one .tsx file with separate createSmithers calls, so the child keeps its own ctx.input type and output refs, and the parent keeps its own output table for the subflow result:
The parent’s input schema is optional but recommended whenever it reads ctx.input.repo, turning ctx.input from unknown into a typed object; the child has its own input schema and ctx.input, populated from the parent’s <Subflow input={...}> prop.

Notes

  • childRun (default) gives the child its own DB row for retry/cache/resume scope; the persisted result is the child’s last task row, not a table-keyed snapshot (see What childRun persists).
  • inline renders the child tree as siblings in the parent plan, sharing its scope.
  • Subflows compose; children may contain <Subflow> themselves.
  • Declare the child’s result via smithers(build, { output }) unless its schema is named output; that’s what a parent <Subflow> persists.