CLI Reference
The norq CLI is a single binary that handles all Norq operations: project setup, linting, compilation, testing, preview, sending, code generation, and editor integration.
Installation
Coming soon. Package registry distribution is not yet available. See norq.sh for updates. To build from source:
make install(requires Rust toolchain).
Commands
norq init
Create a new Norq project in the current directory.
norq init # minimal project (config only)
norq init --example # project with example templatesCreates norq.config.yaml and notifications/ directory with system/, transactional/, and promotional/ type folders. With --example, adds three ready-to-use notifications:
| Notification | Channels |
|---|---|
system/security-alert |
email, sms, slack, push |
transactional/order-confirmation |
email, sms, slack, push |
promotional/weekly-digest |
email, sms, slack, push |
Each notification includes data.schema.yaml, data.samples.yaml, and tests.yaml.
norq new
Scaffold a new notification directory.
norq new transactional/order-shipped
norq new transactional/order-shipped --channels email,sms,slack
norq new promotional/marketing/promotions/welcome-offer --channels emailCreates:
notifications/transactional/order-shipped/
email.md
data.schema.yaml
data.samples.yaml
The --channels flag controls which channel template files are created. Without it, only email.md is scaffolded. With --channels email,sms,slack, the directory also includes sms.md and slack.md.
The first segment must be a notification type (system, transactional, or promotional). Within each type folder, supports category/subcategory paths with / separators (up to 4 segments: type/category/subcategory/name). Each segment must match [a-z0-9]+(-[a-z0-9]+)* (lowercase, hyphen-separated). Intermediate directories are created automatically.
norq lint
Lint notification templates for structure, syntax, and deliverability issues.
norq lint # all notifications
norq lint transactional/order-shipped # one notification
norq lint --json # JSON output (for CI/tooling)| Flag | Description |
|---|---|
--json |
Output diagnostics as JSON |
Output is sorted by max severity per file (Error-bearing files first, then
Warning-bearing, then Info-only), then by severity within each file, then by
line number. norq lint exits non-zero when any diagnostic has severity Error.
Norq checks 96 rules across structure, template syntax, accessibility, brand health, and channel-specific deliverability:
| Category | Rules | Channels |
|---|---|---|
| Structure | 9 rules | All |
| Template syntax | 15 rules | All |
| Accessibility (WCAG) | 9 rules | All |
| Email deliverability | 17 rules | |
| Brand | 9 rules | Project-level (one pass per brand.yaml) |
| SMS | 6 rules | SMS |
| Push | 10 rules | Push |
| 6 rules | ||
| Slack | 3 rules | Slack |
| MS Teams | 2 rules | MS Teams |
Brand lint rules (run once per project against brand.yaml):
| Code | Severity | Description |
|---|---|---|
brand/broken-ref |
error | A {path.to.token} alias or basedOn target doesn’t exist. |
brand/cyclic-ref |
error | Token alias chain or basedOn chain forms a cycle. |
brand/unknown-pipe |
error | Color expression uses an unsupported pipe (only darken, lighten, alpha, mix, contrast in v1). |
brand/invalid-pipe-arg |
error | Pipe receives the wrong arg shape (e.g. percentage out of range). |
brand/bad-color |
error | Color value is not a parseable sRGB hex. |
brand/invalid-dimension |
error | Dimension uses a unit other than px / rem. |
brand/missing-primary |
warn | No colors.brand token. Most templates expect a primary brand color. |
brand/missing-typography |
warn | An element-typography.<tag> mapping points at a typography token that isn’t defined. |
brand/font-not-delivered |
warn | A typography token’s fontFamily references {fonts.X} but no fonts.X block is declared. |
brand/orphaned-token |
warn | A custom token is defined but no other brand entry references it (the 17 standard EmailTheme color names and the body/code typography tokens are exempt; templates aren’t scanned). |
brand/unknown-element-type |
warn | defaults.<x> targets an element the email compiler doesn’t know about (only image, button, heading, text, divider, section are recognised). |
email/low-contrast is also extended to scan brand.styles.* bundles that declare both bg and color – references resolve through the brand, so a style like { bg: "{colors.brand}", color: "{colors.body}" } will fire if the resolved pair fails WCAG AA.
Template attr token rules (run on every directive param + block attr):
| Code | Severity | Description |
|---|---|---|
template/unknown-color-token |
error | A color= value is neither a #hex literal nor a name defined in brand.tokens.colors. Diagnostic message lists every valid option for the current project. |
template/unknown-bg-token |
error | Same as above for bg=. |
template/nested-columns-unsupported |
warn | A :::columns directive is nested inside a :::col ancestor (including via :::section, which is parser-shorthand for :::columns + :::col). The inner columns silently disappear during compilation; the fix is to place column blocks as siblings with a shared bg. |
These rules supersede the older template/invalid-attr-value for bg/color keys – attr values that previously rendered as empty backgrounds now fail the lint with an actionable message.
Push lint rules:
| Code | Severity | Description |
|---|---|---|
push/missing-title |
error | No title source (frontmatter, :::header, or first heading). |
push/title-length |
warn | Title exceeds 50 characters. |
push/body-length |
info | Body exceeds 120 characters. |
push/payload-size |
warn | Payload exceeds 4 KB. |
push/too-many-actions |
warn | More than 2 action buttons. |
push/unknown-ios-key |
warn | ios: frontmatter has an unknown key. |
push/unknown-android-key |
warn | android: frontmatter has an unknown key. |
push/unknown-web-key |
warn | web: frontmatter has an unknown key. |
push/invalid-priority |
error | android.priority must be normal or high. |
push/invalid-interruption-level |
error | ios.interruption-level must be one of passive, active, time-sensitive, critical. |
See Lint Rules for the complete reference with descriptions, severity levels, and fix guidance.
norq compile
Compile a notification template into channel payloads.
norq compile transactional/welcome --json
norq compile transactional/welcome --channel email --json
norq compile transactional/welcome --sample "New user" --json
norq compile transactional/welcome --data ./data.json --json
norq compile transactional/welcome --channel email --sample "New user" --json| Flag | Description |
|---|---|
--channel <c> |
Compile only this channel |
--data <file> |
Path to a JSON data file |
--sample <name> |
Named sample from data.samples.yaml |
--json |
Output as JSON (default for programmatic use) |
--strict |
Force strict-mode runtime validation on (overrides config) |
--no-strict |
Force strict-mode off (overrides config) |
If neither --data nor --sample is provided, the first sample is used automatically.
Strict mode is on by default. It refuses to emit output when the template
references data missing from the supplied data object, uses an unknown pipe, or
passes a wrong-type pipe argument. Opt out per-invocation with --no-strict,
or set strict: false in norq.config.yaml for a project-wide opt-out. See
Strict mode below.
norq test
Run assertion tests defined in tests.yaml.
norq test # all notifications
norq test transactional/order-shipped # one notification
norq test --json # JSON outputSee the testing page for test syntax and assertion operators.
norq preview
Preview compiled output for a specific channel.
norq preview transactional/welcome --channel email
norq preview transactional/welcome --channel email --sample "Pro user"
norq preview transactional/welcome --channel email --terminal| Flag | Description |
|---|---|
--channel <c> |
Channel to preview (required) |
--sample <name> |
Named sample to use |
--terminal |
Print to terminal instead of opening a browser |
norq render
Render email previews and device screenshots via a render-capable provider. Subcommands:
norq render preview
Render an email to a single image (desktop or mobile viewport).
norq render preview transactional/welcome
norq render preview transactional/welcome --device mobile
norq render preview transactional/welcome --dark-mode --out preview.png
norq render preview transactional/welcome --sample "Pro user" --device mobile| Flag | Description |
|---|---|
--device <viewport> |
Viewport size: desktop or mobile (default: desktop) |
--dark-mode |
Render in dark mode |
--sample <name> |
Named sample from data.samples.yaml |
--data <file> |
Path to a JSON data file |
-o, --out <path> |
Output file path. Without --out, raw image bytes are written to stdout |
norq render screenshot
Render an email as it appears in a specific email client.
norq render screenshot transactional/welcome --client gmail_web
norq render screenshot transactional/welcome --client outlook_2021 --out screenshot.png
norq render screenshot transactional/welcome --client iphone_15 --sample "New user"| Flag | Description |
|---|---|
--client <name> |
Email client to render in (required). Examples: outlook_2021, gmail_web, iphone_15, apple_mail |
--sample <name> |
Named sample from data.samples.yaml |
--data <file> |
Path to a JSON data file |
-o, --out <path> |
Output file path. Without --out, raw image bytes are written to stdout |
Both render subcommands require a provider with render capabilities. Currently only the suprsend built-in provider supports rendering (preview + screenshot). If no render-capable provider is configured, you’ll see:
Error: Provider 'console' does not support op 'preview'
Configured providers:
console → no render
Hint: rendering requires a provider with render capabilities.
Built-in: set NORQ_SUPRSEND_WORKSPACE_KEY to enable SuprSend (supports preview + screenshot)
norq send
Compile and send a notification via configured providers.
norq send transactional/welcome --to '{"email":"user@example.com"}' --sample "New user"
norq send transactional/welcome --to '{"email":"user@example.com"}' --data ./data.json
norq send transactional/welcome --to '{"email":"user@example.com","phone":"+1234567890"}' --channels email,sms
norq send transactional/welcome --to '{"email":"user@example.com","cc":["audit@example.com"]}' --channels email
norq send transactional/welcome --to '{"email":"user@example.com"}' --idempotency-key "order-shipped-ORD-123"
norq send transactional/welcome --to '{"email":"user@example.com"}' --dry-run| Flag | Description |
|---|---|
--to <json> |
Recipient as JSON (required) — supports email, phone, plus per-channel sub-shapes; for email, optional cc and bcc arrays |
--data <file> |
Path to a JSON data file |
--sample <name> |
Named sample from data.samples.yaml |
--channels <a,b> |
Send only to these channels |
--idempotency-key <key> |
Forwarded to providers that honor it (Resend, SuprSend) as the Idempotency-Key: header. When omitted, a UUID v4 is generated for this send and applied to every channel. |
--dry-run |
Compile only, do not send |
--json |
Output as JSON |
--strict |
Force strict-mode runtime validation on (overrides config) |
--no-strict |
Force strict-mode off (overrides config) |
Strict mode is on by default. A Handlebars-inspired pre-send validator refuses to deliver when the template references data missing from the runtime data object, uses an unknown pipe, or passes a wrong-type pipe argument. This catches silent data-corruption bugs (typo’d variable names, etc.) before they reach the recipient. See Strict mode.
CLI push limitations. The CLI is for development. It can send push only via providers that use static bearer tokens (e.g.
expowith an access token). It cannot:
- Sign Apple JWT for APNs (also:
ureqis HTTP/1.1 only — APNs requires HTTP/2).- Exchange Google service-account credentials for FCM access tokens.
- Encrypt Web Push payloads.
For those providers, use an SDK. The CLI emits
provider/cli-http2-required,provider/cli-oauth2-not-supported, orprovider/cli-vapid-not-supportedif you try.
Strict mode
Inspired by Handlebars’ strict mode. A pre-compile runtime validator refuses to send a notification when the template references data missing from the runtime data object, uses unknown pipes, or passes wrong-type pipe arguments.
On by default. Set strict: false in norq.config.yaml for a project-wide
opt-out, or pass --no-strict per-invocation.
Legitimate exceptions that strict mode allows:
:::if path— paths used inside the consequent are implicitly guarded.:::each items as item— loop binding is in scope inside the body.{{ path | default "..." }}— thedefaultpipe makes missing paths OK.- Explicit
null—{"field": null}in data is allowed (distinct from “missing field”).
Violations surface as:
undefined-runtime-path— expression path not in runtime dataunknown-pipe— pipe name not inKNOWN_PIPESinvalid-pipe-arg— pipe called with wrong type or arity
All three are reported with line numbers (remapped from expanded-doc coords through any partial expansions back to source coords).
norq dev
Start a preview dev server with hot reload.
norq dev # all notifications, port 3456
norq dev transactional/welcome # specific notification
norq dev --port 8080 # custom portThe dev server provides a web UI where you can:
- Switch between notifications and channels
- Select different sample data
- See live updates as you edit templates
norq doctor
Diagnose project environment, configuration, and health. Output is grouped into sections with three status levels: [✓] pass, [!] warning, [✗] error.
norq doctor # human-readable grouped output
norq doctor --json # machine-readable JSON (for CI/tooling)| Flag | Description |
|---|---|
--json |
Output as JSON |
Sections and diagnostics:
| Section | Check | Status | Trigger | Fix |
|---|---|---|---|---|
| Discovery | Config found | error | No norq.config.yaml in cwd or subdirectories |
norq init |
| Discovery | Multiple projects | warn | More than one norq.config.yaml discovered |
--config <path> |
| Environment | Version | pass | Always shown | — |
| Environment | Env vars | error | ${VAR} in config but VAR not set in environment |
export VAR=value |
| Project | Config valid | error/warn | norq.config.yaml has schema errors or warnings |
Fix config syntax |
| Project | Notifications | pass | Count of notifications by type folder | — |
| Project | Routing gaps | warn | Channel has templates but no entry in routing |
Add routing.<channel>: <provider> |
| Project | Providers | warn | No providers configured | Add providers section |
| Health | Lint | error/warn | norq lint finds errors or warnings |
norq lint |
| Health | Schema coverage | warn | Notification missing data.schema.yaml |
Create schema file |
| Health | Test coverage | warn | Notification missing tests.yaml |
Create tests file |
| Health | Partials | pass | Lists _shared/*.md files |
— |
| Brand | brand.yaml | pass/warn | brand.yaml reachable, or compiled-in defaults in use |
norq brand init |
| Brand | Resolver | pass/warn | Reports broken refs, unknown pipes, basedOn cycles |
norq brand show |
| Brand | AGENTS_BRAND.md | pass/warn | Generated file matches current brand.yaml (only checked when the file already exists) |
norq brand sync-agents |
| Integrations | Codegen | warn | Codegen output directory missing | norq codegen |
Example output:
Discovery
[✓] Found norq project at ./
Environment
[✓] norq v0.1.0-alpha.1
Project
[✓] norq.config.yaml found and valid
[✓] 3 notification(s) (1 promotional, 2 transactional)
[✓] Routing: email → console, sms → console
[✓] Providers: console
Health
[✓] Lint: 0 error(s), 0 warning(s)
[✓] Schema coverage: 3/3 notifications
[!] Test coverage: 1/3 notifications
Create tests.yaml in missing notification dirs
[✓] Partials: email-footer, email-header
Project health: 1 warning(s)
Exit codes:
| Code | Meaning |
|---|---|
| 0 | All checks pass |
| 1 | One or more warnings |
| 2 | One or more errors |
norq codegen
Generate type-safe SDK bindings from data schemas.
norq codegen # use config file targets
norq codegen --lang typescript --out src/generated/norq.ts
norq codegen --check # CI: verify files are up to date| Flag | Description |
|---|---|
--lang <language> |
Target language: typescript, python, go, java, ruby |
--out <path> |
Output file path |
--check |
Verify generated files match (for CI) |
Without --lang/--out, reads targets from the codegen section of norq.config.yaml.
norq lsp
Start the Language Server Protocol server on stdin/stdout.
norq lspUsed by editor extensions (VS Code, Neovim, etc.). Not typically run manually. See the editor setup page.
norq mcp-server
Start the Model Context Protocol server on stdin/stdout.
norq mcp-serverUsed by AI agents and coding assistants. See the AI integration page.
norq add
Add notifications and partials from registries or Git repositories.
norq add welcome
norq add @acme/invoice --overwrite
norq add github:user/repo/notifications/transactional/welcome
norq add welcome invoice --dry-runAccepts bare names (welcome), namespaced names (@acme/welcome), full URLs, or Git shorthand (github:owner/repo/path[@ref]). Multiple items can be added in one call.
Dependency resolution: When a registry item declares registryDependencies, norq add recursively fetches all transitive dependencies, topologically sorts them (dependencies before dependents), and writes files in dependency order. Partials are installed to the notification-local _shared/ directory. Circular dependencies are detected and reported as errors. Existing partials with the same name are not overwritten unless --overwrite is passed.
| Flag | Description |
|---|---|
--overwrite |
Overwrite existing files without prompting |
--skip |
Skip items that already exist locally |
--dry-run |
Show what would be added without writing files |
--path <path> |
Install to a specific directory instead of auto-detecting |
--json |
Output as JSON |
norq registry
Manage and query notification registries. Subcommands:
norq registry build
Build registry.json into static JSON files for hosting.
norq registry build
norq registry build --out dist/registry --json| Flag | Description |
|---|---|
--out <dir> |
Output directory (default: ./public/r) |
--json |
Output as JSON |
norq registry validate
Validate a registry manifest and all referenced items.
norq registry validate
norq registry validate --json| Flag | Description |
|---|---|
--json |
Output as JSON |
norq registry search
Search across configured registries.
norq registry search welcome
norq registry search invoice --type transactional
norq registry search --category account| Flag | Description |
|---|---|
--type <type> |
Filter by notification type (system, transactional, promotional) |
--category <cat> |
Filter by category |
--json |
Output as JSON |
norq registry list
List all items in configured registries.
norq registry list
norq registry list --type promotional --json| Flag | Description |
|---|---|
--type <type> |
Filter by notification type |
--json |
Output as JSON |
norq registry view
View details of a specific registry item.
norq registry view welcome
norq registry view @acme/invoice --files --json| Flag | Description |
|---|---|
--files |
Include file contents in output |
--json |
Output as JSON |
norq provider
Manage providers and channel routing. Subcommands:
norq provider list
List all registered providers and their configuration status.
norq provider listOutput columns:
| Column | Description |
|---|---|
| TYPE | Provider type (built-in or custom) |
| NAME | Provider identifier |
| CHANNELS | Channels the provider supports (all or specific channels) |
| STATUS | See below |
STATUS values:
| Value | Meaning |
|---|---|
configured (config) |
Built-in provider has a providers.<name>.config block in norq.config.yaml and credentials resolved cleanly |
configured (env) |
Built-in provider isn’t in norq.config.yaml but NORQ_<NAME>_* env vars are set |
configured |
Custom provider with credentials resolved (or always-available built-ins like console) |
credentials missing |
Config block present but a required field (e.g. ${RESEND_API_KEY}) is empty |
invalid config |
Config block is malformed (e.g. config: is a string instead of a mapping) |
not configured |
Built-in provider has neither a config block nor env vars set |
Built-in providers
| Provider | Channels | Config keys (providers.<name>.config) |
Env-var fallback |
|---|---|---|---|
| resend | api_key, from, from_name (optional), reply_to (optional, scalar or list) |
NORQ_RESEND_API_KEY, NORQ_RESEND_FROM_ADDRESS |
|
| sendgrid | api_key, from, from_name (optional), reply_to (optional, scalar or list) |
NORQ_SENDGRID_API_KEY, NORQ_SENDGRID_FROM_ADDRESS |
|
| suprsend | all | workspace_key, workspace_secret, base_url (optional) |
NORQ_SUPRSEND_WORKSPACE_KEY, NORQ_SUPRSEND_WORKSPACE_SECRET, NORQ_SUPRSEND_BASE_URL |
| console | all | (none — always available) | (none) |
| fcm | push (ios, android, web) | projectId, serviceAccount (file path, ${ENV}, or inline JSON) |
NORQ_FCM_PROJECT_ID, NORQ_FCM_SERVICE_ACCOUNT |
| apns | push (ios) | keyId, teamId, bundleId, keyPath or keyPem, production |
NORQ_APNS_KEY_ID, NORQ_APNS_TEAM_ID, NORQ_APNS_BUNDLE_ID, NORQ_APNS_KEY_PATH (or NORQ_APNS_KEY_PEM), NORQ_APNS_PRODUCTION |
| expo | push (ios, android) | accessToken (optional) |
NORQ_EXPO_ACCESS_TOKEN |
| webpush | push (web) | vapidPublicKey, vapidPrivateKey, subject |
NORQ_WEBPUSH_VAPID_PUBLIC_KEY, NORQ_WEBPUSH_VAPID_PRIVATE_KEY, NORQ_WEBPUSH_SUBJECT |
Config-driven setup is preferred. Example:
# norq.config.yaml
providers:
resend:
config:
api_key: ${RESEND_API_KEY}
from: norq@alerts.example.com
from_name: "Example"
reply_to: support@example.com
routing:
email: resendEnv-var fallback exists for backward compatibility — when providers.resend is absent from the config, the CLI falls back to NORQ_RESEND_* env vars and reports configured (env).
norq provider routing
Show the channel-to-provider routing table. Displays which provider handles each channel and why.
norq provider routingOutput columns:
| Column | Description |
|---|---|
| CHANNEL | Channel name (email, sms, slack, push, whatsapp, msteams) |
| PROVIDER | Provider that will handle sends for this channel |
| REASON | How the provider was selected (first match or no provider available) |
Resolution order: explicit routing config in norq.config.yaml takes priority, then first-match by registration order.
norq fmt
Format all template files in the project with canonical indentation and spacing.
norq fmt # format all .md files in notifications/
norq fmt --check # check formatting without writing (exit 1 if changes needed)| Flag | Description |
|---|---|
--check |
Check only – report files that need formatting, exit 1 if any do |
The formatter enforces:
- 2-space indent inside directives, +2 for each nesting level
- One blank line between block-level elements
- No trailing blank lines before
:::closers - No multiple consecutive blank lines
- Frontmatter and code blocks preserved as-is
Use --check in CI to enforce consistent formatting:
# GitHub Actions
- run: norq fmt --checkThe LSP also supports document formatting (Format Document) using the same rules.
norq brand
Manage the project’s brand.yaml – the tenant’s visual identity (colors, typography, spacing, radii, styles, fonts, voice). Five subcommands cover the authoring lifecycle:
norq brand init # scaffold brand.yaml from defaults
norq brand show [--json] # print resolved brand (aliases + pipes flattened)
norq brand import <file> [--out X] # convert DESIGN.md / DTCG / Figma / Style Dictionary
norq brand export --format <fmt> # emit dtcg | tailwind | css | json
norq brand diff <left> <right> # semantic diff between two brand filesnorq brand init
norq brand init # writes <config-dir>/brand.yaml
norq brand init --path ./design/brand.yaml
norq brand init --force # overwrite an existing file| Flag | Description |
|---|---|
--path <P> |
Output path. Defaults to <config-dir>/brand.yaml. |
--force |
Overwrite an existing brand.yaml. Refuses without it. |
The scaffolded file is the compiled-in default brand (mirrors the previous flat theme: palette). Edit it to customise tokens.
norq brand show
norq brand show # human-readable summary
norq brand show --json # ResolvedBrand JSON for scriptingPrints the brand the project will compile against – aliases like {colors.brand} and color pipes like {colors.brand | darken 10%} are flattened to literal values. Diagnostics from the resolver (broken refs, unknown pipes, basedOn cycles) are listed at the end.
norq brand import
norq brand import DESIGN.md # writes <config-dir>/brand.yaml
norq brand import tokens.json --out custom.yaml
norq brand import figma.json --stdout # print YAML, don't write| Flag | Description |
|---|---|
--out <P> |
Where to write brand.yaml. Defaults to <config-dir>/brand.yaml. |
--force |
Overwrite an existing brand.yaml. |
--stdout |
Emit the resulting YAML to stdout instead of a file. |
Format detection is content-driven first, extension-only as a tiebreaker. Recognised inputs:
| Format | Detected by |
|---|---|
| DESIGN.md | .md extension |
| DTCG JSON | $schema field or any descendant $value field |
| Figma Tokens Studio | nested groups with {value, type} leaves |
| Style Dictionary | top-level color / size / font / radius groups |
| Norq brand.yaml | a YAML doc with meta: / tokens: / voice: keys |
The DESIGN.md importer is regex-driven over Markdown. It pulls colors from bullet lines that contain a hex literal, dimensions from numbered bullets under ## Spacing / ## Border radius headers, and voice principles from bullets under ## Voice / ## Tone / ## Principles. Hit rate varies by source – the importer prints per-section counts so you can see what was recognised; missing groups stay empty for manual fill-in.
norq brand export
norq brand export --format dtcg # to stdout
norq brand export --format tailwind --out tailwind.config.js
norq brand export --format css --out brand.css
norq brand export --format json| Format | Output |
|---|---|
dtcg |
DTCG 2025.10 JSON. Norq-specific bits (styles, voice, fonts, modes, defaults, codeTheme) emitted under $extensions.norq for round-trip parity. |
tailwind |
tailwind.config.js stub with theme.extend.colors, fontSize, fontFamily, spacing, borderRadius populated. |
css |
:root { --colors-brand: #...; } custom-property block. Dark-mode overrides emit a @media (prefers-color-scheme: dark) block. |
json |
Norq’s own brand.yaml shape, JSON-encoded. |
Pipe expressions ({colors.brand | darken 10%}) are pre-resolved before export. DTCG has no concept of computed tokens, so the exported file is a snapshot – a subsequent brand import reads the literal hex, not the pipe expression.
norq brand diff
norq brand diff old.yaml new.yamlSemantic diff: walks colors, spacing, radii, typography structurally and reports add (+), remove (-), and change (~) per token. Token reorderings inside the YAML produce no noise – only token-name and value changes show up. Defaults are NOT merged; the diff shows only what is literally written in each file.
norq brand sync-agents
Generates AGENTS_BRAND.md – a single Markdown file every AI agent reads – from the resolved brand. The file lives at the project root (next to norq.config.yaml) and is regenerated on demand. Source-of-truth is brand.yaml; AGENTS_BRAND.md is a human- and agent-readable derivative.
norq brand sync-agents # write <config-dir>/AGENTS_BRAND.md
norq brand sync-agents --check # CI mode: exit 1 if regen would change the file
norq brand sync-agents --stdout # print, don't write
norq brand sync-agents --scope rules # subset
norq brand sync-agents --out X.md # custom path| Flag | Description |
|---|---|
--check |
Exit non-zero with a remediation hint if the file on disk differs from what would be regenerated. Use in pre-commit hooks or CI. |
--stdout |
Emit the rendered Markdown to stdout instead of writing to disk. Pipe straight into a system prompt with --scope rules. |
--out <P> |
Override the output path. Defaults to <config-dir>/AGENTS_BRAND.md. |
--scope <s> |
One of rules (principles only – smallest payload), brand (voice + principles), tokens (token reference + heading mapping + quickview), all (everything; default). |
The output is deterministic – same brand + same scope produces the same bytes – so --check does a byte-for-byte comparison without depending on diff tooling.
norq doctor runs the same check and warns when AGENTS_BRAND.md is stale relative to brand.yaml. norq init appends a ## Visual identity pointer to existing AGENTS.md / CLAUDE.md files at the project root (it does not create them). The pointer is stable text and never churns – the regeneratable file carries the churn.
norq completions
Generate shell completions for bash, zsh, fish, elvish, or PowerShell.
norq completions bash >> ~/.bashrc
norq completions zsh >> ~/.zshrc
norq completions fish > ~/.config/fish/completions/norq.fish
norq completions elvish >> ~/.elvish/rc.elv
norq completions powershell >> $PROFILE| Argument | Description |
|---|---|
<shell> |
Shell to generate completions for: bash, zsh, fish, elvish, powershell |
norq version
Show version information.
norq version
norq version --jsonGlobal flags
These flags work on all commands:
| Flag | Description |
|---|---|
--config <path> |
Explicit path to norq.config.yaml. Skips automatic discovery. |
Configuration
All project configuration lives in norq.config.yaml:
notifications: ./notifications
providers:
resend:
config:
api_key: ${RESEND_API_KEY}
from: "notifications@myapp.com"
routing:
email: resend
sms: suprsend
codegen:
- lang: typescript
out: src/generated/norq.tsEnvironment variables use ${VAR_NAME} syntax and are substituted at load time.
Providers
Providers handle sending notifications to channels and rendering previews. Norq ships with seven built-in providers — three general-purpose plus four push-specialist:
| Provider | Channels | Render | Required env vars |
|---|---|---|---|
| suprsend | all | preview, screenshot | NORQ_SUPRSEND_WORKSPACE_KEY, NORQ_SUPRSEND_WORKSPACE_SECRET |
| resend | — | NORQ_RESEND_API_KEY, NORQ_RESEND_FROM_ADDRESS |
|
| sendgrid | — | NORQ_SENDGRID_API_KEY, NORQ_SENDGRID_FROM_ADDRESS |
|
| console | all | — | none (always available, prints to stderr) |
| fcm | push (ios, android, web) | — | NORQ_FCM_PROJECT_ID, NORQ_FCM_SERVICE_ACCOUNT |
| apns | push (ios) | — | NORQ_APNS_KEY_ID, NORQ_APNS_TEAM_ID, NORQ_APNS_BUNDLE_ID, NORQ_APNS_KEY_PATH (or NORQ_APNS_KEY_PEM) |
| expo | push (ios, android) | — | NORQ_EXPO_ACCESS_TOKEN (optional) |
| webpush | push (web) | — | NORQ_WEBPUSH_VAPID_PUBLIC_KEY, NORQ_WEBPUSH_VAPID_PRIVATE_KEY, NORQ_WEBPUSH_SUBJECT |
Built-in providers are auto-registered when their env vars are set. Console is always registered as a fallback.
Custom providers
Define custom providers under providers: in norq.config.yaml. A custom provider specifies which channels it supports, authentication, and HTTP request definitions for each operation:
providers:
mailgun:
channels: [email]
config:
domain: { required: true }
api_key: { required: true, secret: true }
auth:
type: basic
username: api
password: "{{secrets.api_key}}"
send:
method: POST
url: "https://api.mailgun.net/v3/{{config.domain}}/messages"
body:
from: "notifications@{{config.domain}}"
to: "{{input.recipient}}"
subject: "{{input.subject}}"
html: "{{input.html}}"
response:
success_status: [200]
message_id_path: "$.id"Multi-channel custom providers use a per-channel send map:
providers:
internal-gateway:
channels: [email, sms]
config:
base_url: { required: true }
api_key: { required: true, secret: true }
auth:
type: bearer
token: "{{secrets.api_key}}"
send:
email:
method: POST
url: "{{config.base_url}}/v1/email"
body:
to: "{{input.recipient}}"
subject: "{{input.subject}}"
html: "{{input.html}}"
sms:
method: POST
url: "{{config.base_url}}/v1/sms"
body:
to: "{{input.recipient}}"
text: "{{input.body}}"Auth types: bearer (token), basic (username/password), api_key (header or query param with in, key, value).
Config values marked secret: true are resolved from NORQ_<PROVIDER>_<KEY> env vars (uppercased). Non-secret config uses the same convention.
Routing
The routing: section maps channels to providers explicitly. Without explicit routing, Norq uses first-match by registration order (built-ins first, then custom providers in config order).
routing:
email: mailgun
sms: internal-gateway
slack: suprsend
push: fcm
push.ios: apns # platform-scoped routing wins over channel-wide pushUse norq provider routing to inspect the resolved routing table and see which provider handles each channel.
Provider errors
| Error | Cause | Fix |
|---|---|---|
Unknown provider: {name} |
The provider name in routing: doesn’t match any built-in or custom provider |
Check spelling in routing: config. Run norq provider list to see available providers |
Provider '{provider}' error: {message} |
The provider returned an error during request preparation or sending | Check provider credentials and configuration. The {message} contains details from the provider |
No provider configured for channel '{channel}' |
No provider is available to handle this channel | Add a provider that supports this channel, or add a routing: entry pointing to one |
Provider '{provider}' does not support op '{op}' |
The provider doesn’t implement the requested operation (e.g. render preview) | Use a different provider. For render ops, SuprSend is currently the only built-in with render support |
Provider '{provider}' does not support channel '{channel}' |
The provider was routed a channel it can’t handle | Fix routing: to map this channel to a provider that supports it |
Provider '{provider}' missing required config: {message} |
A required config field is not set | Set the required env var. Run norq provider list to check configuration status |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (lint errors, compilation failure, etc.). norq doctor: warnings present |
| 2 | norq doctor only: errors present |