Subworkflows
How a large process composes out of smaller ones: an action spawns a child workflow per row of a query, and a trigger resolves the parent’s activity once they all settle.
Prerelease
Editorial Workflows is a prerelease, built in public. Read How the prerelease works before you rely on it.
A subworkflow is a workflow instance created by another workflow. Use subworkflows when one parent process must start the same smaller process for several items and wait for those children to settle.
For example, a localization workflow can start one translation workflow per locale. Each translation runs independently, while the parent tracks the group and continues when no child remains active.
Spawn a child for each row
A spawn declaration belongs to an action. Its forEach expression returns one identifiable row per child, and with maps values from that row into the child’s input fields. A definition used only this way declares lifecycle: 'child', so it cannot be started on its own.
defineActivity({
name: 'translate',
actions: [
{
name: 'fan-out',
when: 'true', // spawn on stage entry
spawn: {
forEach: '*[_id == $fields.subject._id][0].locales', // one child per locale on the article
definition: {name: 'translate-article'}, // which workflow to spawn
with: {
locale: '$row',
// a doc.ref read resolves to the referenced document, so rebuild the {id, type} pair
source: '{"id": $fields.subject._id, "type": $fields.subject._type}',
},
},
},
{
name: 'all-done', // no implicit gate: resolve once no child in the cohort is still active
when: "count($subworkflows[activity == 'translate' && current && status == 'active']) == 0",
status: 'done',
},
],
})Wait for the cohort
Spawning and waiting are separate actions. The engine settles every child created by one spawn commit before the parent reacts to that cohort. The all-done trigger then resolves the parent activity only when no child remains active.
Each child produces one cohort row. current selects children from the open stage visit. status distinguishes active, done, and aborted children. Both done and aborted are settled, so require status == 'done' when the parent must continue only after every child succeeds.
The complete sequence now follows from the definition above. The engine creates the child cohort in one commit, child actions update their own instances, and each child commit propagates to the parent until the completion trigger resolves the activity.
Spawn on demand
An action without when spawns on demand instead: nothing happens at stage entry, and the cohort materializes in the commit that fires the action. The gate needs one more clause for this shape. Before the caller fires, the current cohort is empty, so a bare settled count passes because there is nothing left to count, and would resolve the activity before anything spawns; require a non-empty cohort first.
{
name: 'kick-off', // no when: spawns when a caller fires it
spawn: {/* same declaration as above */},
},
{
name: 'all-done',
// an empty cohort counts as settled, so require it exists first
when:
"count($subworkflows[activity == 'translate' && current]) > 0 && " +
"count($subworkflows[activity == 'translate' && current && status == 'active']) == 0",
status: 'done',
},Choose what happens on exit
If the stage exits while children are still running, onExit on the spawn declaration decides their fate. 'detach', the default, lets them finish on their own, outside the gate; 'abort' cancels them, recursively. Aborting the parent instance always cascades to its children.
Spawn inputs are checked at deploy
Every key in spawn.with must name a child workflow field that can consume an input value. Deploy rejects missing fields and fields whose initial value does not come from input, reporting the invalid parent-to-child contracts together as SpawnContractsInvalidError. This prevents a deployed parent from silently handing the child data it would ignore.
Reference
Spawn declaration
Set this object as an action’s spawn property. When that action fires, the engine creates the declared child workflow instances in the same commit.
forEach
Condition
Required GROQ expression producing one identifiable row per child.
definition
{name, version?}
Required deployed child definition; version defaults to latest.
with
Record<string, Condition>
Optional initial child fields evaluated per
$row.context
Record<string, Condition>
Optional parent values delivered to each child as
$context.onExit
detach | abort
What happens to live children when their cohort leaves scope; defaults to
detach.
Expression scope
Always available
$self, $fields, $parent, $ancestors, $stage, $now, $context, $effects, $effectStatus, $activities, $allActivitiesDone, $anyActivityFailed, $subworkflows
All spawn expressions;
forEachsees workflow-scope fields only.Caller-fired action
$actor, $assigned
Available when the spawning action is caller-fired.
with only
$row
Unavailable to
forEachandcontext.Unavailable
$can, $params
Never bound at spawn sites. See Conditions.
Cohort row
interface SubworkflowVarRow {
rowKey: string
activity: string
action: string
definition: string
current: boolean
status: 'active' | 'done' | 'aborted'
// …
}Visiting agent?
Editorial Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.

