Table of Contents

Build Dynamic Prompts with Templates

Hard-coded prompts break when requirements change. Adding a new tool means editing string concatenation. Swapping languages means duplicating code. PromptTemplate solves this by separating prompt structure from runtime data: you write the template once, then render it with different contexts.

For the concept overview, see the Prompt Templates glossary entry.


Why This Matters

Two production problems that prompt templates solve:

  1. Prompt configuration without code changes. When your system prompt depends on the user's role, available tools, or preferred language, templates let you store prompt structure in configuration files, databases, or version control and inject values at runtime. Changing the prompt never requires recompiling.
  2. Testable prompt logic. Templates are compiled objects with an inspectable variable list. You can unit-test that a template produces the expected output for given inputs, catching prompt regressions before they reach production.

Prerequisites

Requirement Minimum
.NET SDK 8.0+
LM-Kit.NET 2026.2.6+

No model is required for template parsing and rendering. A model is only needed if you feed the rendered prompt into a conversation or agent.


Step 1: Create the Project

dotnet new console -n PromptTemplateDemo
cd PromptTemplateDemo
dotnet add package LM-Kit.NET

Step 2: Parse and Render a Simple Template

Templates use Mustache-style {{variable}} placeholders by default. Call Parse once, then Render as many times as needed.

using LMKit.TextGeneration.Prompts;

// Parse once (compiles to an internal AST)
var template = PromptTemplate.Parse(
    "You are {{role}}. Help the user with {{topic}}."
);

// Render many times with different data
string prompt1 = template.Render(new PromptTemplateContext
{
    ["role"] = "a senior C# developer",
    ["topic"] = "async programming"
});

string prompt2 = template.Render(new PromptTemplateContext
{
    ["role"] = "a data scientist",
    ["topic"] = "feature engineering"
});

Step 3: Apply Filters and Defaults

Filters transform values inline using the pipe (|) syntax. Chain multiple filters left to right. Inline defaults provide fallback values for missing variables.

var template = PromptTemplate.Parse(
    "Welcome, {{name|trim|capitalize}}! Your role: {{role:user}}."
);

string result = template.Render(new PromptTemplateContext
{
    ["name"] = "  alice  "
    // "role" not set: renders as "user" (inline default)
});
// "Welcome, Alice! Your role: user."

Built-in Filters

Filter Example Result
upper {{name\|upper}} ALICE
lower {{name\|lower}} alice
trim {{name\|trim}} removes whitespace
capitalize {{name\|capitalize}} Alice
title {{name\|title}} Title Case
length {{name\|length}} character count
reverse {{name\|reverse}} reversed string
truncate:N {{text\|truncate:50}} truncated with ...
replace:old:new {{text\|replace:world:universe}} string replacement
default:val {{name\|default:Anonymous}} fallback value
json {{text\|json}} JSON-safe escaping

Step 4: Add Conditional Logic

Use {{#if condition}} to include prompt sections only when a variable is truthy (non-null, non-empty, non-false). Add {{#else}} for a fallback branch. Use {{#unless}} for negated conditions.

var template = PromptTemplate.Parse(@"
{{#if premium}}You are a premium support agent. Provide detailed answers.
{{#else}}You are a helpful assistant. Keep answers concise.
{{/if}}
{{#unless banned}}Feel free to use code examples.{{/unless}}");

string prompt = template.Render(new PromptTemplateContext
{
    ["premium"] = true,
    ["banned"] = false
});

Truthiness Rules

Value Truthy?
null No
false No
"" (empty string) No
"false", "0" No
0 (any numeric zero) No
Empty array/list No
Everything else Yes

Step 5: Loop Over Collections

{{#each collection}} iterates over arrays, lists, or any IEnumerable. Inside the loop, {{this}} references the current item, {{@index}} is the zero-based index, and {{@first}} is true for the first iteration. Object properties are exposed directly.

var template = PromptTemplate.Parse(@"You have access to these tools:
{{#each tools}}- {{name}}: {{description}}
{{/each}}");

string prompt = template.Render(new PromptTemplateContext
{
    ["tools"] = new[]
    {
        new { name = "calculator", description = "Perform arithmetic" },
        new { name = "web_search", description = "Search the web" }
    }
});

For simple string arrays, use {{this}}:

var template = PromptTemplate.Parse(@"Constraints:
{{#each constraints}}- {{this}}
{{/each}}");

Step 6: Scope Into Nested Objects

{{#with object}} opens a scope where the object's properties are directly accessible without dot notation.

var template = PromptTemplate.Parse(
    "{{#with user}}Agent for {{Name}} ({{Email}}){{/with}}"
);

string result = template.Render(new PromptTemplateContext
{
    ["user"] = new { Name = "Alice", Email = "alice@example.com" }
});
// "Agent for Alice (alice@example.com)"

Dot notation also works without {{#with}}:

var template = PromptTemplate.Parse("Hello {{user.Name}}!");

Step 7: Register Custom Helpers

Helpers are functions callable from within the template. Register them on the context.

var context = new PromptTemplateContext()
    .Set("name", "world");

context.RegisterHelper("now", _ =>
    DateTime.UtcNow.ToString("yyyy-MM-dd"));

context.RegisterHelper("shout", args =>
    args.Length > 0 ? args[0]?.ToString()?.ToUpper() + "!" : "");

var template = PromptTemplate.Parse("Today: {{now}}. {{shout name}}");
string result = template.Render(context);
// "Today: 2026-02-18. WORLD!"

Step 8: Register Custom Global Filters

Global filters are available to all templates in the process. Register them once at startup.

PromptTemplateFilters.Register("exclaim", (value, args) =>
    value + "!!!");

PromptTemplateFilters.Register("pad", (value, args) =>
{
    int width = args.Length > 0 ? int.Parse(args[0]) : 10;
    return value.PadRight(width);
});

var template = PromptTemplate.Parse("{{message|exclaim}}");
string result = template.Render(new PromptTemplateContext
{
    ["message"] = "hello"
});
// "hello!!!"

Step 9: Use Alternative Syntaxes

If Mustache braces conflict with your content (e.g., JSON examples in prompts), switch to Dollar or Percent syntax.

// Dollar syntax: ${variable} or $variable
var dollar = PromptTemplate.Parse(
    "Hello ${name|upper}!",
    new PromptTemplateOptions { Syntax = PromptTemplateSyntax.Dollar }
);

// Percent syntax: %variable%
var percent = PromptTemplate.Parse(
    "Hello %name|upper%!",
    new PromptTemplateOptions { Syntax = PromptTemplateSyntax.Percent }
);

Dollar and Percent syntaxes support variables and filters but not block constructs (if, each, with).


Step 10: Handle Errors and Validate Templates

Use TryParse for user-supplied templates. Enable StrictVariables to catch missing variables at render time.

// Safe parsing
if (PromptTemplate.TryParse(userInput, out var template, out var errors))
{
    // Inspect required variables
    Console.WriteLine($"Variables: {string.Join(", ", template.Variables)}");

    string result = template.Render(context);
}
else
{
    foreach (string error in errors)
        Console.WriteLine($"Parse error: {error}");
}

// Strict mode
var options = new PromptTemplateOptions { StrictVariables = true };
var strict = PromptTemplate.Parse("Hello {{name}}", options);

try
{
    strict.Render(new PromptTemplateContext()); // throws: missing "name"
}
catch (PromptTemplateException ex)
{
    Console.WriteLine(ex.Message); // "Missing required variable: name"
}

Step 11: Combine with Agents

Use templates to build agent system prompts dynamically based on runtime configuration.

using LMKit.Model;
using LMKit.Agents;
using LMKit.TextGeneration.Prompts;

var instructionTemplate = PromptTemplate.Parse(@"You are an expert in {{domain}}.
{{#if constraints}}Constraints:
{{#each constraints}}- {{this}}
{{/each}}{{/if}}
{{#if tools}}Available tools:
{{#each tools}}- {{name}}: {{description}}
{{/each}}{{/if}}
Always respond in {{language:English}}.");

string instruction = instructionTemplate.Render(new PromptTemplateContext
{
    ["domain"] = "machine learning",
    ["constraints"] = new[] { "Be concise", "Cite sources" },
    ["tools"] = new[]
    {
        new { name = "web_search", description = "Search the web" },
        new { name = "calculator", description = "Math operations" }
    },
    ["language"] = "French"
});

using var model = LM.LoadFromModelID("qwen3:8b");

var agent = Agent.CreateBuilder(model)
    .WithPersona("ML Expert")
    .WithInstruction(instruction)
    .Build();

Complete Example

using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
using LMKit.TextGeneration.Prompts;
using LMKit.TextGeneration.Sampling;

LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// Define a template for the system prompt
var systemTemplate = PromptTemplate.Parse(@"You are an expert assistant specializing in {{domain}}.
{{#if verbose}}Provide thorough explanations with examples.{{#else}}Keep answers concise.{{/if}}
Always respond in {{language:English}}.");

// Render with runtime configuration
string systemPrompt = systemTemplate.Render(new PromptTemplateContext
{
    ["domain"] = "cloud architecture",
    ["verbose"] = false,
    ["language"] = "English"
});

Console.WriteLine($"System prompt:\n{systemPrompt}\n");

// Use the rendered prompt in a conversation
using LM model = LM.LoadFromModelID("gemma3:4b",
    loadingProgress: p => { Console.Write($"\rLoading: {p * 100:F0}%"); return true; });
Console.WriteLine();

var chat = new MultiTurnConversation(model)
{
    MaximumCompletionTokens = 1024,
    SamplingMode = new RandomSampling { Temperature = 0.7f },
    SystemPrompt = systemPrompt
};

var result = chat.Submit("What is the CAP theorem?", CancellationToken.None);
Console.WriteLine(result.Content);

Summary

Construct Syntax Purpose
Variable {{name}} Inject a value
Dot notation {{user.email}} Access nested properties
Filter {{name\|upper}} Transform inline
Chain {{name\|trim\|upper}} Multiple transformations
Default {{role:user}} Fallback for missing values
Conditional {{#if}}...{{#else}}...{{/if}} Toggle sections
Negation {{#unless}}...{{/unless}} Negated conditional
Loop {{#each items}}...{{/each}} Iterate collections
Scope {{#with obj}}...{{/with}} Access object properties
Helper {{helperName arg}} Custom functions