Lint Rules

Norq's linter catches issues before they reach your users — from spam triggers and accessibility violations in email to segment costs in SMS and payload limits in push notifications. Run norq lint to check all rules, or get real-time feedback in VS Code.

How to use

norq lint                                # check all notifications
norq lint transactional/order-shipped    # check one notification
norq lint --json                         # JSON output for CI/tooling

In VS Code with the Norq extension installed, diagnostics appear inline as you type — no manual run needed. See LSP & VS Code for setup.

Severity levels:

Severity Meaning
Error Blocks delivery or compilation. Must fix.
Warning Will likely cause problems in production. Should fix.
Info Optimization opportunity. Worth knowing.

Email

Email has the most rules because deliverability, accessibility, and spam filtering each impose their own constraints.

Rule ID Severity Description
email/missing-subject Warning No subject in frontmatter
email/missing-alt Warning Image missing alt text
email/subject-length Info Subject exceeds 60 characters
email/missing-preheader Warning No preheader in frontmatter
email/preheader-length Info Preheader outside 40–130 character range
email/size-limit Warning Compiled HTML exceeds 102 KB
email/heading-hierarchy Warning Heading levels skip (e.g., h1 → h3)
email/spam-words Info Spam trigger words in subject or preheader
email/link-text-quality Warning Non-descriptive link text
email/low-contrast Warning Text/background fails WCAG 2.1 AA contrast
email/subject-all-caps Info Subject is entirely uppercase
email/subject-excessive-punctuation Info Subject has 2+ consecutive ! or ?
email/url-shortener Warning Links use URL shortening services
email/text-to-image-ratio Warning Email is image-heavy with little text
email/all-caps-body Info Body text block is entirely uppercase
email/missing-unsubscribe Info No unsubscribe link found
email/font-size-minimum Info Font size below 14px accessibility minimum

email/missing-subject

Severity: Warning

Why it matters: Most email providers require a subject line. Without one, sends may be rejected outright or delivered with a blank subject, which tanks open rates.

How to fix: Add subject: to the frontmatter at the top of email.md:

---
subject: "Your order has shipped"
---

email/missing-alt

Severity: Warning

Why it matters: Images without alt text fail accessibility standards and increase spam score. Screen readers skip the image entirely, and spam filters penalize image-only content.

How to fix: Add descriptive alt text to every image:

![Order confirmation graphic](https://cdn.example.com/order.png)

Use a meaningful description, not a filename.


email/subject-length

Severity: Info

Why it matters: Mobile clients — Gmail on Android, Apple Mail on iPhone — typically cut subjects off around 40–60 characters. Recipients see a truncated subject and may not open.

How to fix: Keep subjects under 60 characters. Front-load the most important words.


email/missing-preheader

Severity: Warning

Why it matters: When preheader is absent, email clients fill that inbox preview line with the first text from the email body — often a navigation link, disclaimer, or "View in browser" prompt. This wastes premium inbox real estate.

How to fix: Add preheader: to frontmatter:

---
subject: "Your order has shipped"
preheader: "Track your package and see delivery details"
---

email/preheader-length

Severity: Info

Why it matters: A preheader under 40 characters leaves inbox preview space blank. One over 130 characters gets truncated mid-sentence, sometimes at an awkward word.

How to fix: Aim for 40–130 characters. Test across Gmail, Apple Mail, and Outlook.


email/size-limit

Severity: Warning

Why it matters: Gmail clips emails larger than 102 KB, showing a "Message clipped" link instead of the rest of the email. Hidden content often includes CTAs and unsubscribe links — both critical.

How to fix: Reduce inline content. Break long emails into shorter ones, move verbose copy to landing pages, and avoid inlining large base64 images.


email/heading-hierarchy

Severity: Warning

Why it matters: Screen readers use heading levels to navigate documents. Jumping from h1 to h3 (skipping h2) breaks this structure, causing confusion for users with assistive technology.

How to fix: Use headings in sequential order. Start with # (h1), then ## (h2), then ### (h3). Do not skip levels.


email/spam-words

Severity: Info

Why it matters: Spam filters score messages based on known trigger phrases in subject and preheader fields. Common offenders: "free", "act now", "click here", "guaranteed", "limited time offer".

How to fix: Rephrase to remove trigger words. Focus on specific value rather than generic urgency.


Severity: Warning

Why it matters: Link text like "click here", "here", or "read more" is meaningless out of context. Screen readers enumerate links; non-descriptive text leaves users unable to navigate. Spam filters also score these negatively.

How to fix: Use descriptive link text that explains the destination:

[Track your shipment](https://example.com/track/12345)   ✓
[Click here](https://example.com/track/12345)             ✗

email/low-contrast

Severity: Warning

Why it matters: WCAG 2.1 AA requires a contrast ratio of 4.5:1 for normal text and 3:1 for large text (18px+ or 14px+ bold). Low contrast text is unreadable for users with low vision.

How to fix: Use a contrast checker to verify your text/background color pairs. Adjust your theme's brandColor, buttonColor, or text colors in norq.config.yaml until they pass.


email/subject-all-caps

Severity: Info

Why it matters: ALL CAPS subjects are a classic spam signal. Many filters penalize them, and they read as aggressive to recipients.

How to fix: Use standard title case or sentence case for subjects.


email/subject-excessive-punctuation

Severity: Info

Why it matters: Subjects with !! or ?? read as shouting and are flagged by spam filters. One exclamation point is already aggressive; two or more is a reliable spam indicator.

How to fix: Use a single punctuation mark, or none at all.


email/url-shortener

Severity: Warning

Why it matters: URL shorteners (bit.ly, tinyurl.com, t.co, etc.) hide the destination domain from spam filters and recipients alike. Many corporate email gateways block messages containing shortened URLs outright.

How to fix: Use full, direct URLs. If link length is a concern in a plain-text context, use link text to mask a long URL rather than shortening it.


email/text-to-image-ratio

Severity: Warning

Why it matters: Emails composed almost entirely of images trigger spam filters because spammers use image-only emails to hide content from text scanners. Low text-to-image ratio correlates strongly with promotion/spam classification.

How to fix: Add more text content alongside images. Describe what the image shows, add a caption, or include body copy.


email/all-caps-body

Severity: Info

Why it matters: A body text block written entirely in uppercase reads as shouting and reduces readability. It is also a secondary spam signal when combined with other indicators.

How to fix: Use normal casing. Use bold (**text**) for emphasis instead.


email/missing-unsubscribe

Severity: Info

Why it matters: CAN-SPAM (US) requires an unsubscribe mechanism in all commercial email. Gmail and Yahoo both announced deliverability requirements that include one-click unsubscribe for bulk senders. Missing one risks regulatory exposure and inbox placement.

How to fix: Add an unsubscribe link, typically in a :::footer block:

:::footer
[Unsubscribe]({{unsubscribeUrl}}) · [Preferences]({{preferencesUrl}})
:::

email/font-size-minimum

Severity: Info

Why it matters: The xs size token maps to 10px, which is below the 14px minimum recommended by accessibility guidelines. Text at 10px is unreadable for many users and fails WCAG 1.4.4 at common zoom levels.

How to fix: Use sm (12px) at minimum, or md (16px) for body copy. Reserve xs only for legal-text contexts where you explicitly accept the tradeoff.


SMS

SMS rules focus on encoding and cost, since every character and encoding choice has a direct impact on segment count and billing.

Rule ID Severity Description
sms/no-images Warning Images in SMS template
sms/no-tables Warning Tables in SMS template
sms/encoding-warning Info Non-GSM-7 characters detected
sms/extended-gsm-chars Info Extended GSM characters that count double
sms/message-length Info Message likely exceeds one segment
sms/segment-count Warning (3+) / Info (2) Message requires multiple segments

sms/no-images

Severity: Warning

Why it matters: SMS is a plain-text protocol. Image syntax in an SMS template won't render — recipients will see raw Markdown, a broken link, or nothing at all depending on the provider.

How to fix: Remove image syntax from sms.md. If you need to share an image, include a plain URL in the message text that links to the image.


sms/no-tables

Severity: Warning

Why it matters: Markdown table syntax does not translate to SMS. The pipe characters and dashes will appear as raw text, making the message unreadable.

How to fix: Remove tables. Present the same information as plain prose or a simple list.


sms/encoding-warning

Severity: Info

Why it matters: GSM-7 encoding allows 160 characters per segment. Characters outside GSM-7 — emoji, accented letters, smart quotes, curly apostrophes — force UCS-2 encoding, which drops segment capacity to 70 characters. The same message that fit in one segment under GSM-7 may now require three segments under UCS-2, tripling cost.

How to fix: Avoid emoji and non-ASCII characters in SMS templates, or consciously accept the encoding cost. Use straight quotes (', ") instead of typographic quotes.


sms/extended-gsm-chars

Severity: Info

Why it matters: Some characters are technically in the GSM-7 character set but occupy two character slots: {, }, [, ], \, ~, |, ^, and . A message with 10 of these characters uses 10 extra character-slots beyond what the visible character count suggests.

How to fix: Be aware of double-counting when estimating message length. If you are near a segment boundary, replacing extended characters with alternatives can keep you in the previous segment.


sms/message-length

Severity: Info

Why it matters: A single SMS segment holds 160 GSM-7 characters or 70 UCS-2 characters. Multi-segment messages cost more and may arrive out of order on some carriers.

How to fix: Shorten the message. Remove filler words, use shorter URLs (via a redirect service you control), and consider what information is truly essential in an SMS context.


sms/segment-count

Severity: Warning (3+ segments) / Info (2 segments)

Why it matters: Each segment is billed separately. A 3-segment message costs 3× a single-segment message. Carriers may also apply concatenation fees on top. Long messages also have higher delivery failure rates.

How to fix: Shorten the message to fit within fewer segments. Two segments (Warning: 3+, Info: 2) is acceptable for important messages; three or more is worth revisiting the content strategy.


Push

Push notification rules exist because each mobile platform enforces hard payload and display limits that result in silent delivery failures or truncated content.

Rule ID Severity Description
push/missing-title Warning No title source found
push/title-length Warning Title exceeds 50 characters
push/body-length Info Body exceeds 120 characters
push/payload-size Warning Payload exceeds 4 KB
push/too-many-actions Warning More than 2 action buttons

push/missing-title

Severity: Warning

Why it matters: Push notifications without a title display the app name (iOS) or no title at all (some Android configurations). A missing title reduces CTR and looks unprofessional.

How to fix: Add a title using any of these approaches:

---
title: "Your order has shipped"
---

Or use an :::header directive or a # heading at the top of the template.


push/title-length

Severity: Warning

Why it matters: iOS truncates push notification titles at approximately 50 characters on lock screen and notification center. Content beyond that is invisible to the recipient.

How to fix: Keep titles under 50 characters. Put the key information first.


push/body-length

Severity: Info

Why it matters: In collapsed notification view (the default), body text is truncated around 120 characters. Recipients who do not expand the notification miss everything after the cutoff.

How to fix: Front-load the most important information. Keep the first 120 characters self-contained so the message is useful even if not expanded.


push/payload-size

Severity: Warning

Why it matters: APNs (iOS) and FCM (Android) both enforce a 4 KB payload limit. Payloads over this limit are silently rejected — the notification never delivers, and no error appears in your logs unless you explicitly check the provider response.

How to fix: Reduce content size. Store large data server-side and fetch it when the notification is tapped rather than embedding it in the payload.


push/too-many-actions

Severity: Warning

Why it matters: Cross-platform support for push action buttons is inconsistent. iOS supports up to 4 categories, but most practical UIs show 2. Android notification channels cap at 3. Beyond 2 actions, behavior varies widely across devices and OS versions.

How to fix: Limit to 2 action buttons for reliable cross-platform behavior.


WhatsApp

WhatsApp Business API enforces strict limits on template structure. Violations result in template rejection by Meta during the approval process — not at send time.

Rule ID Severity Description
whatsapp/too-many-buttons Warning More than 3 buttons in an action block
whatsapp/body-length Warning Body exceeds 1,024 characters
whatsapp/header-length Warning Header exceeds 60 characters
whatsapp/footer-length Warning Footer exceeds 60 characters
whatsapp/button-label-length Warning Button label exceeds 20 characters
whatsapp/cta-button-limit Warning More than 2 CTA buttons

whatsapp/too-many-buttons

Severity: Warning

Why it matters: WhatsApp allows a maximum of 3 quick reply or action buttons per template. Templates with more than 3 are rejected by the Meta API during submission.

How to fix: Reduce to 3 or fewer buttons. Prioritize the most important actions.


whatsapp/body-length

Severity: Warning

Why it matters: WhatsApp template body text is capped at 1,024 characters by the API. Templates that exceed this are rejected during Meta approval and cannot be sent.

How to fix: Shorten the body. If your message requires more context, link to a webpage for details.


whatsapp/header-length

Severity: Warning

Why it matters: WhatsApp header text is limited to 60 characters. Longer headers are rejected by the API.

How to fix: Keep header text concise — treat it as a subject line, not a paragraph.


whatsapp/footer-length

Severity: Warning

Why it matters: WhatsApp footer text is limited to 60 characters. Longer footers are rejected by the API.

How to fix: Keep footer text short. Common uses: brand name, opt-out notice.


whatsapp/button-label-length

Severity: Warning

Why it matters: Button labels in WhatsApp templates are capped at 20 characters. Labels that exceed this are truncated or cause the template to be rejected.

How to fix: Use short, action-oriented labels: "Track Order", "View Details", "Get Offer".


whatsapp/cta-button-limit

Severity: Warning

Why it matters: WhatsApp allows a maximum of 2 call-to-action (URL/phone) buttons per template. Exceeding this limit causes template rejection.

How to fix: Limit to 2 CTA buttons. Use quick reply buttons for additional options if needed.


Slack

Slack's Block Kit API enforces limits on blocks and element counts. Payloads that exceed them receive API errors and fail to post.

Rule ID Severity Description
slack/block-count Warning More than 50 blocks
slack/section-text-length Warning Section text exceeds 3,000 characters
slack/actions-count Warning More than 5 action elements

slack/block-count

Severity: Warning

Why it matters: The Slack API rejects messages with more than 50 blocks. If your message exceeds this, it will not post — the API returns an error and the notification silently fails.

How to fix: Reduce content. Use fewer sections, consolidate related content, or split the message into multiple notifications.


slack/section-text-length

Severity: Warning

Why it matters: Slack limits section block text to 3,000 characters. Sections that exceed this are rejected by the API.

How to fix: Split long sections into multiple section blocks. Move detailed content to a linked webpage.


slack/actions-count

Severity: Warning

Why it matters: An actions block in Slack supports a maximum of 5 interactive elements (buttons, selects, etc.). More than 5 causes the API to reject the entire message payload.

How to fix: Limit action blocks to 5 elements. Combine or remove less important actions.


MS Teams

Teams Incoming Webhook cards have payload size and action limits that result in webhook rejection when exceeded.

Rule ID Severity Description
msteams/card-payload-size Warning Card payload exceeds 28 KB
msteams/action-count Warning More than 6 actions

msteams/card-payload-size

Severity: Warning

Why it matters: Microsoft Teams Incoming Webhooks reject card payloads larger than 28 KB. The webhook returns an error and the notification does not appear in the channel.

How to fix: Reduce card content. Remove unnecessary images, shorten text, and avoid embedding large data in the card body. Link out to detailed content instead.


msteams/action-count

Severity: Warning

Why it matters: Teams Adaptive Cards support up to 6 top-level actions. Actions beyond the limit are pushed into an overflow menu that most users will not discover.

How to fix: Limit the card to 6 actions. Surface only the most important actions directly; remove or consolidate the rest.


Accessibility

Accessibility rules enforce WCAG 2.1/2.2 compliance and screen reader best practices. These apply to all channels.

Rule Severity WCAG Description
a11y/alt-text-quality Warning 1.1.1 Generic or filename-based alt text
a11y/alt-text-length Info 1.1.1 Alt text exceeds 125 characters
a11y/image-only-link Warning 1.1.1 Link contains only an image with no text fallback
a11y/link-text-is-url Warning 2.4.4 Link text is a raw URL
a11y/table-missing-headers Warning 1.3.1 Data table has no header row
a11y/empty-heading Warning 1.3.1 Heading with no text content
a11y/excessive-emoji Info Excessive emoji in subject or body text
a11y/duplicate-heading-text Info 2.4.6 Same-level headings with identical text
a11y/adjacent-duplicate-links Info 2.4.4 Consecutive links pointing to same URL

a11y/alt-text-quality

Severity: Warning
WCAG: 1.1.1 Non-text Content

Why it matters: Screen readers announce alt text to describe images. Generic text like "image.png", "logo", or "banner" provides no useful information to visually impaired users.

How to fix: Replace with text that conveys the image's purpose:

![banner.png](url)           <!-- Bad: filename as alt -->
![Summer sale: 30% off](url) <!-- Good: describes the content -->

a11y/alt-text-length

Severity: Info
WCAG: 1.1.1 Non-text Content

Why it matters: Alt text over 125 characters becomes tedious for screen reader users. Long descriptions are better placed in surrounding body text.

How to fix: Shorten alt text to under 125 characters. Move detailed descriptions into the body.


Severity: Warning
WCAG: 1.1.1 / 2.4.4

Why it matters: If a link contains only an image and the image fails to load (common in email clients that block images by default), the link becomes invisible. There is no text fallback for screen readers or image-blocked views.

How to fix: Add text alongside the image inside the link, or use a text link with the image as decoration:

[![Shop now](banner.jpg)](https://shop.example.com)
<!-- Better: use a button link instead -->
[Shop Now](https://shop.example.com){button}

Severity: Warning
WCAG: 2.4.4 Link Purpose

Why it matters: Screen readers often list all links on a page. Raw URLs like "https://example.com/orders/12345" are meaningless out of context. Descriptive text helps users navigate by link list.

How to fix:

[https://example.com/track](https://example.com/track)  <!-- Bad -->
[Track your order](https://example.com/track)            <!-- Good -->

a11y/table-missing-headers

Severity: Warning
WCAG: 1.3.1 Info and Relationships

Why it matters: Screen readers use table headers to announce column context as users navigate cells. Without headers, data tables become a grid of unlabeled values.

How to fix: Add a header row to your markdown table:

| Product | Qty | Price |    <!-- Header row -->
|---------|-----|-------|
| Widget  | 2   | $10   |

a11y/empty-heading

Severity: Warning
WCAG: 1.3.1 Info and Relationships

Why it matters: Screen reader users navigate by headings to scan document structure. An empty heading creates a dead entry in the heading list and breaks the document outline.

How to fix: Add text content to the heading, or remove it if it was added by mistake.


a11y/excessive-emoji

Severity: Info

Why it matters: Screen readers announce each emoji by its full Unicode name (e.g., "face with tears of joy", "rocket", "fire"). A subject line with 5 emoji forces the listener through 5 verbose announcements before reaching the actual content.

Triggers: More than 2 emoji in subject/preheader, or more than 3 emoji in a single body paragraph or heading.

How to fix: Reduce emoji count. Use one emoji for emphasis, not several for decoration.


a11y/duplicate-heading-text

Severity: Info
WCAG: 2.4.6 Headings and Labels

Why it matters: Screen reader users who navigate by heading list see "Order Details" twice with no way to distinguish which section is which. Unique headings help users jump to the right section.

How to fix: Differentiate heading text (e.g., "Shipping Details" vs "Billing Details" instead of two "Details" headings).


Severity: Info
WCAG: 2.4.4 Link Purpose

Why it matters: Two consecutive links pointing to the same URL appear as separate entries in screen reader link lists. This creates confusion and extra navigation steps.

How to fix: Combine into a single link:

[View order](https://example.com/order) [details](https://example.com/order)  <!-- Bad -->
[View order details](https://example.com/order)                                <!-- Good -->

Template

Template rules apply to all channels. They catch syntax errors, undefined variables, and authoring patterns that produce incorrect output.

Rule ID Severity Description
template/unknown-directive Error Unrecognized :::name directive
template/undefined-variable Warning {{path}} not in data schema
template/nullable-access Warning Nullable variable used without :::if guard
template/raw-expression Warning Triple-brace {{{ }}} unescaped expression
template/thematic-break Warning Bare --- in body may be unintended
template/directive-in-table Warning Directive inside a Markdown table
template/inline-directive Warning ::: mid-line instead of at line start
template/unknown-attr Warning Unrecognized {key="value"} attribute
template/unsupported-attr Warning Valid attribute used on wrong directive/channel
template/invalid-attr-value Warning Attribute value not in the allowed set
template/empty-directive-body Warning Directive has no content
template/action-without-link Warning :::action has no link
template/table-in-fields Warning Table inside :::fields won't render

template/unknown-directive

Severity: Error

Why it matters: A :::name that Norq does not recognize is silently dropped during compilation, meaning the content it wraps never appears in the output. This is a guaranteed content loss.

How to fix: Check the spelling. Valid directives: :::header, :::footer, :::action, :::callout, :::hero, :::fields, :::media, :::columns, :::list, :::highlight, :::centered, :::raw, :::if, :::each. See Directives for the full reference.


template/undefined-variable

Severity: Warning

Why it matters: A {{path}} expression that references a variable not declared in data.schema.yaml will render as an empty string at runtime. If the variable is supposed to show user data (name, order ID, etc.), recipients will see a blank.

How to fix: Add the variable to data.schema.yaml, or fix the typo in the expression.


template/nullable-access

Severity: Warning

Why it matters: Using a nullable variable directly (without checking if it exists first) can produce blank output or runtime errors when the value is absent.

How to fix: Wrap nullable variable usage in an :::if guard:

:::if user.middleName
{{user.middleName}}
:::

template/raw-expression

Severity: Warning

Why it matters: Triple-brace expressions ({{{value}}}) output raw, unescaped HTML. If the value contains user-supplied content, this is an XSS vulnerability. Even for non-user content, unescaped output can break the compiled email's HTML structure.

How to fix: Use double-brace {{value}} unless you are deliberately injecting trusted HTML. If you do need raw HTML, confirm the value is safe and controlled.


template/thematic-break

Severity: Warning

Why it matters: A bare --- on its own line is valid YAML frontmatter syntax and valid Markdown horizontal rule syntax. In the body of a template, it compiles to an <hr> element, which may not be what you intended. It also triggers false positives in frontmatter parsers if placed near the top.

How to fix: Use *** for intentional horizontal rules in the body. Reserve --- for frontmatter delimiters only.


template/directive-in-table

Severity: Warning

Why it matters: Markdown table syntax parses pipe characters (|) as column separators. A :::directive inside a table cell breaks the table's parse structure and the directive fails to render correctly.

How to fix: Move the directive outside the table. If you need structured data alongside a directive, place the directive before or after the table.


template/inline-directive

Severity: Warning

Why it matters: The ::: syntax must appear at the start of a line. A :::directive mid-sentence is not recognized as a block directive and will appear as literal text in the output.

How to fix: Move the directive to its own line with no preceding characters:

Some text.
 
:::callout
This is the callout content.
:::

template/unknown-attr

Severity: Warning

Why it matters: An unrecognized key in a {key="value"} attribute block is silently ignored. If you misspelled an attribute name, the intended behavior won't apply.

How to fix: Check the attribute name against the directive documentation. Common attributes: align, size, url, type, label.


template/unsupported-attr

Severity: Warning

Why it matters: Some attributes are valid in general but not supported by a specific directive or channel. For example, url on a :::callout has no effect. The attribute is accepted but silently ignored during compilation.

How to fix: Verify which attributes a directive supports. Check the relevant channel docs for per-channel attribute support.


template/invalid-attr-value

Severity: Warning

Why it matters: Attributes like size and align accept only a specific set of values. An invalid value (e.g., size="huge") is silently ignored, reverting to the default.

How to fix: Use a value from the allowed set. Valid size values: xs, sm, md, lg, xl, 2xl, 3xl, 4xl. Valid align values: left, center, right.


template/empty-directive-body

Severity: Warning

Why it matters: A directive with no content between its opening and closing markers compiles to an empty block. This is almost always a mistake — either the content was accidentally deleted or the directive was left as a placeholder.

How to fix: Add content to the directive body, or remove the directive entirely.


Severity: Warning

Why it matters: An :::action block without a link produces a button with no destination. On email, this is an unclickable button. On other channels, it may be dropped entirely.

How to fix: Add a link inside the action block:

:::action
[Track Shipment]({{order.trackingUrl}})
:::

template/table-in-fields

Severity: Warning

Why it matters: The :::fields directive compiles to a key-value table layout. A Markdown table nested inside it conflicts with that layout and will not render correctly — the inner table is treated as raw text.

How to fix: Move the table outside the :::fields block. Use :::fields for simple key-value pairs only.


Structure

Structure rules validate your notification directory layout against the required folder conventions. These run before any template is parsed.

Rule ID Severity Description
structure/missing-type-folder Warning Required type folder missing
structure/invalid-root-entry Error Unknown entry at notifications root
structure/invalid-name Error Directory name violates naming rules
structure/too-deep Warning Notification nested beyond 4 levels
structure/duplicate-channel-format Error Both .md and .json for same channel
structure/no-channels Error Notification directory has no channel files
structure/unrecognized-file Warning Unknown file in notification directory

structure/missing-type-folder

Severity: Warning

Why it matters: Norq requires exactly three top-level type folders under notifications/: system/, transactional/, and promotional/. Missing folders cause the resolver to skip those notification types.

How to fix: Create the missing folder(s). Run norq init in an existing project to scaffold missing structure without overwriting existing files.


structure/invalid-root-entry

Severity: Error

Why it matters: Only system/, transactional/, promotional/, and directories starting with _ (e.g., _shared/) are allowed at the notifications/ root. Anything else causes a resolver error.

How to fix: Move the entry into one of the allowed type folders, rename it to start with _ if it is a shared/partial directory, or delete it if it should not be there.


structure/invalid-name

Severity: Error

Why it matters: Notification directory names must match [a-z0-9]+(-[a-z0-9]+)* — lowercase letters, digits, and hyphens only, no leading/trailing hyphens. Invalid names break ID generation and routing.

How to fix: Rename the directory to use only lowercase letters, digits, and hyphens. Examples: order-shipped, password-reset, welcome-offer.


structure/too-deep

Severity: Warning

Why it matters: The resolver supports at most 4 levels from the notifications root: type/category/subcategory/notification. Deeper nesting is not recognized, and notifications inside it will not be discovered.

How to fix: Flatten the directory structure to stay within 4 levels. Example of maximum depth: notifications/transactional/account/security/password-reset/.


structure/duplicate-channel-format

Severity: Error

Why it matters: Having both email.md and email.json in the same notification directory is ambiguous — Norq cannot determine which format to use, and compilation will fail.

How to fix: Delete one of the two files. Use .md for Markdown templates (all channels except those requiring native JSON) and .json only where native JSON format is explicitly required.


structure/no-channels

Severity: Error

Why it matters: A notification directory with no channel files (email.md, sms.md, slack.md, push.md, whatsapp.md, msteams.md, or their .json variants) is not a valid notification. The resolver will not recognize it as a notification and it cannot be compiled or sent.

How to fix: Add at least one channel file, or delete the directory if it was created by mistake.


structure/unrecognized-file

Severity: Warning

Why it matters: Files in a notification directory that Norq does not recognize (data.schema.yaml, data.samples.yaml, tests.yaml, and channel files are the expected set) are ignored during compilation. An unrecognized filename often indicates a typo in a required file.

How to fix: Check the filename for typos. Rename to one of the recognized filenames, or move the file to a location where it belongs.