Native JSON Mode
For channels where you need full control over the payload structure, Norq supports native JSON mode. Instead of writing Markdown, you write the channel's native format directly with {{expression}} interpolation.
When to use native JSON
Use native JSON mode when:
- You need channel-specific features that directives don't cover (e.g., Slack date pickers, Teams input forms)
- You have existing Block Kit or Adaptive Card JSON you want to reuse
- You need precise control over the payload structure
Use Markdown mode when:
- You want cross-channel consistency from a single source
- You want LSP intelligence (completions, hover, diagnostics)
- You're writing standard notification content (text, buttons, fields)
Supported channels
| Channel | JSON mode | Why |
|---|---|---|
| Slack | slack.json |
Block Kit JSON with datepickers, selects, etc. |
| Push | push.json |
Custom push payload structure |
whatsapp.json |
Interactive messages, template messages, flows | |
| Teams | msteams.json |
Adaptive Card JSON with inputs, actions |
| Not supported | Email always uses .md (HTML rendering is the reason Norq exists) |
|
| SMS | Not supported | SMS is plain text; use .md |
File naming
Place the JSON file alongside your Markdown templates:
notifications/order-shipped/
email.md # always Markdown
sms.md # always Markdown
slack.json # native JSON
data.schema.yaml
data.samples.yaml
You cannot have both slack.md and slack.json for the same channel. The linter enforces this.
File format
A native JSON file is the channel's payload format with a special $norq metadata key:
{
"$norq": {
"enabled": true
},
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Order #{{order.id}} shipped"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Hey *{{user.first_name}}*, your order has been shipped."
}
}
],
"text": "Order {{order.id}} shipped"
}The $norq key is metadata (currently just enabled) and is stripped from the compiled output.
Expressions in JSON
Variable interpolation works the same as in Markdown mode:
"text": "Hello {{user.first_name}}"Pipes use Handlebars helper syntax (helper name before the variable, not the | pipe operator):
"text": "Hello {{capitalize user.first_name}}"
"text": "You have {{count items}} items"
"text": "Tags: {{join tags \", \"}}"All 40 pipes from the Markdown pipe system are available as Handlebars helpers with the same names. Examples by category:
String: capitalize, uppercase, lowercase, titlecase, truncate, trim, trim_start, trim_end, replace, append, prepend, default, pluralize, md5, split
Array: count, join, unique, at, first, last, reverse, sort, slice
Math: add, subtract, multiply, divide, round, mod, abs, ceil, floor, percent
Formatting: number, currency
Serialization: json, from_json
Date: date, timezone
"text": "Total: {{currency order.total \"USD\"}}"
"text": "{{titlecase user.name}} has {{count items}} {{pluralize (count items) \"item\" \"items\"}}"
"text": "Ordered on {{date order.date \"%B %d, %Y\"}}"See Expressions and Pipes for the full pipe reference with descriptions and examples.
Control flow in JSON
Since :::if and :::each are Markdown constructs, native JSON uses Handlebars control flow:
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{#if order.shipped}}Your order shipped!{{else}}Processing...{{/if}}"
}
},
{{#each order.items as |item|}}
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*{{item.name}}* x {{item.qty}}"
}
}{{#unless @last}},{{/unless}}
{{/each}}
]
}Payload wrapping per channel
Each channel expects a specific top-level structure:
Slack
Must have a blocks key. Compiled into { blocks, text }.
Push
Keys: title, body, image, action_url, platforms.
Auto-detected by content:
"template"key present -> template message"type": "button"-> interactive message"link"key -> image message- Otherwise -> text message
Teams
The entire JSON becomes the Adaptive Card body.
Data contracts and samples
The data.schema.yaml and data.samples.yaml files still apply to native JSON templates. Variable completions and null safety checks work in native files too. The linter validates JSON structure against the channel's native schema.
Linter behavior
The linter emits an informational diagnostic for native JSON files:
info: slack.json uses native mode — consider slack.md for better cross-channel consistency
This is not a warning -- it is purely informational.