Sub-Chains¶
Sub-chains let a chain step invoke another chain by name, enabling reusable workflow components. The parent chain pauses at that step until the sub-chain completes, then resumes with the sub-chain's result.
How It Works¶
flowchart TB
subgraph Parent Chain
A[Step 1: Check] --> B[Step 2: Sub-Chain]
B --> C[Step 3: Notify]
end
subgraph Sub-Chain: escalate-and-notify
D[Step 1: PagerDuty] --> E[Step 2: Slack]
end
B -.->|invokes| D
E -.->|result| B - The parent chain reaches a sub-chain step
- A new child chain execution is spawned from the referenced chain config
- The parent enters
waiting_sub_chainstatus - The child chain runs independently through the background processor
- When the child completes, the parent resumes with the child's final step result
- If the child fails, the parent applies the step's
on_failurepolicy
Configuration¶
TOML¶
# The parent chain
[[chains]]
name = "incident-response"
timeout_seconds = 3600
[[chains.steps]]
name = "check-severity"
provider = "monitoring-api"
action_type = "get_severity"
[[chains.steps]]
name = "escalate"
sub_chain = "escalate-and-notify" # Invokes another chain
on_failure = "skip" # Skip if sub-chain fails
[[chains.steps]]
name = "resolve"
provider = "ticketing"
action_type = "close_ticket"
# The reusable sub-chain
[[chains]]
name = "escalate-and-notify"
timeout_seconds = 300
[[chains.steps]]
name = "page-oncall"
provider = "pagerduty"
action_type = "create_incident"
[[chains.steps]]
name = "notify-channel"
provider = "slack"
action_type = "post_message"
Programmatic (Rust)¶
use acteon_core::chain::{ChainConfig, ChainStepConfig, StepFailurePolicy};
let parent = ChainConfig::new("incident-response")
.with_step(ChainStepConfig::new(
"check-severity", "monitoring-api", "get_severity",
serde_json::json!({}),
))
.with_step(
ChainStepConfig::new_sub_chain("escalate", "escalate-and-notify")
.with_on_failure(StepFailurePolicy::Skip),
)
.with_step(ChainStepConfig::new(
"resolve", "ticketing", "close_ticket",
serde_json::json!({}),
));
let sub_chain = ChainConfig::new("escalate-and-notify")
.with_step(ChainStepConfig::new(
"page-oncall", "pagerduty", "create_incident",
serde_json::json!({}),
))
.with_step(ChainStepConfig::new(
"notify-channel", "slack", "post_message",
serde_json::json!({}),
));
Sub-Chain Step Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Step identifier |
sub_chain | string | Yes | Name of the chain to invoke |
on_failure | string | No | Failure policy: "abort", "skip", "dlq" |
delay_seconds | u64 | No | Delay before spawning the sub-chain |
branches | array | No | Branch conditions evaluated after sub-chain completes |
default_next | string | No | Default next step when no branch matches |
Note
sub_chain and provider are mutually exclusive. A step is either a provider step or a sub-chain step.
Template Variables¶
Sub-chains inherit the parent's origin_action, so {{origin.*}} templates in the sub-chain resolve to the root triggering action. The sub-chain's step results are available within the sub-chain via {{prev.*}} and {{steps.NAME.*}}.
After a sub-chain step completes, the parent chain can access the sub-chain's final result:
{{prev.response_body}}— the last completed step's response body from the sub-chain{{prev.success}}— whether the sub-chain's last step succeeded
Timeout Inheritance¶
The child chain's effective timeout is the minimum of:
- The parent chain's remaining time (
parent.expires_at - now) - The sub-chain's own
timeout_seconds(if configured)
This ensures a sub-chain cannot outlive its parent.
Cancellation Cascade¶
Cancelling a parent chain automatically cancels all running child chains. This is recursive: if a child chain has its own sub-chains, those are cancelled too.
Cycle Detection¶
Acteon validates the sub-chain reference graph at startup. Cycles are rejected:
Self-references are also detected and rejected.
DAG Visualization¶
The DAG API provides a hierarchical view of chain execution:
Instance DAG (running/completed chain)¶
Definition DAG (config-only, no running instance)¶
Both return a DagResponse with nested structure for sub-chain steps:
{
"chain_name": "incident-response",
"chain_id": "chn-abc123",
"status": "waiting_sub_chain",
"nodes": [
{
"name": "check-severity",
"node_type": "step",
"provider": "monitoring-api",
"status": "completed"
},
{
"name": "escalate",
"node_type": "sub_chain",
"sub_chain_name": "escalate-and-notify",
"status": "waiting_sub_chain",
"child_chain_id": "chn-def456",
"children": {
"chain_name": "escalate-and-notify",
"chain_id": "chn-def456",
"status": "running",
"nodes": [
{ "name": "page-oncall", "node_type": "step", "status": "completed" },
{ "name": "notify-channel", "node_type": "step", "status": "pending" }
],
"edges": [{ "source": "page-oncall", "target": "notify-channel" }],
"execution_path": ["page-oncall"]
}
},
{
"name": "resolve",
"node_type": "step",
"provider": "ticketing",
"status": "pending"
}
],
"edges": [
{ "source": "check-severity", "target": "escalate", "on_execution_path": true },
{ "source": "escalate", "target": "resolve", "on_execution_path": false }
],
"execution_path": ["check-severity"]
}
Chain List Filtering¶
Sub-chain executions appear in the chain list with a parent_chain_id field. Use the waiting_sub_chain status filter to find parent chains waiting on sub-chains.
Nesting Depth¶
Sub-chains can be nested (a sub-chain can itself contain sub-chain steps). The DAG visualization supports up to 10 levels of nesting depth. Deeply nested chains may indicate a design issue — consider flattening the workflow.
See Also¶
- Task Chains — base chain concepts, configuration, and failure policies