Use merge tags, conditional blocks, and dynamic sections to create personalized, relevant emails
Personalized emails outperform generic ones. Recipients are more likely to open, read, and act on emails that address them by name, reference their specific activity, and show content relevant to their situation. Lettr’s template language provides merge tags, conditionals, loops, and filters that let you build a single template that renders differently for each recipient.
<p style="font-size: 16px; color: #4a4a68;"> Hello {{ name }},</p><p style="font-size: 16px; color: #4a4a68;"> Your account ({{ email }}) has been active since {{ signup_date }}.</p>
Always provide defaults for personalization fields. An email that reads “Hello ,” because the name field is missing looks broken.
Copy
<!-- Provide fallback values with the 'or' keyword --><p>Hello {{ name or 'there' }},</p><p>Your {{ plan_name or 'current' }} plan includes {{ email_limit or '1,000' }} emails per month.</p>
Never assume all substitution data will be present. Missing fields render as empty strings by default, which creates gaps in your content. Use the or keyword on every variable that appears in visible text.
By default, merge tags are HTML-escaped to prevent cross-site scripting. If you need to render trusted HTML content, use triple curly braces:
Copy
<!-- HTML-escaped (safe for user-provided data) --><p>{{ user_message }}</p><!-- Unescaped (use only with trusted, system-generated content) --><div>{{{ custom_html_block }}}</div>
Only use triple curly braces ({{{ }}}) with content you control — never with user-submitted data. Unescaped content can inject arbitrary HTML into your emails.
{{if plan == 'enterprise'}} <p style="color: #4a4a68;"> Your Enterprise plan includes unlimited emails and dedicated support. </p>{{elseif plan == 'pro'}} <p style="color: #4a4a68;"> Your Pro plan includes 50,000 emails per month. <a href="https://example.com/upgrade" style="color: #6366F1;">Upgrade to Enterprise</a> for unlimited sending. </p>{{elseif plan == 'free'}} <p style="color: #4a4a68;"> Your Free plan includes 3,000 emails per month. <a href="https://example.com/upgrade" style="color: #6366F1;">Upgrade your plan</a> to send more. </p>{{else}} <p style="color: #4a4a68;"> <a href="https://example.com/pricing" style="color: #6366F1;">Choose a plan</a> to get started. </p>{{end}}
Batch sending with per-recipient substitution data is the most efficient way to send personalized emails at scale. One API call with 200 recipients and individual data is much faster than 200 separate calls.
Before sending, test your template with different substitution data to verify all branches render correctly.
Copy
// Test case 1: Premium user with itemsconst premiumData = { name: 'Alice', premium: true, plan: 'enterprise', items: [ { name: 'Item A', quantity: '1', price: '$10.00' } ]};// Test case 2: Free user with no itemsconst freeData = { name: '', // Test missing name → should fall back to default premium: false, plan: 'free', items: [] // Test empty loop};// Test case 3: Edge case — very long valuesconst edgeCaseData = { name: 'Alexandrina Konstantinopolskaya-Featherington', premium: true, plan: 'pro', items: Array(20).fill({ name: 'Item', quantity: '1', price: '$5.00' })};
Pay special attention to empty states. What does your template look like when items is an empty array? When name is missing? When a conditional branch has no matching case? Every possible state should render cleanly.
Forgetting the or keyword results in empty strings when data is missing. “Hello ,” or “Your plan includes emails per month” looks broken. Always add fallbacks: {{ name or 'there' }}.
Over-personalizing subject lines
Using “Hey !” in every subject line has diminishing returns. Recipients learn to ignore it, and some find it off-putting. Save name personalization for the email body and keep subject lines focused on the content.
Not testing all conditional branches
If you have 4 plan tiers, you need to test all 4 renderings plus the else fallback. A broken conditional in a branch you didn’t test will surprise you in production.
Using unescaped output with user data
Triple curly braces ({{{ }}}) skip HTML escaping. If user-submitted data contains HTML tags or scripts, it renders raw in the email. Only use triple braces with system-generated, trusted content.
Sending the same test data every time
Testing always with name: 'Test User' and plan: 'pro' misses edge cases. Vary your test data to cover missing fields, empty arrays, long strings, and unusual characters.