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:
- 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.
- 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 |