Symphonix Documentation · Core 0.61.0

Subflows

One flow running another as a single step.

This is the engineering reference, rendered from the repository

It is the same file the platform's own tests are written against — LGFlow/docs/guides/subflows.md — rendered here rather than copied, so it cannot fall behind the software it describes. It is written for somebody working on the platform and uses the internal project names: LGFlow is Symphonix Core, LGStudio is Symphonix Studio, and LGMonitor or LGStudioMonitor is the Symphonix Control Panel. Names that begin LGFlow and appear in code — LGFlowValidationError and the rest — are exported identifiers and are spelled exactly as the software spells them.

A flow run as a step of another flow.

A job that stands on its own — a customer lookup, an order search, a document answerer — is a flow of its own, run as one step of another:

{ "id": "find_customer", "type": "flow", "flow": "./flows/customer-lookup.json",
  "input":  { "customerId": "{{customer_id}}" },
  "output": { "customer": "found", "found": "known" } }

The child is an ordinary flow file: its own description, inputs, outputs, channels, nodes, tools and routing. That contract — the one every flow has declared since 0.7 — is exactly the call signature this node uses, and nothing else crosses between them. input fills the child's inputs from the parent's state; output says where each of the child's outputs lands in the parent. The child cannot read the parent's other channels, and cannot write them.

Reading a flow reads the flows it runs — each address is relative to the file that names it — so loadFlow gives you the whole tree, and the compiler refuses to build a parent whose child is missing rather than failing halfway through a run. A flow cannot run itself, directly or in a circle.

Steps of a child are reported as <node>/<their node>: find_customer/look_up in the trace, the steps panel and the journal, so a run reads as what it is. A child's handler is registered under the node that runs it — "find_customer.lookUp" — so two children may each have a lookUp without either being renamed; a bare name is offered to any child that wants it.

A child may stop and talk to the person — v0.14. An ask, a form, an approval or a tool short of a required input inside a flow run as a step puts its question to whoever the flow on top is talking to:

  -> choose/read_orders
  (waiting at "choose/ask_which")
  Which order would you like to cancel?
    11011 - placed 2026-09-19, 933.5
    10692 - placed 2026-09-03, 878
  > 10692
  -> choose/ask_which  ->  choose/pick  ->  choose

The run waits and resumes exactly as it does for the flow on top: the child waits in the same checkpointer — one for the whole tree — and the answer goes back into the child, not to the start of the step. The question names the step that asked it the way a step is named, choose/ask_which, so a page can say where it came from and a canvas can light up the node that is waiting.

Put the question where the work is. A flow that looks an order up and needs to know which order should ask for it itself, rather than making every flow that runs it ask first and pass the answer in. Below 0.14 this is refused, with the version that allows it named in the message.

Generating a project generates the whole tree. One flow and the flows it runs make one project: each child is written beside flow.json at the address its node gives, and its code under src/flows/<its file>/, so the generated agent loads and runs exactly the tree that was authored.

flow.json                                     the desk
flows/customer-lookup.json                    the flows it runs, at their own addresses
flows/order-history.json
src/nodes/writeReport.js                      the desk's own code
src/flows/customer-lookup/nodes/lookUpCustomer.js     each child's, kept together
src/flows/order-history/nodes/readOrders.js

A handler or a router is registered as "<the node>.<name>", which is where the compiler looks for a child's first. A tool or a service is not scoped — callTool and the host are handed down as they are — so a project has one of each by name, and readiness refuses a name two flows in the tree describe differently rather than letting one of them quietly win. Settings, packages and .env names are the tree's, merged.

examples/013VerSample is the whole thing working: a customer desk whose lookup and order history are separate files. npm run example:sample13.

examples/014VerSample is the interactive half: a cancellation desk that asks nothing itself and still puts two things to you — a question from one flow it runs, an approval card from another. npm run example:sample14 at a terminal, or open it in the gallery, where it is served with the agent chat page.


When you do not know which child#

A flow node names its child when the flow is written. When which child depends on the question — a desk for the database, one for the documents, one for everything else — v0.19 adds an agent node that resolves a name through the project's registry while the run is happening:

{ "id": "delegate", "type": "agent",
  "agents": ["sql", "documents", "general"],
  "agent": "{{decision.agent}}",
  "input": "{{decision.input}}",
  "output": "agent_result" }

Underneath it is this same execution: the same compiler, the same isolated state, the same <node>/<their node> naming of steps, the same handling of a child that stops to ask something. What differs is when the child is chosen, and therefore when it can be checked. Prefer a flow node whenever you know — it is checked earlier and reads plainer. See Many agents.