The channel-agnostic grammar shared by stored email, Slack and SMS templates: {{variable}}, {{upper variable}} (the closed filter set, TEMPLATE_FILTERS), {{#if optionalVariable}}…{{/if}} and {{#equals axis "VALUE"}}…{{/equals}}. Imports nothing.
parseTemplateText(source): the AST plus recoverable parse issues (MALFORMED_TAG, UNCLOSED_BLOCK, UNEXPECTED_BLOCK_CLOSE, MISNESTED_BLOCK).interpolateTemplate(nodes, variables, { escape?, onMissingVariable? }): one pass, a missing value renders empty, #if keeps a value non-empty after trim, the first matching #equals per axis wins. escape applies to interpolated values only.validateTemplateText(source, schema, hooks?) / validateTemplateNodes: checks against { variables: Record<name, "required" | "optional">, axes: Record<axis, values[]> }. Codes: UNKNOWN_PLACEHOLDER, UNKNOWN_CONDITION_AXIS, UNKNOWN_EQUALS_VALUE, INVALID_OPTIONAL_GUARD, OPTIONAL_VARIABLE_UNGUARDED, UNICODE_FORMAT_CONTROL. A channel adds its own #equals rules through hooks.equals, as notification-services' email validator does.Channel rules (length caps, link hosts, escaping, coverage) stay with the channel. See docs/proposals/SLACK_TEMPLATES.md § 5.2.