skip to content

In a Koog strategy, what do nodeStart and nodeFinish define?

level: middleimportance: should knowfreq 48%

answer

  1. Boundary, not a working step
  2. They carry the run's signature
  3. One way in, several ways out
  4. Edges compile only when types line up
  5. transformed adapts a mismatched hop

basics

~10 s

They are the graph's typed boundary. nodeStart's output is whatever you pass to agent.run, and nodeFinish's input is what the run returns. Every other node must connect between them with types the compiler accepts.

solid answer

~40 s

In Koog every strategy has exactly one entry, `nodeStart`, and one exit, `nodeFinish`. They are not steps that do work — they carry the strategy's input and output types. `strategy("name") { }` defaults to a `String` in and `String` out, and `strategy<TInput, TOutput>("name") { }` lets you fix your own types, so `agent.run(input)` is typed end to end. `nodeStart` has no incoming edges and `nodeFinish` no outgoing ones, but many edges may lead into `nodeFinish` — each represents one way the run can end. Because every node has declared input and output types, an edge compiles only if they line up; where they do not, `transformed { }` on the edge converts the upstream value. A graph with no reachable path to `nodeFinish` is a bug the compiler will not catch.

go deeper

for a junior

Know that nodeStart carries whatever you passed to agent.run and nodeFinish carries what the run returns, and that a strategy defaults to String in and String out.

for a middle

Explain that every node has declared input and output types, that an edge only compiles when they line up, and that transformed adapts an edge whose types do not match.

for a senior

Point out the limit of the type system: it proves each hop is legal but never that a path to nodeFinish exists, which is why maxIterations and a review of every node's exits still matter.

for a principal

Frame the typed boundary as the contract other teams code against — changing a strategy's input or output type is an API change with compile-time blast radius, which is precisely why it belongs in types rather than in prompt text.

## The boundary, not a step `nodeStart` and `nodeFinish` are supplied inside every `strategy { }` block. It helps to stop thinking of them as nodes that execute something and start thinking of them as the graph's *signature*. `nodeStart` hands the strategy's input to the first real node. `nodeFinish` accepts the value that becomes the agent run's result. Nothing else happens at either end. Concretely: whatever you pass to `agent.run(...)` arrives as `nodeStart`'s output, and whatever reaches `nodeFinish` is what `run` returns to your Kotlin code. ## Typing the graph `strategy("name") { ... }` gives you the common case, a `String` in and a `String` out. The generic form, `strategy<TInput, TOutput>("name") { ... }`, lets you declare your own types — a data class in, a data class out — and the whole graph is then checked against them. That matters because every node in Koog carries an input and an output type too. `nodeLLMRequest()` accepts the strategy input and produces a model response. `nodeExecuteTool()` accepts a tool call and produces a tool result. A custom `node<In, Out>("name") { input -> output }` declares whatever you like. An `edge(a forwardTo b)` compiles only when `a`'s output type is acceptable as `b`'s input type. Wire a tool result into a node expecting the strategy input and you get a compile error in the IDE, not a runtime failure in production. This is the concrete meaning of "type-checked control flow", which is the main thing Koog trades on for JVM teams: on a dynamically typed agent framework the same mistake is discovered when the graph actually reaches that hop, possibly in production, possibly after money has been spent on model calls. ## transformed: bridging the gap Types will not always line up, and that is expected. An edge can carry a transformation: `transformed { }` maps the upstream node's output into the shape the downstream node wants — pulling text out of a model response, wrapping a tool result in a domain object, or projecting a data class down to the `String` a default-typed `nodeFinish` accepts. Combine it with a condition and the edge both filters and converts. The discipline point: `transformed` is for adapting shapes, not for hiding logic. A transform that quietly performs a business decision is control flow smuggled onto an edge, where it is harder to see and harder to test than a named node doing the same work. ## One entry, many exits The asymmetry is worth stating explicitly. `nodeStart` has no incoming edges — there is exactly one way into the graph. `nodeFinish` has no outgoing edges, but it may have many incoming ones, and normally does: the model answered without tools; the model answered after tools; a validation node gave up; a guard rejected the request. Each incoming edge is one termination path, and reviewing that set is the fastest way to reason about how a strategy can end. Inside a subgraph, `nodeStart` and `nodeFinish` refer to *that subgraph's* own boundary, not the outer strategy's. This is what makes a subgraph composable: it looks like a node from the outside, with its own input and output types, and its internal start and finish are local. ## The failure the compiler cannot see Types tell you a hop is legal. They tell you nothing about reachability. A graph where some node's outgoing conditions can all be false has a hole: at runtime the run reaches that node, matches nothing, and cannot advance to `nodeFinish`. Nothing in the type system objects. This is why `maxIterations` on the agent is not optional in spirit — it is the backstop for logical gaps that typing structurally cannot catch, and why reviewing a strategy means tracing every node's exits, not just checking it compiles. ## How to talk about it The crisp answer is: they are the strategy's input and output types made explicit as graph endpoints, they give the run a compile-checked signature, and the number of edges into `nodeFinish` is the number of ways the agent can end. Then note the limit — typing proves the wiring is legal, not that it terminates.

  • What does transformed on an edge do, and when should you avoid it?
    It converts the upstream node's output into the type the downstream node expects — extracting text from a model response, or projecting a data class to a String. Avoid putting decisions in it: a transform that branches or applies business rules is control flow hidden on an edge, harder to read and test than the same logic in a named node.
  • Does having multiple edges into nodeFinish mean the graph has multiple outputs?
    No — it means multiple termination paths, all producing the same output type. Each incoming edge is one way the run can end: answered directly, answered after tools, rejected by a guard. Listing them is the quickest way to audit how a strategy can finish, and a strategy with only one is often missing an error path.
  • What do nodeStart and nodeFinish refer to inside a subgraph?
    That subgraph's own boundary, not the outer strategy's. A subgraph declares its own input and output types and looks like a single node from outside, so its internal start and finish are local to it. That local boundary is exactly what makes subgraphs composable and independently reasoned about.

saying these in an interview costs you the question

  • Thinks nodeStart performs the first LLM call
  • Assumes a strategy may only have one edge into nodeFinish
  • Believes the graph is untyped and checked at runtime
  • Says types alone guarantee the graph terminates
  • Confuses a subgraph's nodeFinish with the outer strategy's

context