---
title: Sub-flows
path: concepts/sub-flows
status: published
---

# Sub-flows

A flow can invoke another deployed Core via the `subflow_call` node. The compiler emits an `IRCoreCallBlock` (`kind: "core_call"`) per the ScaiCore IR spec.

## Per-node config

```jsonc
{
  "type": "subflow_call",
  "config": {
    "target": "core://acme/customer-lookup",
    "flow_name": "LookupCustomer",
    "input_bindings": { "customer_id": "{{message.customer_id}}" },
    "instance_key": null,
    "version": null,
    "timeout": "30s",
    "is_async": false
  }
}
```

- **`target`** — a `core://{tenant_slug}/{slug}` URI pointing at the called Core. Cross-tenant calls supported if the caller's principal has access on the other tenant.
- **`flow_name`** — optional; targets a specific flow within the called Core when the Core exposes multiple.
- **`input_bindings`** — map of `{argument_name: expression}`. Expressions can reference the calling flow's scope.
- **`instance_key`** — for entity-mode targets, picks the instance. Pass-through to the runtime.
- **`version`** — pin to a specific Core version. Omit to follow the target's default.
- **`timeout`** — wall-clock budget.
- **`is_async`** — `true` for fire-and-forget; `false` (default) waits for the sub-flow's result.

## Canvas picker

The Sub-flow Target editor lists the calling tenant's flows from `flowsListStore`, lets you click one to fill in the `core://` URI, and falls back to a free-text URI input for cross-tenant calls.

## When to reach for sub-flows

- **Reuse.** A "summarize transcript" flow that lives once and is called from three different parent flows.
- **Specialization.** A heavy reasoning step that warrants its own deploy/lifecycle (different model registry, different deploy cadence).
- **Composition with HITL.** The sub-flow contains the HITL surface; the parent doesn't have to know about it.
- **Cross-tenant integration.** Calling out to a partner-published Core via its `core://{partner}/{slug}` URI.

## When NOT to

- "Just to organize the canvas" — large flows are easier to manage as one graph with named sections than as a fan-out of tiny sub-flows.
- "To split deterministic steps" — `@rigid` blocks and plugin calls compose cheaply within the same flow. Sub-flows have invocation overhead.

## IR shape

```python
{
  "kind": "core_call",
  "target": "core://acme/customer-lookup",
  "flow_name": "LookupCustomer",
  "input_bindings": {"customer_id": IRExpression(...)},
  "instance_key": None,
  "version": None,
  "timeout": "30s",
  "is_async": False
}
```

Per `SCAICORE-COMPILER-IR.md` §5.9.

## YAML shape

```yaml
steps:
  - core_call:
      target: core://acme/customer-lookup
      flow_name: LookupCustomer
      input_bindings:
        customer_id: "{{message.customer_id}}"
      timeout: 30s
    meta: { node_id: node_abc, node_kind: subflow_call, ... }
```

The `core_call` YAML keyword isn't yet shown in ScaiGrid's public scaicore.md example — this is a best-effort emission matching the IR field names. If ScaiGrid's YAML loader doesn't recognize it on your deployment, fall back to `--format ir`.
