Skip to content

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


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:

{{instruction_persona}}

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

  1. before_user_as_system is the default recommendation — most modern chat-tuned models treat system turns as highest-priority.
  2. If the model ignores the context, try before_user_as_user.
  3. in_place_of_user is useful when you want to completely control the user-side turn (must include {{user_message}}).
  4. 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_user without {{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_user placements {{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 your Retrieve node has source: "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.