Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
87
88%
Does it follow best practices?
Impact
87%
1.85xAverage score across 45 eval scenarios
Advisory
Suggest reviewing before use
Process steps in order. Do not skip ahead.
Ask the user, one question at a time:
String (default for chat-shaped agents), Message.User (when you control the message shape upstream), or a typed app object?String is the common default; typed outputs require matching node typesProceed immediately to Step 2.
Before opening the editor, write the topology as a numbered list:
If you can't name every step and edge in plain English, you don't yet know the strategy — go back and clarify with the user. The DSL captures the topology; if the topology is fuzzy, the DSL output will be too.
Proceed immediately to Step 3.
This is the most common source of bugs. Match the node family to its expected input:
String-input nodes (use at the top of a strategy, or after another node returns String):
nodeLLMRequest() — call the LLM with the current message as a user turnnodeLLMRequestOnlyCallingTools() — like above but the LLM cannot reply with text, only tool callsnodeLLMRequestWithoutTools() — text-only replynodeLLMRequestForceOneTool(tool) — force a specific tool call (covered in use-llm-node-variants)nodeLLMRequestStructured() — typed JSON output (covered in add-structured-output)nodeLLMRequestMultipleChoices() / nodeLLMRequestStreaming() — covered in use-llm-node-variantsnodeLLMModerateText() — content moderationMessage.User-input nodes (use mid-graph where the previous node produced a Message.User):
nodeLLMSendMessage() / variantsnodeLLMSendToolResults() / variants — pair with nodeExecuteToolsnodeLLMModerateMessage()Picking the wrong family produces compile-time type errors that read like generic DSL noise. The fix is almost always swapping Request for SendMessage or vice versa.
Proceed immediately to Step 4.
nodeExecuteTools ExplicitlyIn 1.0, nodeExecuteTools() returns ReceivedToolResults directly — the auto-writeback variant from earlier versions is gone. Always:
val nodeCallLLM by nodeLLMRequest()
val nodeExecuteTool by nodeExecuteTools()
val nodeSendToolResult by nodeLLMSendToolResults()
edge(nodeStart forwardTo nodeCallLLM)
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCalls { true })
edge(nodeCallLLM forwardTo nodeFinish onTextMessage { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCalls { true })
edge(nodeSendToolResult forwardTo nodeFinish onTextMessage { true })Without the nodeExecuteTool forwardTo nodeSendToolResult edge, tool outputs never reach the LLM and the agent silently loses memory of what it ran.
Proceed immediately to Step 5.
Edges declare topology and branching. Vocabulary:
edge(a forwardTo b) — unconditionaledge(a forwardTo b onToolCalls { predicate(it) }) — when the LLM produced tool callsedge(a forwardTo b onTextMessage { predicate(it) }) — when the LLM produced a text replyedge(a forwardTo b onCondition { boolean predicate(it) }) — arbitrary predicateedge(a forwardTo b onIsInstance SomeClass::class) — type-discriminated branchingedge(a forwardTo b transformed { reshape(it) }) — change shape between nodesa then b — shorthand for an unconditional sequential edgeMember vs extension — which imports you need. forwardTo, onCondition, and transformed are infix members (no import). The other primitives (onToolCalls, onTextMessage, onIsInstance, onMessageParts, onSuccessful, onFailure, asUserMessage, asToolResultMessage) are top-level extensions in ai.koog.agents.core.dsl.extension.* and need explicit imports. Full table:
skills/author-strategy/references/edge-primitive-imports.mdBranching logic belongs in edge predicates, not inside node bodies. Lifting it to edges keeps the topology inspectable.
Proceed immediately to Step 6.
If a step needs several LLM calls, its own tool subset, or its own model, lift it into a subgraph. Reference: examples/simple-examples/.../subgraphwithtask/CustomStrategy.kt.
import ai.koog.agents.ext.agent.subgraphWithTask
import ai.koog.agents.ext.agent.subgraphWithVerification
import ai.koog.agents.ext.agent.CriticResult
val generate by subgraphWithTask<Unit, String>(generateTools, llmModel = OpenAIModels.Chat.GPT4o) { input ->
"system prompt for the generation step..."
}
val verify by subgraphWithVerification(tools = verifyTools, parallelTools = false) { input: String ->
"system prompt for verifying the generated result..."
}
val fix by subgraphWithTask<CriticResult<String>, String>(tools = fixTools, llmModel = AnthropicModels.Opus_4_6) { vr ->
"system prompt that incorporates ${vr.feedback}..."
}
edge(verify forwardTo fix onCondition { !it.successful })
edge(verify forwardTo nodeFinish onCondition { it.successful } transformed { "Project is correct." })
edge(fix forwardTo verify)The generate → verify → fix shape is the canonical "real agentic workflow" demo — it shows iterative correction without inventing a planner.
subgraphWithTask, subgraphWithVerification, and CriticResult live in package ai.koog.agents.ext.agent but ship inside the agents-core artifact, which the koog-agents umbrella already pulls. No extra dependency needed — the standalone ai.koog:agents-ext artifact is a separate 1.0.0-beta module and is NOT required for these APIs.
If you type-annotate the returned strategy, the strategy type lives in ai.koog.agents.core.agent.entity.AIAgentGraphStrategy (not bare ai.koog.agents.core.agent).
Proceed immediately to Step 7.
val classify by nodeLLMRequest() reads as edge(classify forwardTo ...). Names like node1, step1, llm erase what the strategy is doing.
Proceed immediately to Step 8.
Path: labels (same convention as scaffold-agent):
Path: src/main/kotlin/com/example/Strategy.kt — strategy definition with the graph DSLPath: src/main/kotlin/com/example/Main.kt — modified agent construction passing strategy = ...AIAgent(strategy = myStrategy("name"), ...). If the strategy isn't singleRunStrategy (the default), the strategy = parameter must be named explicitly./gradlew build to confirm it compiles. The most common failure is a node-type mismatch — go back to Step 3Skill(skill: "use-planner") insteadFinish here.
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
skills
add-observability
add-persistence
add-rag
add-structured-output
add-token-budgeting
add-tool
cache-llm-calls
define-prompt
domain-model-subtask-pipeline
references
enable-prompt-caching
handle-agent-events
manage-state
migrate-from-0-x
model-planner-subtasks
persist-chat-history
query-sql-from-agent
scaffold-agent
snapshot-and-restore
test-koog-agents
trace-agent-internals
use-attachments
use-functional-agent
use-llm-node-variants
use-planner
wire-a2a
wire-acp-server
wire-ktor-server
wire-mcp-server
wire-spring-boot