Norq

Data Contracts

Every notification can define a data contract using JSON Schema. The schema powers LSP completions, nullable-access warnings, codegen, and sample data validation.

Here is a minimal example:

# notifications/welcome/data.schema.yaml
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [user]
properties:
  user:
    type: object
    required: [first_name]
    properties:
      first_name: { type: string }

File

data.schema.yaml – placed inside the notification directory. One schema per notification, shared across all channels.

Format

Standard JSON Schema (draft 2020-12) written in YAML for readability:

$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [user, action_url]
properties:
  user:
    type: object
    required: [first_name, email]
    properties:
      first_name: { type: string }
      last_name: { type: string }
      email: { type: string, format: email }
      tier: { type: string, enum: [free, pro, enterprise] }
  action_url: { type: string, format: uri }
  unsubscribe_url: { type: string, format: uri }

Required vs. optional fields

Fields listed in required are guaranteed non-null. Fields not in required are nullable – they can be absent at runtime. The linter warns if you use them without a guard (:::if or | default).

The linter uses this distinction for null safety:

# 'last_name' is not in required -- using it without a guard is a warning
warning: 'last_name' may be null — wrap in :::if guard or use | default "..."
  --> email.md:8:5
   |
 8 |     Hello {{user.last_name}}
   |           ^^^^^^^^^^^^^^^^^^
   = rule: template/nullable-access

Fix with an :::if guard:

::: if user.last_name
Hello {{user.last_name}}
:::else
Hello {{user.first_name}}
:::

Or with a | default pipe:

Hello {{user.last_name | default user.first_name}}

Nullable fields

Express nullable fields using oneOf:

tracking_id:
  oneOf: [{ type: string }, { type: "null" }]

This tells the linter that tracking_id can be null, enabling nullable-access warnings.

Array types

items:
  type: array
  items:
    type: object
    required: [name, qty, price]
    properties:
      name: { type: string }
      qty: { type: integer }
      price: { type: number }

Arrays are used with :::each loops:

::: each order.items as item
- {{item.name}} x {{item.qty}}
:::

Description fields

Add description to schema fields – the LSP surfaces these as hover documentation in the editor:

properties:
  tier:
    type: string
    enum: [free, pro, enterprise]
    description: "User's subscription tier, determines feature access"

What the schema enables

Feature Without schema With schema
LSP completions No variable suggestions Full autocompletion with types
Null safety No warnings Warns on nullable access outside :::if
Codegen Generates Record<string, any> Generates typed interfaces/structs
Sample validation No validation Samples validated against schema
Preview Works (no validation) Works + validation errors shown

Why JSON Schema?

  • Developers already know it (OpenAPI, VS Code settings, Adaptive Cards all use it)
  • Every language has validators: ajv (JS), jsonschema (Python), gojsonschema (Go)
  • Can be generated from Prisma schemas, TypeScript types, OpenAPI specs
  • VS Code validates it natively with the JSON Schema meta-schema

Sample data

Sample data lives in a separate file (data.samples.yaml) and must validate against the schema:

samples:
  - name: "New user"
    data:
      user:
        first_name: "Gaurav"
        email: "gaurav@example.com"
        tier: "free"
      action_url: "https://app.example.com/start"
      unsubscribe_url: "https://app.example.com/unsub"
 
  - name: "Pro user"
    data:
      user:
        first_name: "Priya"
        last_name: "Singh"
        email: "priya@example.com"
        tier: "pro"
      action_url: "https://app.example.com/start"
      unsubscribe_url: "https://app.example.com/unsub"

Each sample has a name (used in CLI --sample flag, preview dropdown, and test references) and a data object.

See Project Configuration for provider setup, routing, codegen, and the brand: config key, and Brand for the visual identity token system.