C# Scriban
last modified October 18, 2026
In this article we show how to use the Scriban template engine in C#.
Scriban template engine
A template engine or template processor is a library designed to combine templates with a data model to produce documents. Template engines are often used to generate large amounts of emails, in source code preprocessing, or producing dynamic HTML pages.
A template consists of static parts and dynamic parts. The dynamic parts are later replaced with data. The rendering function combines the template with the data to produce the final document.
Scriban uses {{ }} for code blocks and {%{ }%} for
escape blocks. Any other text is considered as a text block and is output as
is. Scriban can also parse templates written in the Liquid language.
The Template.Parse method parses the text into a template and the
Template.Render method renders the template into the final output
using the provided data model.
$ dotnet add package Scriban
We add the Scriban package to the project.
Scriban simple example
The following is a simple Scriban example.
using Scriban;
var name = "John Doe";
var tpl = Template.Parse("Hello {{ name }}!");
var res = tpl.Render(new { name });
Console.WriteLine(res);
The program prints a small message to the console.
var tpl = Template.Parse("Hello {{ name }}!");
The Template.Parse method parses the specified text into a
template. With {{ }} we output the content of the
name variable.
var res = tpl.Render(new { name });
The Render method renders the template using the specified model.
We pass an anonymous object; its name property becomes the
name variable in the template.
$ dotnet run Hello John Doe!
In the next example, we pass two variables to the template engine.
using Scriban;
var name = "John Doe";
var occupation = "gardener";
var tpl = Template.Parse("{{ name }} is a {{ occupation }}");
var res = tpl.Render(new { name, occupation });
Console.WriteLine(res);
The program prints a message using data from two variables: name
and occupation.
$ dotnet run John Doe is a gardener
Scriban for loop
Loops are created with the for statement.
using Scriban;
string[] words = ["sky", "blue", "falcon", "book", "ocean", "dog"];
var html = """
<ul>
{{- for word in words }}
<li>{{ word }}</li>
{{- end }}
</ul>
""";
var tpl = Template.Parse(html);
var res = tpl.Render(new { words });
Console.WriteLine(res);
The example uses the for statement to output an array of words
into an HTML list. The template is written as a raw string literal, which spans
multiple lines and needs no escaping of quotes.
{{- for word in words }}
<li>{{ word }}</li>
{{- end }}
We iterate over the array of words. The - character after the
opening braces strips the whitespace, including the newline, on the left of the
code block. Similarly, -}} strips the whitespace on the right.
Without it, each code block would leave an empty line in the output.
$ dotnet run
<ul>
<li>sky</li>
<li>blue</li>
<li>falcon</li>
<li>book</li>
<li>ocean</li>
<li>dog</li>
</ul>
Scriban string functions
We can use several string functions in the templates. Functions are applied
with the pipe operator |; the value on the left becomes the first
argument of the function.
using Scriban;
var msg = "an old falcon";
var data = """
{{ msg | string.capitalize }}
{{ msg | string.upcase }}
The message has {{ msg | string.size }} characters.
The message has {{ msg | string.split ' ' | array.size }} words.
""";
var tpl = Template.Parse(data);
var res = tpl.Render(new { msg });
Console.WriteLine(res);
We have a string message. We apply several string functions on the message.
{{ msg | string.capitalize }}
{{ msg | string.upcase }}
We capitalize and upcase the message.
The message has {{ msg | string.size }} characters.
We count the number of characters in the message.
The message has {{ msg | string.split ' ' | array.size }} words.
We count the number of words in the message. The pipes can be chained: the
result of string.split is passed to array.size.
$ dotnet run An old falcon AN OLD FALCON The message has 13 characters. The message has 3 words.
Scriban array functions
Scriban has built-in array functions.
using Scriban;
int[] vals = [2, 1, -3, 0, -1, -2, 3];
var data = """
The array has {{ vals.size }} elements
First element: {{ vals[0] }}
Last element: {{ vals[-1] }}
Sorted elements: {{ vals | array.sort }}
Reversed elements: {{ vals | array.reverse }}
Contains 0: {{ vals | array.contains 0 }}
Joined: {{ vals | array.join ', ' }}
""";
var tpl = Template.Parse(data);
var res = tpl.Render(new { vals });
Console.WriteLine(res);
We have an array of integers. With built-in array functions, we count, sort, reverse, search, and join the elements. A negative index counts from the end of the array.
$ dotnet run The array has 7 elements First element: 2 Last element: 3 Sorted elements: [-3, -2, -1, 0, 1, 2, 3] Reversed elements: [3, -2, -1, 0, -3, 1, 2] Contains 0: true Joined: 2, 1, -3, 0, -1, -2, 3
Scriban conditions
We can use if/else if/else conditions in templates.
using Scriban;
string?[] names = ["John", "Nelly", null, "George"];
var data = """
{{- for name in names }}
{{- if !name }}
Hello there!
{{- else }}
Hello {{ name }}!
{{- end }}
{{- end }}
""";
var tpl = Template.Parse(data);
var res = tpl.Render(new { names });
Console.WriteLine(res);
We have an array of names. With the if condition, we check if the
element is null. In Scriban, null and false are
falsy values.
$ dotnet run Hello John! Hello Nelly! Hello there! Hello George!
Scriban read template from file
In the following example, we read the template from a file.
<table>
<thead>
<tr>
<th>Name</th>
<th>Occupation</th>
</tr>
</thead>
<tbody>
{{- for user in users }}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.occupation }}</td>
</tr>
{{- end }}
</tbody>
</table>
The template file outputs data in an HTML table.
using Scriban;
List<User> users =
[
new("John Doe", "gardener"),
new("Roger Roe", "driver"),
new("Lucy Smith", "teacher")
];
var fileName = "users.tpl";
var data = File.ReadAllText(fileName);
var tpl = Template.Parse(data, fileName);
var res = tpl.Render(new { users });
Console.WriteLine(res);
record User(string Name, string Occupation);
The data is stored in a list of records. The template file is loaded with
the File.ReadAllText method.
var tpl = Template.Parse(data, fileName);
The optional second parameter is the source path; it is used in error messages.
<td>{{ user.name }}</td>
By default, Scriban exposes .NET members in snake case. The Name
property of the record is accessed as name; a property such as
FirstName would be accessed as first_name.
$ dotnet run
<table>
<thead>
<tr>
<th>Name</th>
<th>Occupation</th>
</tr>
</thead>
<tbody>
<tr>
<td>John Doe</td>
<td>gardener</td>
</tr>
<tr>
<td>Roger Roe</td>
<td>driver</td>
</tr>
<tr>
<td>Lucy Smith</td>
<td>teacher</td>
</tr>
</tbody>
</table>
Scriban parse errors
A template with a syntax error does not throw an exception when parsed.
Instead, we check the HasErrors property and the
Messages collection.
using Scriban;
var tpl = Template.Parse("Hello {{ name }!");
if (tpl.HasErrors)
{
foreach (var msg in tpl.Messages)
{
Console.WriteLine(msg);
}
return;
}
Console.WriteLine(tpl.Render(new { name = "John" }));
The code block is missing one closing brace. Calling Render on such
a template would throw an InvalidOperationException.
$ dotnet run
<input>(1,15) : error : Invalid token found `}`. Expecting <EOL>/end of line.
<input>(1,15) : error : Unexpected } while no matching {
Scriban custom functions
For more control over the rendering, we use a TemplateContext
with a ScriptObject. A script object holds variables and
functions that are available in the template.
using System.Globalization;
using Scriban;
using Scriban.Runtime;
var culture = CultureInfo.GetCultureInfo("en-US");
var so = new ScriptObject();
so.Add("customer", "Roger Roe");
so.Add("items", new[] { 2.5m, 10.0m, 4.25m });
so.Import("money", new Func<decimal, string>(v => v.ToString("C2", culture)));
var context = new TemplateContext();
context.PushGlobal(so);
var data = """
Customer: {{ customer }}
{{- total = 0 }}
{{- for item in items }}
item {{ for.index + 1 }}: {{ item | money }}
{{- total = total + item }}
{{- end }}
Total: {{ total | money }}
""";
var tpl = Template.Parse(data);
Console.WriteLine(tpl.Render(context));
The program prints a simple invoice. The prices are formatted with a custom
money function.
so.Import("money", new Func<decimal, string>(v => v.ToString("C2", culture)));
The Import method registers a .NET delegate as a template function.
var context = new TemplateContext(); context.PushGlobal(so);
We push the script object to the global scope of the context and render the template with the context instead of a model object.
{{- total = 0 }}
...
{{- total = total + item }}
Templates can define their own variables. An assignment produces no output. The
for.index variable holds the zero-based index of the current
iteration.
$ dotnet run Customer: Roger Roe item 1: $2.50 item 2: $10.00 item 3: $4.25 Total: $16.75
Scriban Liquid templates
Liquid is a template language created by Shopify. Scriban can parse Liquid
templates with the Template.ParseLiquid method.
using Scriban;
var data = """
{% for user in users -%}
{{ user.name | upcase }} is {{ user.age }} years old
{% endfor -%}
""";
var users = new[]
{
new { name = "John Doe", age = 34 },
new { name = "Lucy Smith", age = 28 }
};
var tpl = Template.ParseLiquid(data);
Console.WriteLine(tpl.Render(new { users }));
Liquid uses {% %} for statements and {{ }} for
output. Liquid filters, such as upcase, are mapped to Scriban's
built-in functions.
$ dotnet run JOHN DOE is 34 years old LUCY SMITH is 28 years old
Source
In this article we have worked with the Scriban template engine in C#.
Author
List all C# tutorials.