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.