Getting Started with Norq

Norq is a notification-templates-as-code framework. You write templates in Markdown, and the compiler turns them into channel-native payloads — responsive HTML email, SMS text, Slack Block Kit, push notifications, WhatsApp messages, and MS Teams Adaptive Cards.

Try it in the browser — no install needed.

Install

# Rust
cargo install norq
 
# Node.js
npm install -g @suprsend/norq-cli
 
# Python
pip install norq
 
# Universal
curl -fsSL https://norq.sh/install.sh | sh

Create a project

norq init --example

This creates a project with three example notifications — one per type:

norq.config.yaml
notifications/
  system/
    security-alert/          ← security alert with conditional actions
  transactional/
    order-confirmation/      ← order with line items, computed totals
  promotional/
    weekly-digest/           ← content digest with conditional pro features

Each notification contains templates for multiple channels, a data schema, sample data, and tests.

The schema-first workflow

Norq follows a strict order: schema → samples → templates → lint. Skipping steps leads to bugs.

1. Define your data shape

Every variable used in templates must be declared here. Fields not in required are nullable — the linter enforces null guards.

# data.schema.yaml
type: object
required: [customer, items, order]
properties:
  customer:
    type: object
    required: [name, email]
    properties:
      name: { type: string }
      email: { type: string, format: email }
  items:
    type: array
    items:
      type: object
      required: [name, qty, price]
      properties:
        name: { type: string }
        qty: { type: integer }
        price: { type: number }
  order:
    type: object
    required: [id, total, payment_method]
    properties:
      id: { type: string }
      total: { type: number }
      payment_method: { type: string }
      estimated_delivery: { type: string, format: date }
      tracking_url: { type: string, format: uri }

estimated_delivery and tracking_url are not required — they're nullable. Templates must guard them with :::if or | default.

2. Create sample data

Create at least two samples: a "full" sample with all fields, and a "minimal" sample with only required fields. The minimal sample catches nullable access bugs.

# data.samples.yaml
samples:
  - name: "Full order"
    data:
      customer: { name: "Alice", email: "alice@example.com" }
      items:
        - { name: "Wireless Headphones", qty: 1, price: 79.99 }
        - { name: "USB-C Cable", qty: 3, price: 12.99 }
        - { name: "Phone Case", qty: 1, price: 24.99 }
      order:
        id: "ORD-2024-1847"
        total: 143.95
        payment_method: "credit card"
        estimated_delivery: "2024-12-01"
        tracking_url: "https://track.example.com/ORD-2024-1847"
  - name: "Minimal order"
    data:
      customer: { name: "Bob", email: "bob@example.com" }
      items:
        - { name: "Widget", qty: 1, price: 9.99 }
      order:
        id: "ORD-2024-1848"
        total: 9.99
        payment_method: "paypal"

The "Minimal order" sample has no estimated_delivery or tracking_url. If your template crashes with this sample, you have a nullable access bug.

3. Write templates

Each channel gets its own file. Start with email (most complex), then adapt for others.

email.md — uses directives for structure, :::table for line items, pipes for formatting:

---
subject: "Order #{{order.id}} confirmed — {{items | count}} {{items | count | pluralize 'item' 'items'}}"
preheader: "Thanks for your order"
---
 
::: header
# Order Confirmed
:::
 
Hi {{customer.name}},
 
Thank you for your order! Here's a summary of **{{items | count}} {{items | count | pluralize "item" "items"}}**:
 
::: table items as item
| Product | Qty | Price | Subtotal |
|---------|-----|-------|----------|
| {{item.name}} | {{item.qty}} | {{item.price | currency "USD"}} | {{item.price | multiply item.qty | currency "USD"}} |
:::
 
::: fields
Order ID: #{{order.id}}
Total: {{order.total | currency "USD"}}
Payment: {{order.payment_method | capitalize}}
::: if order.estimated_delivery
Estimated Delivery: {{order.estimated_delivery | date "%B %d, %Y"}}
:::
:::
 
::: if order.tracking_url
::: action
[Track Your Order]({{order.tracking_url}}){primary}
:::
:::
 
::: footer
Questions? [Contact support](https://support.example.com) · Order #{{order.id}}
:::

Key concepts in this template:

  • Frontmatter (---) sets email subject and preheader
  • Directives (:::header, :::fields, :::action, :::footer) compile to channel-native elements
  • :::table iterates over an array and renders a table with computed subtotals
  • Variable pipe args{{item.price | multiply item.qty}} computes price × quantity
  • Pipescurrency "USD", capitalize, date "%B %d, %Y", pluralize
  • :::if guards nullable fields (tracking_url, estimated_delivery)

sms.md — short, no directives, every character counts:

Order #{{order.id}} confirmed! Total: {{order.total | currency "USD"}}.
 
::: if order.tracking_url
Track it: {{order.tracking_url}}
:::

slack.md — uses Slack-native Block Kit elements:

::: header
Order #{{order.id}} Confirmed
:::
 
Hi {{customer.name}}, your order is confirmed.
 
::: fields
Order ID: #{{order.id}}
Total: {{order.total | currency "USD"}}
Payment: {{order.payment_method | capitalize}}
Items: {{items | count}}
:::
 
::: if order.tracking_url
::: action
[Track Order]({{order.tracking_url}}){primary}
:::
:::

Same data, different structure per channel. That's the core idea.

4. Lint

norq lint

The linter checks all templates against the schema:

  • Undefined variables{{bogus}} not in schema
  • Nullable access — using optional fields without :::if or | default
  • Inline directives::: mid-line (renders as literal text)
  • Directives in tables:::each inside markdown tables (use :::table instead)

Fix all warnings. They indicate real problems.

5. Compile and preview

# Compile to JSON
norq compile transactional/order-confirmation --channel email --sample "Full order" --json
 
# Start a live preview server
norq dev
 
# Preview a specific notification
norq preview transactional/order-confirmation --channel email

The compiled output for email includes subject, html, and text. For SMS it includes body and segments. For Slack it includes blocks (Block Kit JSON).

6. Test

Add assertions in tests.yaml:

tests:
  - name: "Email subject has order ID"
    channel: email
    sample: "Full order"
    assert:
      subject: { contains: "ORD-2024-1847" }
 
  - name: "Email subject has item count"
    channel: email
    sample: "Full order"
    assert:
      subject: { contains: "3 items" }
 
  - name: "SMS under 3 segments"
    channel: sms
    sample: "Full order"
    assert:
      sms_segments: { lte: 3 }
 
  - name: "Minimal order renders clean"
    all_channels: true
    sample: "Minimal order"
    assert:
      diagnostics: { errors: 0 }
 
  - name: "All channels render without errors"
    all_channels: true
    all_samples: true
    assert:
      diagnostics: { errors: 0 }

Run:

norq test

The last test is the most important — it verifies every channel renders with every sample, catching nullable access bugs.

7. Send

# Via console provider (prints to stdout — great for development)
norq send transactional/order-confirmation \
  --to '{"email":"alice@example.com"}' \
  --sample "Full order"

8. Send from code

import { Norq } from "@suprsend/norq";
 
const norq = new Norq();
const result = await norq.send("transactional/order-confirmation", {
  to: { email: "alice@example.com" },
  data: {
    customer: { name: "Alice", email: "alice@example.com" },
    items: [
      { name: "Wireless Headphones", qty: 1, price: 79.99 },
      { name: "USB-C Cable", qty: 3, price: 12.99 },
    ],
    order: {
      id: "ORD-2024-1847",
      total: 105.96,
      payment_method: "credit card",
      tracking_url: "https://track.example.com/ORD-2024-1847",
    },
  },
});

SDKs available for Node.js, Python, Go, Java, and Ruby.

Editor support

  • VS Code — install the norq extension for diagnostics, completions, and hover
  • Zed — install norq from the extension registry
  • Any LSP editor — run norq lsp as a language server

Notification types

Norq organizes notifications into three required type folders:

Type Folder When to use
System notifications/system/ Always delivered — password resets, security alerts
Transactional notifications/transactional/ Always delivered — order confirmations, shipping updates
Promotional notifications/promotional/ Subject to user preferences — digests, campaigns

Next steps