Variables

Use SML interpolation with {{name}} and ${name}, including where variables are allowed and how they compile.

SML templates interpolate per-recipient data with a flat variable lookup, no expressions, loops, or conditionals. Two equivalent syntaxes are supported:

<Text>Hi {{firstName}}, your plan is ${plan}.</Text>

{{firstName}} and ${plan} both resolve the same way: a flat name looked up against the send-time variables object. There's no dot-path evaluation beyond a flat key, no filters, and no logic, the same interpolation semantics templates already use today, carried over unchanged into SML.

Where variables are allowed

A {{variable}} (or ${variable}) can appear:

  • As text content inside inline-model elements: <Preview>, <Heading>, <Text>, and inside <Button>'s label.
  • Inside a string attribute value, e.g. href="{{ctaUrl}}" or src="{{heroImage}}".

It cannot appear as a bare child of a block-only element (<Container>, <Section>, <Row>, <Column>), wrap it in <Text> first. That's the same rule that applies to plain text: block elements don't accept inline content directly.

<!-- Compile error: invalid-child -->
<Section>{{issueNumber}}</Section>

<!-- Correct -->
<Section>
  <Text>{{issueNumber}}</Text>
</Section>

In attributes

Any string attribute can carry a variable, most commonly href on <Button>/<a> and src on <Image>:

<Button href="{{ctaUrl}}" class="bg-blue-600 text-white px-6 py-3 rounded-md">
  Open dashboard
</Button>
<Image src="{{heroImage}}" alt="Template gallery" width={536} />

Numeric attributes (level, width, height on <Heading>/<Image>/<Spacer>) take a literal {16}-style number, not an interpolation, variables only ever resolve to strings at send time.

Compiling and plain text

Variables print through the compiler verbatim, the compiled HTML and the generated plain-text alternative both keep {{name}}/${name} as literal text; substitution happens per recipient at send time, not at compile time. This is why a compiled artifact can be cached and reused across every recipient of a send: the artifact is one deterministic compile of the source, and interpolation happens downstream, per recipient, in the send path.

Discovering the variables a template uses

Because interpolation is a flat name lookup with no dynamic construction, every variable reference in a document can be statically collected, that's what powers the editor's variables rail and the test-send form's schema-driven inputs, without scanning raw JSON. Variable collection also walks <If test="..."> predicates. a name referenced only inside a predicate (never in {{}}/${} interpolation) still shows up in the variables rail and the test-send form.

Variables in conditionals

{{}}/${} interpolation and <If> predicates both read from the same flat variable lookup, but they're not the same feature: interpolation always prints a value (or leaves it blank), while <If test="plan == 'pro'"> uses a variable to choose between two branches of markup at send time and never prints the predicate itself. See Conditionals for the predicate grammar and how a missing variable resolves inside a test.

On this page