Skip to content

Spawn Workflow Steps

The spawn step type is used for fan-out workflow execution. A parent workflow creates one child workflow instance per row in a line_items form field, optionally waits for their completion, then routes to the next step.

Typical uses: bulk reimbursement requests, multi-vendor invoice processing, onboarding task assignment, or any process where the number of sub-approvals is determined at submit time.

spawn_individual_approvals:
name: Process Individual Reimbursements
type: spawn
workflow: individual-reimbursement-approval # child workflow name
items: reimbursement_items # line_items field on the parent form
wait_for: all
pass:
- department
- reimbursement_date
map:
employee_name: employee_name
description: description
amount: amount
return:
approved_amount: approved_amount # child field → parent item row field
approver_name: approver_name
on_complete:
continue_to: completed
on_failure:
end_workflow: true

| Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | workflow | String | Yes | Name of the child workflow to spawn (looked up by name in the same company). | | items | String | Yes | Name of the line_items form field whose rows drive the fan-out. One child is created per row. | | wait_for | String | No | Fan-in strategy: all (default), any, or none. | | pass | List | No | Top-level parent form fields to copy verbatim into every child. | | map | Dict | No | Explicit field renames: child_field: source_field. Source is resolved from parent top-level first, then from the current item row. | | return | Dict | No | Values copied back from each completed child into the parent item row: child_field: row_field. | | on_complete | Dict | No | Routing when all (or any) children finish successfully. | | on_failure | Dict | No | Routing when a child is rejected. |

| Value | Behaviour | | :--- | :--- | | all (default) | Parent pauses until every child instance completes. | | any | Parent advances as soon as the first child completes. | | none | Fire-and-forget — parent advances immediately after spawning. Children continue independently. |

Each child's form data is assembled from three sources in priority order. A field set by an earlier source is never overwritten by a later one.

| Priority | Source | Description | | :--- | :--- | :--- | | 1 (highest) | map: entries | Explicit renames. Source is resolved from the parent's top-level request_data first; if not found there, from the current item row. | | 2 | pass: fields | Copies parent top-level fields by the same name into the child. Skipped if the key was already set by map:. | | 3 (lowest) | Item row keys | Every key in the current line item row is merged in, filling any slots not already set by map: or pass:. |

Parent form fields:

  • department (text) — global context
  • reimbursement_date (date) — global context
  • reimbursement_items (line_items) — rows with employee_name, description, amount, category

Spawn step:

map:
employee_name: employee_name # explicit (identical source/dest — optional)
description: description
amount: amount
pass:
- department
- reimbursement_date

Child instance for row {employee_name: "Alice", description: "Hotel", amount: 250, category: "accommodation"}:

{
"employee_name": "Alice",
"description": "Hotel",
"amount": 250,
"department": "Engineering",
"reimbursement_date": "2026-06-01",
"category": "accommodation"
}

category arrives via step-3 auto-merge even though it is not listed in map: or pass:.

The items: property must reference a line_items field on the parent form. Each row in that field becomes one child instance.

form:
fields:
- name: reimbursement_items
type: line_items
label: Reimbursement Line Items
required: true
min_items: 1
item_fields:
- name: employee_name
type: text
label: Employee Name
required: true
- name: amount
type: currency
label: Amount
required: true
currency: USD
validation:
min: 0.01
max: 100000
- name: category
type: dropdown
label: Category
required: true
options:
- value: travel
label: Travel
- value: meals
label: Meals

See Field Types Reference for the full line_items schema.

If the child workflow named in workflow: does not exist, the Validate button in the Workflow Designer shows an error with a Generate & Open Child Workflow button.

Clicking it creates a new child workflow pre-populated with:

  • All fields from map: targets — with correct type, required, currency, and validation carried over from the parent's item_fields
  • All fields from pass: — with their full definitions copied from the parent form
  • Any remaining item_fields not covered by map: or pass:, also with full definitions

This means a currency field with validation: {min: 0.01, max: 100000} will appear as a currency field (not text) in the generated child workflow.

While children are running, the parent instance holds a system:spawn_coordinator step in PENDING state. This step stores:

  • items_field — the line_items field name
  • workflow_name — the child workflow
  • wait_for — the configured fan-in strategy
  • return — optional child-to-parent field mapping
  • on_complete / on_failure — routing for when the children resolve

When the coordinator is resolved to APPROVED, any return mapping is applied first: child field values are written back into the corresponding rows of the parent's items field. The parent workflow then advances.

Use the return block to copy values produced by each child workflow back into the parent row that spawned it.

return:
approved_total: total_approved # child.request_data.approved_total → row.total_approved
approver_name: approved_by # child.request_data.approver_name → row.approved_by

Rules:

  • return is applied only on a successful fan-in (on_complete). It is ignored when children fail.
  • The left-hand key is a field name in the child workflow's request_data.
  • The right-hand value is a field name in the parent item row.
  • Only rows with a completed child are updated. With wait_for: any, rows whose children did not finish are left unchanged.
  • Missing or null child values do not overwrite existing row values.

This is useful for aggregating per-child outcomes (e.g. the approved amount, assigned approver, or final status) back into the parent's line items for reporting or downstream steps.

Users who can view the parent workflow instance automatically have visibility into all its child instances. This applies to:

  • The employee who submitted the parent request
  • Managers (by org hierarchy) of the submitter
  • Approvers who acted on any parent step
  • Anyone with a view_all_roles company role on the parent workflow

This means a finance manager who approved the parent bulk-reimbursement request can open each individual child reimbursement approval without needing a separate permission grant.

In test mode (is_test_mode: true in instance metadata), the spawn step logs what it would do — item count, field mappings — without creating real child instances. The coordinator step is created as APPROVED immediately, and the parent advances via on_complete routing. This lets you test parent-level logic without needing the child workflow to exist.

| Scenario | Behaviour | | :--- | :--- | | items field is an empty list | Coordinator resolves immediately (APPROVED); zero children created; parent follows on_complete. | | wait_for: none | All children are created, then the coordinator resolves immediately; parent does not wait. | | Child workflow not found | Parent follows on_failure routing (or is rejected if on_failure is not set). | | map: source key missing in both parent and item row | That child field is left unset (not set to null), allowing item_fields auto-merge to fill it. |