New-customer onboarding assembly
Goal
Onboarding a new customer is the same handful of steps every time: verify the customer (KYC), provision their account, then stand up a regional footprint in each region you serve. Rather than copy those steps into every workflow, build each as its own small graph once and compose them.
This recipe assembles three reusable child graphs into one parent:
kyc-checkandprovision-accountrun in sequence askind: graph(subgraph) nodes.provision-regionis broadcast across several regions with afan_out: broadcastOVER a subgraph target, so the same child graph runs N times in parallel, each with isolated state.- A coordinator agent kicks the whole thing off from a single instruction using
the
workspace_ext__invoke_graphtool, and gets the rolled-up result back.
The headline mechanic is subgraph composition: a child graph's output flows
into the parent (nodes.<child>.text), a failing child fails the parent instead
of being silently skipped, and a broadcast over a subgraph gives every instance
its own nested run. As with the other recipes, each step is shown in the
console first, then Via the CLI; for the graphs the editor's Import
spec paste and the primectl create graph -f manifest describe the same spec.
Ingredients
- An LLM provider for the agent inside each child graph (in this recipe each
child graph is
begin -> agent -> end; the agent does the actual KYC / provisioning work). The composition mechanics are model-independent, so you can pin each agent's output for a reproducible run. - A workspace.
- No triggers or subscriptions: the coordinator agent starts the assembly itself
with
invoke_graph.
If you have not connected primectl yet, see "Connecting the CLI" in the
RAG knowledge base recipe.
Walkthrough
1. Build the three reusable child graphs
Each is a tiny begin -> agent -> end graph. The agent does the work; the end
node exposes the agent's text as the child graph's output. Keep them generic so
the same graph slots into any parent. (Create a kyc-agent, a
provision-agent, and a region-agent first under Compute > Agents, each
with your LLM provider and a one-line system prompt.)
In the console:
- Go to Compute > Graphs, click New graph, give it the ID
kyc-check, pick thekyc-agentas Seed agent, and click Create. - In the editor click Import spec, paste the JSON from the manifest's
spec(below), Load into editor, and Save. - Repeat for
provision-accountandprovision-region, swapping theidand theagent_id.
Via the CLI:
primectl create -f kyc-check.yaml
primectl create -f provision-account.yaml
primectl create -f provision-region.yaml
kind: graph
spec:
id: kyc-check
description: Verify a new customer's identity.
max_iterations: 10
nodes:
- { kind: begin, id: begin }
- { kind: agent, id: work, agent_id: kyc-agent, input_template: "{{ initial_input }}" }
- { kind: end, id: end, output_template: "{{ nodes.work.text }}" }
edges:
- { kind: static, from_node: begin, to_node: work }
- { kind: static, from_node: work, to_node: end }
Create provision-account and provision-region the same way (swap the agent
and the id). The only thing that matters for composition is that each child's
end node renders the output you want the parent to see: that text is what the
parent reads off nodes.<child>.text.
2. Assemble the parent graph
The parent references each child by id as a kind: graph node, chains the two
sequential ones, then fans the region child out with a broadcast spec.
In the console:
- New graph, ID
onboarding-assembly, any seed agent, Create. - Import spec, paste the manifest's
specbelow, Load into editor, Save. (To build it by hand instead: add a Begin, two Graph subgraph nodes (kyc->kyc-check,provision->provision-account), a Fan-out in broadcast mode (targetregion, count 3), a Graph noderegion->provision-region, a Fan-inrollup, and an End; wirebegin -> kyc -> provision -> regionsandregion -> rollup -> endwith static edges. The fan-out spec spawns the region instances, so there is noregions -> regionedge.)
Via the CLI:
primectl create -f onboarding-assembly.yaml
kind: graph
spec:
id: onboarding-assembly
description: Compose KYC + account provisioning, then a regional footprint.
max_iterations: 30
nodes:
- { kind: begin, id: begin }
- { kind: graph, id: kyc, graph_id: kyc-check, input_template: "Customer: {{ initial_input }}" }
- { kind: graph, id: provision, graph_id: provision-account, input_template: "Provision for {{ initial_input }}" }
- kind: fan_out
id: regions
specs:
- { kind: broadcast, target_node_id: region, count: 3 }
- { kind: graph, id: region, graph_id: provision-region, input_template: "Provision region #{{ fanout_index }}" }
- { kind: fan_in, id: rollup, aggregate_template: "{% for r in nodes.region %}region #{{ loop.index0 }}: {{ r.text }}\n{% endfor %}" }
- { kind: end, id: end, output_template: "KYC={{ nodes.kyc.text }} | PROV={{ nodes.provision.text }}\n{{ nodes.rollup.text }}" }
edges:
- { kind: static, from_node: begin, to_node: kyc }
- { kind: static, from_node: kyc, to_node: provision }
- { kind: static, from_node: provision, to_node: regions }
- { kind: static, from_node: region, to_node: rollup }
- { kind: static, from_node: rollup, to_node: end }
Five things that decide whether the composition runs the way you expect:
- A
kind: graphnode runs a whole child graph and hands its End output up. Inside the parent,nodes.kyc.textis thekyc-checkgraph'sendoutput (andnodes.kyc.parsedis its parsed object when the child'sendproduced one). The parent'sendtemplate weaves the two sequential children together. The child's output is captured from its End node, not reconstructed from a streamed text guess, so it is never silently empty. broadcastover a subgraph runs the child N times. Theregionsfan-out spawnsregion[0],region[1],region[2], each a fullprovision-regionrun. Inside theregionnode'sinput_templateyou read the instance index with{{ fanout_index }}. There is noregions -> regionedge: the fan-out spec does the dispatch.- Each broadcast instance gets its own isolated nested run. A subgraph node
that is a fan-out instance runs under its own state subtree (
region[0],region[1], ...), so the three concurrent region runs never collide on one shared checkpoint.nodes.regionis alistaligned by instance index. - The fan_in joins the list.
nodes.regionis a list, so therolluptemplate loops it. The fan_in waits for all instances before it fires; you wire the join with theregion -> rollupedge. - A failing child fails the parent. If a child graph ends
failed(a bad template, a tool error, a node that fails), the parent's subgraph node endsfailedtoo and the parent does not advance past it. Composition does not swallow a broken child.
3. Kick it off from a coordinator agent
The coordinator runs the whole assembly inside its own session with
workspace_ext__invoke_graph and gets the graph's output back as the tool
result, so a single agent instruction drives the entire onboarding.
In the console:
- Go to Compute > Agents, New agent, ID
onboarding-coordinator, pick the LLM provider/model; on the Tools tab checkworkspace_ext__invoke_graph; on Advanced paste the system prompt (below). Click Create.
Via the CLI:
primectl create -f coordinator.yaml
kind: agent
spec:
id: onboarding-coordinator
description: Kicks off the onboarding assembly.
model: { provider_id: <llm>, model_name: <model> }
tools:
- workspace_ext__invoke_graph
system_prompt:
- >-
When asked to onboard a customer, call workspace_ext__invoke_graph with
graph_id 'onboarding-assembly' and input set to the customer name, then
report the returned output.
Start an agent session on onboarding-coordinator with an instruction like
Onboard the new customer Acme Corp. The agent calls invoke_graph, the
onboarding-assembly graph runs nested inside the session, and the rolled-up
output comes back as the tool result.
In the console, click New session, set the Binding to agent, pick
onboarding-coordinator and a Workspace, type the instruction into
Initial instructions, and click Create.
Via the CLI:
primectl session run <workspace-id> --agent onboarding-coordinator \
-i "Onboard the new customer Acme Corp"
Testing
Start a coordinator session (console or primectl session run) and watch it to
completion.
Expected outcome (verified):
-
The coordinator's
workspace_ext__invoke_graphtool result carries the fully-assembled onboarding summary, which only renders if every child subgraph's output propagated:KYC=KYC VERIFIED for Acme Corp | PROV=ACCOUNT PROVISIONED. region #0: REGION READY region #1: REGION READY region #2: REGION READY -
The parent graph state records the sequential children
kycandprovisionasended, plus oneregion[i]node per broadcast instance, each with its own isolated node directory. The invoke-graph run nests under the agent session, and each region instance is its own run dir (...__region[0],...__region[1],...__region[2]), never one shared...__region. You can confirm this withprimectl workspace files ls <workspace-id> .state/graphs(the run dirs) andprimectl workspace files get <workspace-id> .state/sessions/<sid>/messages.jsonl --content(the rolled-up transcript). -
A failing child fails the parent: if you point a parent's subgraph node at a child graph that ends
failed, the parent graph endsfailedand that subgraph node is markedfailedwith the child's failure detail. Composition surfaces the failure instead of marching on.
Notes and limitations
- Both subgraph paths are covered. Composition has two code paths that behave
identically here: the
kind: graphNODE path (used by the sequential and broadcast children inside the parent) and theworkspace_ext__invoke_graphTOOL path (used by the coordinator to run the parent). Both capture the child's End output and both fail the caller when the child fails. - Keep nesting shallow. Two levels of nesting (coordinator -> parent ->
child) is exercised and clean. Deeply nested broadcast-over-subgraph trees
multiply the number of concurrent child runs, so size your
countandmax_iterationsfor the fan-out you actually need. - The child graph's
endoutput is the contract. Whatever a child graph'sendnode renders is what the parent reads onnodes.<child>.text. Give each child a deliberateendoutput_template(oroutput_schemafor a parsed object) rather than relying on whatever the last node happened to stream.