Skip to content

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?

RazorScriban
Sandbox❌ Full access to .NET✅ Isolated
DB StorageComplex✅ Simple
PerformanceSlow (compiles assemblies)✅ Fast (compiles IL)
DependenciesASP.NET CoreLightweight (~200KB)
SyntaxC# (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}} items

Conditionals

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

CategoryExamples
Stringstring.upcase, string.downcase, string.capitalize, string.truncate
Mathmath.format, math.round, math.ceil, math.floor
Datedate.to_string, date.now, date.add_days, date.add_hours
Arrayarray.size, array.first, array.last, array.sort
Regexregex.match, regex.replace

See the Scriban documentation for the complete reference.

Released under the MIT License.