Composition steps
Composition steps let one workflow invoke another. Use them to build reusable workflow building blocks, break long workflows into testable pieces, and fan out to background jobs.
Composition steps (workflow.execute, workflow.executeAsync, workflow.output, workflow.fail) are in technical preview in 9.4. The parameter shapes can change in future releases. Use for prototypes and reusable utility workflows; hold off on critical paths until composition reaches GA.
| Problem | Why composition helps |
|---|---|
| You're copying the same steps across many workflows. | Extract them into a shared child workflow and call it. |
| A long workflow is hard to test end to end. | Break it into smaller pieces and test each one in isolation. |
| You need to fan out to N background jobs. | Use workflow.executeAsync to fire and forget. |
| Different teams own different parts of a process. | Each team maintains their own child workflow. |
A parent calls a child with workflow.execute (synchronous) or workflow.executeAsync (fire-and-forget). The child is a normal workflow with two additions:
- It declares its outputs in the top-level
outputsfield so callers know what to expect. - It emits outputs with
workflow.output, or terminates withworkflow.failon error.
The engine validates child outputs against the declared schema before returning them to the caller, so the parent can reference {{ steps.<child>.output.<field> }} without guarding against missing keys.
| Use | When |
|---|---|
workflow.execute (synchronous) |
The parent needs the child's result to continue. |
workflow.executeAsync (asynchronous) |
Fire and forget. Notifications, logging, fan-out to background workers. |
The execution engine enforces a maximum composition depth to prevent infinite recursion. If a child tries to invoke a grandchild that would exceed the limit, the step fails at the depth check. A workflow also cannot call itself directly.
Run a child workflow synchronously. The parent waits for the child to finish and receives its validated outputs.
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
workflow-id |
with |
string | Yes | Name of the child workflow to run. Use the child's name field, not an internal saved-object ID. |
inputs |
with |
object | No | Typed inputs passed to the child. Must match the child's inputs declaration. |
- name: enrich_alerts
type: workflow.execute
with:
workflow-id: "shared--enrich-alerts"
inputs:
alerts: "${{ event.alerts }}"
- name: use_enrichment
type: cases.createCase
with:
title: "Threat: {{ steps.enrich_alerts.output.enrichment_stats.top_indicator }}"
Both workflow-id and inputs live inside the with block. workflow-id is the only kebab-case parameter in workflows that sits inside with rather than at the step top level.
Start a child workflow without blocking. Fire and forget. Use for fan-out patterns where the parent doesn't need the child's result.
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
workflow-id |
with |
string | Yes | Name of the child workflow to run. |
inputs |
with |
object | No | Typed inputs passed to the child. |
- name: notify_async
type: workflow.executeAsync
with:
workflow-id: "shared--post-to-slack"
inputs:
message: "Processing started"
channel: "#soc-oncall"
The parent continues immediately. The child runs on its own, and its success or failure is independent of the parent.
Emit the final outputs of a workflow. Outputs are validated against the workflow's declared outputs schema. An optional top-level status field controls the execution's terminal state.
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
status |
top level | string | No | completed (default), cancelled, or failed. |
| output values | with |
object | Yes | Output values that match the declared output schema. |
outputs:
- name: enriched_documents
type: object
- name: enrichment_stats
type: object
steps:
# ...enrichment logic...
- name: return_result
type: workflow.output
with:
enriched_documents: "${{ steps.enrich.output }}"
enrichment_stats: "${{ steps.stats.output }}"
workflow.output is terminal. Once it runs, the workflow reports its outputs to the caller and exits.
Stop the workflow with a failed terminal state. Useful for short-circuiting when input validation fails or a required condition is not met.
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
message |
with |
string | No | Human-readable failure message. |
reason |
with |
string | No | Machine-readable failure code. |
- name: abort
type: workflow.fail
if: "not steps.validate.output.ok"
with:
message: "Required input missing"
reason: "missing_input"
If the workflow was invoked by a parent through workflow.execute, the parent receives the error on steps.<child>.error.
A common pattern: a platform team maintains a set of shared--<verb>-<noun> workflows, each with a clean manual trigger and a documented input and output schema. Product teams then compose those shared workflows into their own domain-specific automation.
# Platform team's shared workflow
name: shared--enrich-alerts
description: Enrich alerts with threat intel and geo data.
triggers:
- type: manual
inputs:
- name: alerts
type: object
required: true
outputs:
- name: enriched_alerts
type: object
- name: enrichment_stats
type: object
steps:
# ...enrichment logic...
- name: return_result
type: workflow.output
with:
enriched_alerts: "${{ steps.enrich.output }}"
enrichment_stats: "${{ steps.stats.output }}"
# Security team's workflow that composes it
name: security--triage-with-enrichment
triggers:
- type: alert
steps:
- name: enrich
type: workflow.execute
with:
workflow-id: "shared--enrich-alerts"
inputs:
alerts: "${{ event.alerts }}"
- name: open_case
type: cases.createCase
with:
title: "Threat: {{ steps.enrich.output.enrichment_stats.top_indicator }}"
- Workflow authoring techniques: How to structure a workflow for composition.
- Triggers overview: Composed workflows typically use
manualtriggers so they can be tested in isolation. - Flow control steps: For fan-out across independent executions, combine
foreachwithworkflow.executeAsync.