Use Mustache Templates for Prompt Projection¶
Control exactly how retrieved knowledge and instructions land in the prompt
by writing a Mustache template on your
Generate node's template parameter and choosing a placement.
Prerequisites
- A working agent — see Build a chat agent with a graph.
- For RAG templates: a RAG agent that retrieves chunks — see Create a Simple RAG Assistant.
How projection works¶
Before each Generate call the server builds a Mustache context from
the current turn's data and renders the node's template string. The
rendered text is then spliced into the prompt according to placement.
The context contains:
| Variable | Type | Contents |
|---|---|---|
user_message |
string | The raw user-message text for this turn. |
instructions |
list of {name, text} |
All InstructionComponents from the current interaction (one per InstructionNode that ran). |
instruction_<name> |
string | Direct lookup for one instruction by node name, e.g. {{instruction_greeter}}. |
knowledge |
list of {name, has_chunks, chunks:[{id,text,distance}]} |
All KnowledgeComponents from the current interaction (one per Retrieve node). |
knowledge_<source> |
list of {id,text,distance} |
Direct lookup for one retriever's chunks by source label, e.g. {{#knowledge_rag}}. |
Each knowledge item carries a has_chunks boolean that is true when the retriever found at least one chunk and false when it returned nothing. Use it to gate the context preamble without the preamble accidentally repeating once per chunk:
{{#knowledge}}
{{#has_chunks}}Context from {{name}}:
{{#chunks}}- {{text}}
{{/chunks}}{{/has_chunks}}
{{^chunks}}No relevant information was found for this topic.{{/chunks}}
{{/knowledge}}
No HTML escaping
All variable values are inserted verbatim — apostrophes, quotation marks, and
ampersands are never HTML-encoded. You do not need triple-brace syntax
({{{variable}}}).
Placement values¶
| Value string | Effect |
|---|---|
in_place_of_user |
The rendered template replaces the user turn (default). Requires {{user_message}} in the template. |
before_user_as_user |
Emitted as an extra user turn before the real user message. |
before_user_as_system |
Emitted as a system turn before the user message. |
after_user_as_user |
Emitted as an extra user turn after the real user message. |
after_user_as_system |
Emitted as a system turn after the user message. |
For any placement other than in_place_of_user you should not put
{{user_message}} in the template — the real user message appears as its
own separate turn.
Step 1 — Simple RAG template¶
The most common pattern: inject all retrieved chunks before the user message:
from tryll_client import GraphDescription, NodeType
RAG_TEMPLATE = (
"{{#knowledge}}"
"{{name}}:\n"
"{{#chunks}}- {{text}}\n{{/chunks}}"
"\n{{/knowledge}}"
)
graph = (
GraphDescription()
.add_node("retrieve", NodeType.Retrieve, {
"embedded_string_storage": "my_kb",
"embedding_model": "nomic-embed-text-v1.5",
"source": "docs",
})
.add_node("generate", NodeType.Generate, {
"template": RAG_TEMPLATE,
"placement": "before_user_as_system",
"system_prompt": "Answer using the context above.",
})
.wire("retrieve", "found", "generate")
.wire("retrieve", "not_found", "generate")
.wire("generate", "default", "END")
.set_start_node("retrieve")
.set_default_model_name("Llama 3.2 3B Instruct (Q4_K_M)")
)
namespace TC = Tryll::Client;
constexpr std::string_view kRagTemplate =
"{{#knowledge}}"
"{{name}}:\n"
"{{#chunks}}- {{text}}\n{{/chunks}}"
"\n{{/knowledge}}";
TC::GraphDescription graph;
graph.AddNode("retrieve", TC::NodeType::Retrieve, {
{"embedded_string_storage", "my_kb"},
{"embedding_model", "nomic-embed-text-v1.5"},
{"source", "docs"},
})
.AddNode("generate", TC::NodeType::Generate, {
{"template", std::string{kRagTemplate}},
{"placement", "before_user_as_system"},
{"system_prompt", "Answer using the context above."},
})
.Wire("retrieve", "found", "generate")
.Wire("retrieve", "not_found", "generate")
.Wire("generate", "default", "END")
.SetStartNode("retrieve")
.SetDefaultModelName("Llama 3.2 3B Instruct (Q4_K_M)");
Step 2 — Direct per-source lookup¶
When your graph has multiple Retrieve nodes with distinct source labels
you can access each one directly and format them differently:
{{#knowledge_rules}}
Rule: {{text}}
{{/knowledge_rules}}
{{#knowledge_lore}}
Lore: {{text}}
{{/knowledge_lore}}
Each knowledge_<source> section is a list of chunk objects, so you iterate
over it with {{#knowledge_rules}}…{{/knowledge_rules}}.
Step 3 — Instructions from InstructionNode¶
Use an InstructionNode to inject a changeable instruction string into the
prompt without touching the graph:
INSTRUCTION_TEMPLATE = "{{#instructions}}{{text}}\n{{/instructions}}"
graph = (
GraphDescription()
.add_node("persona", NodeType.Instruction, {
"instruction": "You are a friendly guide.",
})
.add_node("generate", NodeType.Generate, {
"template": INSTRUCTION_TEMPLATE,
"placement": "before_user_as_system",
})
.wire("persona", "default", "generate")
.wire("generate", "default", "END")
.set_start_node("persona")
.set_default_model_name("Llama 3.2 3B Instruct (Q4_K_M)")
)
# Later, change persona without recreating the agent:
agent.change_param("persona", "instruction", "You are a sarcastic pirate.")
TC::GraphDescription graph;
graph.AddNode("persona", TC::NodeType::Instruction, {
{"instruction", "You are a friendly guide."},
})
.AddNode("generate", TC::NodeType::Generate, {
{"template", "{{#instructions}}{{text}}\n{{/instructions}}"},
{"placement", "before_user_as_system"},
})
.Wire("persona", "default", "generate")
.Wire("generate", "default", "END")
.SetStartNode("persona")
.SetDefaultModelName("Llama 3.2 3B Instruct (Q4_K_M)");
// Later:
agent.ChangeParam("persona", "instruction", "You are a sarcastic pirate.");
Use {{instruction_<name>}} for a direct single-instruction lookup:
Step 4 — Combine instructions and knowledge¶
Nothing prevents you from using both in one template:
{{#instructions}}{{text}}
{{/instructions}}
{{#knowledge}}{{name}}:
{{#chunks}}- {{text}}
{{/chunks}}
{{/knowledge}}
With placement: before_user_as_system the rendered block becomes a system
turn immediately before the user message.
Placement guidance¶
before_user_as_systemis the default recommendation — most modern chat-tuned models treat system turns as highest-priority.- If the model ignores the context, try
before_user_as_user. in_place_of_useris useful when you want to completely control the user-side turn (must include{{user_message}}).- The
after_*variants are uncommon; they exist for models that prefer seeing the question before the context.
Verify it worked¶
Create the agent with enable_diagnostics=True (Python) /
enableDiagnostics=true (C++). The TurnComplete.debug_info JSON
includes the full rendered prompt for each Generate node, so you
can inspect exactly what the model received.
Common pitfalls¶
in_place_of_userwithout{{user_message}}. The user message is silently dropped. Add{{user_message}}to the template or use a different placement.- Placement with
{{user_message}}. For non-in_place_of_userplacements{{user_message}}expands to the empty string — the user message is emitted as its own separate turn. Remove it from the template. - Unknown sections. Mustache silently ignores unknown section names.
If
{{#knowledge_rag}}renders nothing, check that yourRetrievenode hassource: "rag". - Empty knowledge — Before/After placements. When the template renders
to an empty string (e.g. a
{{#has_chunks}}block with no chunks), the server skips injecting the extra message entirely. The user message still goes through on its own, so the model is never left with an empty turn. - Empty knowledge —
in_place_of_user. If the rendered string is empty the server falls back to emitting the raw user message, so the model always receives the question.