How do you build a prompt with Koog's prompt("id") { } DSL, and what is the id for?
answer
- Builder, not a string template
- Roles become typed message objects
- The id never reaches the model
- Assistant turns carry few-shot examples
- Nothing is sent until the executor runs
basics
~20 sKoog's prompt("id") { } builder returns an immutable Prompt made of typed messages added by system(), user() and assistant() calls. The id is a stable label for that prompt, used to identify it in logs, traces and tooling — not sent as content.
solid answer
~50 s`prompt("ticket-triage") { system(...); user(...); assistant(...) }` is a Kotlin builder that produces an immutable `Prompt`: an ordered list of typed messages, not a concatenated string. `system()` sets instructions, `user()` adds the human turn, and `assistant()` seeds a prior model turn — which is how you supply few-shot examples or replay conversation history. Each provider client converts those typed messages into that vendor's own request shape, so the same `Prompt` object can be sent to different models unchanged. The string id is metadata: a stable name for this prompt that shows up in logging and tracing, so runs are attributable to a named prompt rather than an anonymous blob. Sampling parameters such as temperature travel with the prompt as `LLMParams` rather than being baked into the text. Building the object does nothing by itself; nothing is sent until you pass it to a `PromptExecutor`.
code
kotlin · 8 linesval triage = prompt("ticket-triage") {
system("You classify support tickets. Reply with one word: BILLING, SHIPPING or OTHER.")
user("My card was charged twice.")
assistant("BILLING")
user("Order 4711 never arrived.")
}go deeper
Be able to write the block from memory — system, user, optional assistant — and say plainly that it returns an object and sends nothing until an executor runs it.
Explain why typed messages beat string concatenation across providers, and how assistant turns give you few-shot examples and replayed history.
Talk about prompt ids as an operational handle for tracing and comparison, about keeping variable data out of the system turn, and about what ends up in logs when content capture is on.
Own prompt governance: stable ids, versioning and review of prompt changes, and the policy on what user data may appear in prompts that traces and log sinks will retain.
## What the builder produces `prompt("some-id") { ... }` is a Kotlin type-safe builder. Inside the block you call message functions — `system(...)`, `user(...)`, `assistant(...)` — and the result is an immutable `Prompt` value: an ordered list of typed messages plus an id and parameters. The crucial point for anyone coming from string-templating libraries is that this is *structured*, not textual. You are not producing a big string with role markers in it; you are producing message objects that each client turns into the right wire representation for its vendor. That matters because vendors disagree about roles. Some carry system instructions as a separate top-level field, others as the first message; tool results are encoded differently again. Keeping messages typed until the last possible moment is what lets one `Prompt` be sent to more than one provider without rewriting it. ## The three message kinds `system(...)` carries instructions and persona: who the model is, what format to answer in, what it must never do. It is normally written once at the top. `user(...)` is the human turn — the actual request, plus whatever retrieved context or data you are injecting. `assistant(...)` is the one people misunderstand. It lets you place a *model* turn into the prompt yourself. Two uses dominate: few-shot examples, where you write alternating user/assistant pairs showing the exact answer shape you want, and conversation replay, where you rebuild a multi-turn dialogue from stored history so the model sees what it previously said. ## What the id is for The id is a label, not content. It never becomes text the model reads. Its value is operational: when a run appears in logs or in a trace, it is attributable to a named prompt, so you can ask "how is `ticket-triage` performing?" rather than trying to fingerprint prompt text. Give it a stable, human-meaningful name and keep it stable across edits to the body, because the whole point is to follow one prompt's behaviour over time. ## Parameters live beside the text, not inside it Sampling settings such as temperature are carried as `LLMParams` associated with the prompt rather than being smuggled into the message text. That keeps the knobs machine-readable — a trace can record the temperature actually used, and you can vary it per prompt without editing prose. ## Immutability and multi-turn flow A `Prompt` is a value. Adding a turn means producing a new prompt with an extra message, not mutating a shared object. For an agent this is exactly the right model: the growing conversation is data that the agent threads through its steps, and because it is plain data you can persist it, truncate it, summarise it, or inspect it in a test. It also means the executor stays stateless — nothing about the conversation lives inside the client or executor. ## Building is not sending Evaluating the builder block performs no I/O. There is no hidden network call, no key required, nothing suspends. You get an object. It reaches a model only when you pass it, together with an `LLModel`, to a `PromptExecutor`. This separation is why prompts are trivially unit-testable: construct one and assert on the messages it contains. ## Practical habits Keep the system message short and imperative; long systems drift and are hard to diff. Put variable data in the user turn, not the system turn, so the stable part stays stable. Use `assistant()` for few-shot rather than describing the desired output in prose — showing beats telling, and it keeps the system message small. Name ids by task, not by model, since the same prompt may run against different models. And do not encode secrets or per-user data in a system message you cache or reuse; the prompt is data that will end up in logs and traces if verbose capture is enabled. ## Common beginner mistakes Treating the id as something the model sees, so writing sentences into it. Building one giant `user()` string containing fake "System:" and "Assistant:" headers — that defeats the typed conversion and confuses tool-calling. Rebuilding the whole conversation from scratch on every turn while forgetting the earlier assistant replies, so the model loses the thread. And expecting the builder to call the model, then wondering why nothing happened.
- Why does Koog keep messages typed instead of concatenating them into one string?Because vendors encode roles differently — system instructions may be a separate field or the first message, and tool results have their own encoding. Keeping messages typed until the client serialises them lets the same `Prompt` be sent to different providers unchanged, and it keeps tool-call and tool-result turns machine-readable rather than buried in prose.
- How would you supply few-shot examples with this DSL?Add alternating `user()` and `assistant()` pairs before the real request: each pair shows an input and the exact reply shape you want. This is usually stronger than describing the format in the system message, and it keeps the system message short. Keep examples short and representative, since they cost tokens on every single call.
- Where does temperature belong if not in the message text?On the prompt's parameters, as `LLMParams`, rather than in prose. That keeps sampling settings machine-readable, so a trace can record the value actually used and you can vary it per prompt without rewriting text. Instructions in the message body cannot change sampling behaviour at all — the model has no control over its own decoding parameters.
saying these in an interview costs you the question
- Thinks the prompt id is text the model reads
- Writes fake role headers inside one user string
- Believes building the prompt already calls the model
- Uses assistant() only for the model's live replies, never few-shot
- Puts temperature instructions in the system message