When would you split a Koog strategy into subgraphs instead of one graph?
answer
- Phases, not just length
- A subgraph looks like a node outside
- Its own start and finish, locally
- Narrower tool surface per phase
- Seam is a contract with a cost
basics
~20 sSplit when the run has distinct phases with different tool sets, different termination rules, or different owners. A subgraph is a typed unit with its own nodes, edges and boundary, so it composes and can be tested and reasoned about alone.
solid answer
~50 sA Koog `subgraph<TInput, TOutput>("name") { ... }` is a strategy fragment that behaves like a single node in the outer graph: it has its own `nodeStart` and `nodeFinish`, its own nodes and edges, and its own declared input and output types. Reach for it when phases genuinely differ — a research phase that needs search tools and many tool round-trips, then a drafting phase that should call no tools at all and answer once. Splitting gives you three things a flat graph cannot: a narrower tool surface per phase, so the model has fewer wrong choices; a typed seam you can unit-test and reuse; and a review boundary, since the interesting question about a strategy is what crosses between phases, not what happens inside them. The cost is indirection — two shallow subgraphs around a five-node graph are ceremony, not structure.
code
kotlin · 24 linesval pipeline = strategy("research-then-write") {
val research by subgraph<String, String>("research") {
val ask by nodeLLMRequest()
val runTool by nodeExecuteTool()
val send by nodeLLMSendToolResult()
edge(nodeStart forwardTo ask)
edge(ask forwardTo nodeFinish onAssistantMessage { true })
edge(ask forwardTo runTool onToolCall { true })
edge(runTool forwardTo send)
edge(send forwardTo nodeFinish onAssistantMessage { true })
edge(send forwardTo runTool onToolCall { true })
}
val write by subgraph<String, String>("write") {
val draft by nodeLLMRequest()
edge(nodeStart forwardTo draft)
edge(draft forwardTo nodeFinish onAssistantMessage { true })
}
edge(nodeStart forwardTo research)
edge(research forwardTo write)
edge(write forwardTo nodeFinish)
}go deeper
Know that a subgraph is a strategy fragment with its own nodes, edges, start and finish, and that from the outer graph it behaves like a single typed node.
Explain the mechanics: declared input and output types at the seam, a local nodeStart and nodeFinish, and the ability to give a phase its own tool set and its own exit conditions.
Argue from a real run: research needs many tool round-trips and drafting needs none, so they want different tool surfaces and different termination, and the seam is what you unit-test.
Frame it as a module-boundary decision that also constrains the model's action space — draw seams where you want to guarantee something cannot happen, and be honest that each seam is a contract with a change cost.
## What a subgraph is Inside `strategy("name") { }` you can declare `val phase by subgraph<TInput, TOutput>("phase") { ... }`. The block contains ordinary nodes and edges, and its `nodeStart` and `nodeFinish` refer to *that subgraph's* boundary, not the outer strategy's. From the outside, the subgraph is just a node: you connect edges into and out of it, and the same compile-time type checking applies at the seam. So a subgraph is not an execution mode or a separate agent. It is a naming and scoping construct for graph structure, with a typed contract at the edge. ## The three reasons that actually justify it **Different tool surfaces per phase.** This is the strongest one and the least appreciated. Tool selection quality falls as the tool list grows: a model choosing among four relevant tools is far more reliable than the same model choosing among twenty, most of which are irrelevant to the current phase. A flat graph forces one tool surface across the whole run. Splitting a run into a research phase and a drafting phase lets the research phase see retrieval tools while the drafting phase sees none — which also means the drafting phase *cannot* make an external call, no matter what the model decides. That is a structural guarantee, not a prompt instruction, and structural guarantees are the ones you can put in a design review. **Different termination rules.** A research phase may legitimately loop through a dozen tool round-trips; a drafting phase should call the model once and finish. In one flat graph those two shapes share the same exit conditions and the same iteration allowance, so you tune for the loose one and lose the tightness of the strict one. As separate subgraphs each phase has its own edges and its own exits, and "drafting should never loop" becomes visible in the graph rather than implied. **A seam for testing and ownership.** A subgraph with declared input and output types is a unit: you can drive it in a Kotlin test with a stubbed executor, assert what it produces, and reuse it in a second strategy. It is also the natural review boundary — on a large agent the interesting design question is what data crosses between phases, and a typed seam forces that to be answered explicitly instead of accumulating as shared context nobody owns. ## What splitting costs Indirection, mostly. A five-node graph wrapped in two subgraphs is harder to read than the five nodes, and you have paid ceremony for nothing. There is a second, subtler cost: the seam is a narrowing. Whatever the subgraph does not put into its declared output does not travel forward, so if the drafting phase later turns out to need a detail the research phase saw and discarded, you change a type signature and every caller. That is exactly the property you wanted — explicit contracts — but it is a real change cost, and it is the reason to draw the seam where the phases are genuinely independent rather than wherever the graph happens to be long. ## How to decide The test I would apply in a design review: can you state, in one sentence each, what this phase receives and what it returns, without referring to the other phases' internals? If yes, it is a subgraph. If the answer needs "well, it also needs to know whether the earlier step found anything, and sometimes it goes back", the phases are entangled and the split will leak — you will end up threading state through the seam to fake the coupling, which is worse than a flat graph. A second test: does anything about the phases *differ* — tool set, model, termination, ownership? If every phase runs the same tools with the same rules, subgraphs add naming and nothing else. Naming is worth something, but say that is what you are buying. ## The strategic framing The honest principal-level point is that this is the same decision as module boundaries anywhere else, and the same failure modes apply: too few boundaries and everything is coupled to everything; too many and you spend your time maintaining seams. What makes it sharper in an agent is that a boundary here also constrains the *model's* choices, not just the code's. Restricting a phase's tool surface removes whole classes of wrong action from the possibility space — the model cannot pick a tool it was not given. That is a control you cannot get from prompt wording, and it is usually the argument that decides where the seams go: draw them where you want to be able to say "in this phase, that cannot happen".
- Why is restricting a phase's tool surface stronger than telling the model not to use a tool?Because it removes the option rather than discouraging it. A prompt instruction is advisory — the model can and eventually will ignore it, and you only find out afterwards. A phase that was never given the tool cannot call it under any prompt, any temperature, any adversarial input. That turns a behavioural hope into a structural property you can assert in a design review.
- What is the sign that you drew the subgraph boundary in the wrong place?You start threading extra fields through the seam so a later phase can see what an earlier one knew, or you add an edge that jumps back into a completed phase. Both mean the phases were never independent. At that point the honest fix is to move the boundary or flatten the graph, not to widen the contract until it carries everything.
- Does splitting into subgraphs reduce token cost?Not by itself — a subgraph is a structural construct, not a context-management one. It creates a natural place to put history compression between phases and it can reduce the tool schemas in play for a phase, both of which help. But if you keep passing the whole transcript forward, the split changes readability and control, not spend.
saying these in an interview costs you the question
- Treats a subgraph as a separate agent or process
- Splits by graph length rather than by phase
- Assumes subgraphs reduce token cost on their own
- Thinks a prompt instruction restricts tools as firmly as scope
- Threads state through the seam to fake coupling