Subflows
One flow running another as a single step.
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 -> chooseThe 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.jsA 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.