Editorial Workflows

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

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.

Loading...

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; forEach sees workflow-scope fields only.

  • Caller-fired action

    $actor, $assigned

    Available when the spawning action is caller-fired.

  • Unavailable to forEach and context.

  • 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?

Was this page helpful?