Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
88
88%
Does it follow best practices?
Impact
88%
1.95xAverage score across 43 eval scenarios
Passed
No known issues
Process steps in order. Do not skip ahead.
Ask the user:
If the user is ambiguous, default to read-only and proceed. Read-write requires explicit confirmation — it's an order of magnitude more dangerous.
Proceed immediately to Step 2.
implementation("ai.koog:agents-features-sql:1.0.0")Also include the JDBC driver for the actual database (Postgres, MySQL, SQLite, etc.) — Koog doesn't bundle drivers.
Proceed immediately to Step 3.
Install in the AIAgent(...) trailing lambda:
import ai.koog.agents.features.sql.Sql
val agent = AIAgent(
promptExecutor = ...,
llmModel = ...,
systemPrompt = """
You can query the production analytics database. Schema is restricted to read-only
access on the `public.events` and `public.users` tables. Always limit result sets
to at most 100 rows.
""".trimIndent(),
) {
install(Sql) {
jdbcUrl = System.getenv("ANALYTICS_DB_URL")
username = System.getenv("ANALYTICS_DB_USER")
password = System.getenv("ANALYTICS_DB_PASSWORD")
readOnly = true // STEP 1 default
schemaScope = listOf("public.events", "public.users") // narrow what the agent sees
maxRows = 100 // hard cap on result rows per query
}
}The feature registers sql_query (or similar — exact tool name from the module) into the agent's ToolRegistry automatically. The LLM calls it like any other tool.
Always read credentials from environment variables.
Proceed immediately to Step 4.
The LLM needs to know what tables and columns are available before it can write a query. Two options:
describe_schema tool that returns the schema as JSON. The LLM calls it before writing queries. Use this for larger schemasWithout schema knowledge the LLM hallucinates table/column names and queries fail. Pinning the schema is not optional.
Proceed immediately to Step 5.
maxRows cap helps but does not bound query cost. Consider query timeouts at the JDBC layerreadOnly = true, the credential itself should be read-only at the database. Defense in depthSELECT id, COUNT(*) FROM users not SELECT * FROM users)schemaScope controls what the agent sees, but the database credential grants whatever it grants. The credential's actual permissions are the real boundaryFinish 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
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