Appearance
Templates
GrydNotifications uses Scriban as its template engine. Templates are stored in the database with support for locale and tenant fallback, versioning, and variable detection.
Why Scriban?
| Razor | Scriban | |
|---|---|---|
| Sandbox | ❌ Full access to .NET | ✅ Isolated |
| DB Storage | Complex | ✅ Simple |
| Performance | Slow (compiles assemblies) | ✅ Fast (compiles IL) |
| Dependencies | ASP.NET Core | Lightweight (~200KB) |
| Syntax | C# (complex for non-devs) | Liquid-like (simple) |
Template Syntax
Scriban uses for expressions and {% %} for statements (Gryd uses the default syntax).
Variables
Hello {{name}}, your order #{{order_id}} is confirmed.Filters (Pipes)
Total: {{total | math.format "C2"}}
Date: {{created_at | date.to_string "%d/%m/%Y"}}
Name: {{name | string.upcase}}
Items: {{items | array.size}} itemsConditionals
{{if priority == "Critical"}}
⚠️ URGENT: {{subject}}
{{else}}
{{subject}}
{{end}}Loops
{{for item in items}}
<tr>
<td>{{item.name}}</td>
<td>{{item.quantity}}</td>
<td>{{item.price | math.format "C2"}}</td>
</tr>
{{end}}Full HTML Email Template Example
html
<html>
<body style="font-family: Arial, sans-serif;">
<h1>Order #{{order_id}} Confirmed</h1>
<p>Hi {{customer_name}},</p>
<p>Thank you for your order! Here's a summary:</p>
<table border="1" cellpadding="8" cellspacing="0">
<thead>
<tr><th>Product</th><th>Qty</th><th>Price</th></tr>
</thead>
<tbody>
{{for item in items}}
<tr>
<td>{{item.product_name}}</td>
<td>{{item.quantity}}</td>
<td>{{item.price | math.format "C2"}}</td>
</tr>
{{end}}
</tbody>
</table>
<p><strong>Total: {{total | math.format "C2"}}</strong></p>
{{if notes}}
<p>Notes: {{notes}}</p>
{{end}}
<p>Thanks,<br/>The {{company_name}} Team</p>
</body>
</html>Template Management
Creating Templates via API
http
POST /api/v1/notification-templates
Content-Type: application/json
{
"slug": "order-confirmation",
"name": "Order Confirmation",
"subjectTemplate": "Order #{{order_id}} Confirmed",
"htmlBodyTemplate": "<h1>Hi {{customer_name}}</h1>...",
"textBodyTemplate": "Hi {{customer_name}}, your order is confirmed.",
"locale": "en-US",
"requiredVariables": "order_id,customer_name,total"
}Creating Templates via Code
csharp
await _mediator.Send(new CreateTemplateCommand
{
Slug = "welcome-email",
Name = "Welcome Email",
SubjectTemplate = "Welcome to {{app_name}}, {{user_name}}!",
HtmlBodyTemplate = "<h1>Welcome, {{user_name}}!</h1><p>Get started at <a href=\"{{dashboard_url}}\">your dashboard</a>.</p>",
TextBodyTemplate = "Welcome, {{user_name}}! Get started at {{dashboard_url}}",
Locale = "en-US",
RequiredVariables = "app_name,user_name,dashboard_url"
}, ct);Sending with Templates
csharp
await _mediator.Send(new SendNotificationCommand
{
Channel = NotificationChannel.Email,
Recipients = [new RecipientDto("user@example.com", "John")],
TemplateSlug = "order-confirmation",
TemplateData = new Dictionary<string, object>
{
["order_id"] = "ORD-12345",
["customer_name"] = "John",
["items"] = new[]
{
new { product_name = "Widget", quantity = 2, price = 9.99m },
new { product_name = "Gadget", quantity = 1, price = 29.99m }
},
["total"] = 49.97m,
["company_name"] = "MyApp"
}
}, ct);Locale Fallback
Templates follow a 4-level resolution strategy:
1. Exact match: slug + locale + tenantId → "welcome:pt-BR:tenant123"
2. Locale only: slug + locale → "welcome:pt-BR"
3. Tenant only: slug + tenantId → "welcome:tenant123"
4. Default: slug → "welcome"This allows you to:
- Create a default template (no locale, no tenant)
- Override for specific locales (e.g.,
pt-BR) - Override for specific tenants (white-label)
- Override for specific tenant + locale combinations
Example
csharp
// 1. Create the default template
await _mediator.Send(new CreateTemplateCommand
{
Slug = "welcome",
Name = "Welcome (Default)",
SubjectTemplate = "Welcome!",
HtmlBodyTemplate = "<h1>Welcome, {{name}}!</h1>",
Locale = null // default
}, ct);
// 2. Create a pt-BR override
await _mediator.Send(new CreateTemplateCommand
{
Slug = "welcome",
Name = "Welcome (Portuguese)",
SubjectTemplate = "Bem-vindo!",
HtmlBodyTemplate = "<h1>Bem-vindo, {{name}}!</h1>",
Locale = "pt-BR"
}, ct);When sending to a Brazilian user, the pt-BR template is used. For all other locales, the default template is used.
Template Validation
Validate template syntax and discover variables before saving:
http
POST /api/v1/notification-templates/render
Content-Type: application/json
{
"templateSlug": "order-confirmation",
"data": {
"order_id": "PREVIEW",
"customer_name": "Preview User",
"total": 0.00
}
}Programmatically validate syntax:
csharp
// Via INotificationTemplateRenderer
var result = await _renderer.ValidateAsync("Hello {{name}}, your code is {{code}}");
result.Data!.IsValid; // true
result.Data!.DetectedVariables; // ["name", "code"]
result.Data!.Errors; // [] (empty for valid templates)Inline Templates
For one-off notifications, render a Scriban template without storing it:
csharp
var rendered = await _renderer.RenderInlineAsync(
"Hello {{name}}, your code is {{code}}",
new Dictionary<string, object>
{
["name"] = "John",
["code"] = "ABC123"
});
// rendered.Data == "Hello John, your code is ABC123"Template Caching
Compiled Scriban templates are cached in-memory using a ConcurrentDictionary. The cache key includes:
{slug}:{locale}:{tenantId}:{version}Templates are recompiled only when the version changes. This provides near-zero overhead for repeated renders.
Built-in Scriban Functions
All standard Scriban built-in functions are available:
| Category | Examples |
|---|---|
| String | string.upcase, string.downcase, string.capitalize, string.truncate |
| Math | math.format, math.round, math.ceil, math.floor |
| Date | date.to_string, date.now, date.add_days, date.add_hours |
| Array | array.size, array.first, array.last, array.sort |
| Regex | regex.match, regex.replace |
See the Scriban documentation for the complete reference.