[object Object]

Personalization tokens are the feature that makes marketing automation feel intelligent and is also the feature that makes it look stupid. Every team that uses tokens has shipped a “Hi ,” email at least once. Most have shipped worse. “Special offer for [company] in [industry]” with both brackets unresolved is the kind of send that makes your audience screenshot you for sport.

The fallback syntax exists. Nobody uses it consistently. Here is the system that stops the bleeding.

Why tokens fail silently

HubSpot evaluates tokens at send time. If the property is null, the token renders empty. There is no warning at send time. There is no warning at draft time. The preview uses your test contact, which is fully populated because you populated it. The real list has 30% empties on company name and 60% empties on industry.

The combination of “no compile time check” and “preview always works” is the failure mode.

The fallback hierarchy

Every token gets a fallback. Always. The fallback hierarchy:

  1. Real property value
  2. Adjacent property fallback (firstname → email local part)
  3. Generic friendly fallback (“there”, “your team”)
  4. Rewrite the sentence to not need the token

The fourth option is underrated. If your sentence collapses without the token, the sentence is not strong. Strong copy survives token failure.

Bad:  Hi {{contact.firstname}}, ready to scale {{contact.company}}?
Mid:  Hi {{contact.firstname|there}}, ready to scale {{contact.company|your team}}?
Good: A note on scaling — for {{contact.firstname|you}} and {{contact.company|your team}}.
Best: A note on scaling — five patterns we see in growth teams.

The “best” version uses no tokens at all. It does not feel less personal. It feels more honest.

The audit query

Before you send, query the list for token coverage.

async function tokenCoverage(listId, properties) {
  const members = await getListMembers(listId);
  const total = members.length;
  const coverage = {};

  for (const prop of properties) {
    coverage[prop] = members.filter(
      (m) => m.properties[prop] && m.properties[prop].trim() !== "",
    ).length;
  }

  return Object.entries(coverage).map(([prop, count]) => ({
    property: prop,
    coverage: ((count / total) * 100).toFixed(1) + "%",
    missing: total - count,
  }));
}

console.log(
  await tokenCoverage("list_42", [
    "firstname",
    "company",
    "industry",
    "jobtitle",
  ]),
);

If firstname coverage is under 95%, the sentence cannot lean on firstname. If industry is at 41%, do not use industry-specific copy at all. Use a smart content rule on a different signal.

Smart content is not a token fallback

Teams confuse smart content with token fallback. They are different.

  • Token fallback: same content, different value substituted per recipient
  • Smart content: different content shown per segment

Use smart content when the message itself changes by segment. Use tokens when the message stays and only a value swaps. Mixing them creates Frankenstein emails.

A common abuse: a single email with three smart content blocks based on lifecycle stage, each with two unresolved tokens. The marketer thinks they are personalizing. The recipient sees a generic email with weird gaps.

Job title is the worst offender

Job title is the property most teams reach for in personalization and most often empty. Even when populated, it is wildly inconsistent. “Director of Marketing” and “Marketing Director” and “Sr. Director, Marketing” are the same person across three forms.

Do not personalize on raw job title. Personalize on derived role category, which you compute and store as a clean property.

// Custom code action on contact create or job title change
exports.main = async (event, callback) => {
  const title = (event.inputFields["jobtitle"] || "").toLowerCase();

  let role = "unknown";
  if (/ceo|founder|owner|president/.test(title)) role = "executive";
  else if (/vp|director|head of/.test(title)) role = "leader";
  else if (/manager|lead/.test(title)) role = "manager";
  else if (/engineer|developer|designer|analyst/.test(title))
    role = "ic_technical";
  else if (/marketing|sales|success/.test(title)) role = "ic_gtm";

  callback({ outputFields: { role_category: role } });
};

Now your smart content branches on role_category, not on raw title strings. The branch logic is finite and testable.

Email is not the only place tokens break

CTAs, meeting link confirmation pages, chatbot openers, smart CRM card text, mobile push notifications. Every surface that supports tokens fails the same way and is harder to audit because it is not in the Marketing tab.

Chatbot is the meanest. A bot that says “Hi , how can I help with at ?” on first impression loses the lead. Audit your bot scripts the same way you audit email.

See HubSpot smart content rules discipline for the segmentation side and the HubSpot complete guide for the surface inventory.

Pre-send checklist

Five steps. Tape them to the monitor.

  • Coverage check: every token used has >95% coverage on the send list, or has a fallback
  • Fallback in every token, no exceptions
  • Preview against three test contacts: complete, half-populated, empty
  • Render the plain-text version, look for ”, ,” and ” ” (double space from collapsed tokens)
  • Smart content versions all checked, not just default

The plain-text double-space check is the cheapest one. Two spaces in a sentence means a token between them collapsed.

The token deprecation pattern

Sometimes you remove a property and forget the tokens referencing it still exist in 40 templates. Build a token grep.

async function findTokenUsage(propertyName) {
  const pattern = new RegExp(`{{\\s*contact\\.${propertyName}`, "i");
  const templates = await listAllEmailTemplates();
  return templates.filter((t) => pattern.test(t.html));
}

Run before any property archival. The deprecation does not happen until usage is zero.

Bottom line

  • Every token gets a fallback or the sentence gets rewritten; no exceptions.
  • Coverage-audit the send list before you write the copy, not after.
  • Smart content and token fallback solve different problems; do not stack them.
  • Personalize on derived clean properties, not raw user-entered strings.
  • The pre-send plain-text scan for double spaces catches what previews miss.
[object Object]
Share