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 | shCreate a project
norq init --exampleThis 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 :::tableiterates over an array and renders a table with computed subtotals- Variable pipe args —
{{item.price | multiply item.qty}}computes price × quantity - Pipes —
currency "USD",capitalize,date "%B %d, %Y",pluralize :::ifguards 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 lintThe linter checks all templates against the schema:
- Undefined variables —
{{bogus}}not in schema - Nullable access — using optional fields without
:::ifor| default - Inline directives —
:::mid-line (renders as literal text) - Directives in tables —
:::eachinside markdown tables (use:::tableinstead)
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 emailThe 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 testThe 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
norqextension for diagnostics, completions, and hover - Zed — install
norqfrom the extension registry - Any LSP editor — run
norq lspas 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
- Directives reference — all 16 directives and how they compile per channel
- Expressions and pipes — variables, 40 built-in pipes, variable pipe args
- Control flow — conditionals, loops,
:::table - Channel guides — per-channel best practices
- CLI reference — all commands and flags
- Testing — assertion syntax and strategies