Skip to content

🐞 - Campaign subject is not interpolated with contact variables (only HTML body is) #396

@pgarcias01

Description

@pgarcias01

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

  1. Create a contact book with a custom variable, e.g. nome_cliente.
  2. Add a contact with properties.nome_cliente = "Pedro".
  3. Create a campaign with:
    • subject: Welcome to My Company, {{nome_cliente,fallback=Cliente}}!
    • html: <p>Olá {{nome_cliente,fallback=Cliente}}</p>
  4. 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

  1. 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"] }.
  2. 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" }
     }
  1. Create a campaign pointing at that contact book, using the variable in BOTH the subject and the HTML:
    • 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
     }
  1. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions