What happened?
Summary
When sending a campaign, contact variables (e.g. {{firstName}}, {{nome_cliente,fallback=Cliente}}) are correctly interpolated in the HTML body but are sent as-is in the subject line. Recipients receive the literal placeholder text in their inbox subject.
Reproduction
- Create a contact book with a custom variable, e.g.
nome_cliente.
- Add a contact with
properties.nome_cliente = "Pedro".
- Create a campaign with:
subject: Welcome to My Company, {{nome_cliente,fallback=Cliente}}!
html: <p>Olá {{nome_cliente,fallback=Cliente}}</p>
- Send the campaign.
Expected
Both the subject and the HTML body should show Pedro (or the fallback if missing).
Actual
- HTML body:
Olá Pedro ✅
- Subject in the inbox:
Welcome to My Company, {{nome_cliente,fallback=Cliente}}! ❌
Root cause
In apps/web/src/server/service/campaign-service.ts, replaceContactVariables is applied only to the html string before persisting the email row. The subject is taken straight from emailConfig.subject and never passed through the same regex:
let html = replaceUnsubscribePlaceholders(campaign.html, unsubscribeUrl);
html = replaceContactVariables(html, contact, allowedVariables);
// ...
data: {
from: emailConfig.from,
subject: emailConfig.subject, // ← never interpolated
html, // ← interpolated
...
}
The existing CONTACT_VARIABLE_REGEX already supports the {{key,fallback=...}} syntax, so the fix is just to also run the subject through replaceContactVariables (and replaceUnsubscribePlaceholders is not needed for the subject).
Suggested fix
Apply replaceContactVariables to emailConfig.subject before writing the email row in both code paths (db.email.create for suppressed and normal sends), e.g.:
const subject = replaceContactVariables(
emailConfig.subject,
contact,
allowedVariables,
);
// ...
data: {
...,
subject,
html,
...
}
Replication Steps
-
Create a contact book and declare a custom variable, e.g. nome_cliente.
- Dashboard: Contact Books → New → add variable
nome_cliente.
- Or via API:
POST /api/v1/contactBooks with { "name": "test", "variables": ["nome_cliente"] }.
-
Add a contact to that book with the variable populated:
POST /api/v1/contactBooks/{bookId}/contacts
{
"email": "you@example.com",
"subscribed": true,
"properties": { "nome_cliente": "Pedro" }
}
- Create a campaign pointing at that contact book, using the variable in BOTH the subject and the HTML:
{
"name": "subject-var-repro",
"from": "Test <test@yourdomain.com>",
"contactBookId": "<bookId>",
"subject": "Welcome, {{nome_cliente,fallback=Cliente}}!",
"html": "<p>Hi {{nome_cliente,fallback=Cliente}}!</p><p>{{usesend_unsubscribe_url}}</p>",
"sendNow": true
}
- Open the email in the recipient's inbox.
Result:
- HTML body renders correctly: "Hi Pedro!"
- Inbox subject shows the literal placeholder: "Welcome, {{nome_cliente,fallback=Cliente}}!"
(instead of "Welcome, Pedro!")
Also reproduces with built-in variables like {{firstName}} in the subject.
Self hosted or Cloud?
Self hosted
What browsers are you seeing the problem on?
Chrome (or chrome based like Brave, Arc, etc)
What happened?
Summary
When sending a campaign, contact variables (e.g.
{{firstName}},{{nome_cliente,fallback=Cliente}}) are correctly interpolated in the HTML body but are sent as-is in the subject line. Recipients receive the literal placeholder text in their inbox subject.Reproduction
nome_cliente.properties.nome_cliente = "Pedro".subject:Welcome to My Company, {{nome_cliente,fallback=Cliente}}!html:<p>Olá {{nome_cliente,fallback=Cliente}}</p>Expected
Both the subject and the HTML body should show
Pedro(or the fallback if missing).Actual
Olá Pedro✅Welcome to My Company, {{nome_cliente,fallback=Cliente}}!❌Root cause
In
apps/web/src/server/service/campaign-service.ts,replaceContactVariablesis applied only to thehtmlstring before persisting the email row. Thesubjectis taken straight fromemailConfig.subjectand never passed through the same regex:The existing
CONTACT_VARIABLE_REGEXalready supports the{{key,fallback=...}}syntax, so the fix is just to also run the subject throughreplaceContactVariables(andreplaceUnsubscribePlaceholdersis not needed for the subject).Suggested fix
Apply
replaceContactVariablestoemailConfig.subjectbefore writing theemailrow in both code paths (db.email.createfor suppressed and normal sends), e.g.:Replication Steps
Create a contact book and declare a custom variable, e.g.
nome_cliente.nome_cliente.POST /api/v1/contactBookswith{ "name": "test", "variables": ["nome_cliente"] }.Add a contact to that book with the variable populated:
POST /api/v1/contactBooks/{bookId}/contacts{ "email": "you@example.com", "subscribed": true, "properties": { "nome_cliente": "Pedro" } }POST /api/v1/campaigns{ "name": "subject-var-repro", "from": "Test <test@yourdomain.com>", "contactBookId": "<bookId>", "subject": "Welcome, {{nome_cliente,fallback=Cliente}}!", "html": "<p>Hi {{nome_cliente,fallback=Cliente}}!</p><p>{{usesend_unsubscribe_url}}</p>", "sendNow": true }Result:
(instead of "Welcome, Pedro!")
Also reproduces with built-in variables like
{{firstName}}in the subject.Self hosted or Cloud?
Self hosted
What browsers are you seeing the problem on?
Chrome (or chrome based like Brave, Arc, etc)