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 whatsapp.json Interactive messages, template messages, flows
Teams msteams.json Adaptive Card JSON with inputs, actions
Email 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.

WhatsApp

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.