Marketing User Flags: Store Facts, Not Decisions

Home / MarTech / User Flags
Daniel Robles Published Data architecture Lifecycle

“Is this a new customer?” sounds like a yes or no question, so teams answer it with a flag: is_new_customer = true. Then the store, the app, the sweepstakes form and guest checkout each write their own version of it, a nightly job overwrites them all, and a customer with two orders gets “15% off your first order.” I used to say flags are a legacy way to target people and that profiles make them unnecessary. That is too strong. A profile is made of attributes, and some of them are booleans that should exist. The real problem is narrower: flags that store decisions instead of facts, with no owner, no definition and no expiry, and flags used as triggers. This article separates the flags worth keeping from the ones to delete, shows the event-based alternative (send the facts in the payload, decide at the point of use), and is honest about when your platform or operating model still needs a flag. It builds on Marketing Eventing and the sweepstakes example in Launching a Journey in Braze vs SFMC.

1. Not all flags are the same

A flag is any boolean, or near-boolean, on a person that something reads to decide what to do. Five kinds show up in every marketing stack, and they deserve different treatment. (These are not feature flags; release toggles in tools like LaunchDarkly are a different thing.)

KindExamplesWho sets itVerdict
Fact flagemail_opted_in, is_employee, deceasedThe system of recordkeep
Compliance flagdo_not_contact, legal_hold, fraud_reviewAn accountable person or processkeep, audited
Governed derived flagcross_sell_eligible with an owner, a contract and reason codesOne model, computed in one placekeep, with rules
Campaign flageligible_bf_2024, send_winback = 1Whoever built the campaigndelete
Manual flaga CSV upload that sets vip = trueSomeone, oncereplace

The first two record facts. The third is a decision, but a governed one: the kind of field MarTech Data Contracts describes, with an owner, a definition, freshness rules and reason codes. The last two are decisions stored as data, and they are where the trouble comes from.

2. The new-customer problem

Take Mike B. He bought boots in a store in 2024, entered a sweepstakes last year, and today he creates an app account with Sign in with Apple. Is he a new customer?

DepartmentTheir definition of newMike B. is
MarketingNo prior contact or purchase with the brandNot new (store order, sweepstakes entry)
ProductFirst account createdNew
E-commerceNo online ordersNew (his orders were in store)
StoresNo purchases in any channelNot new (2 orders)
FinanceFirst order in the reporting periodDepends on the month
Data and analyticsFirst time seen in the identity graphNot new (first seen 2024)

Every answer is right for its department. A single boolean cannot hold six definitions, so whoever writes the flag last decides which one wins.

Marketing alone splits it further. Lifecycle wants to know whether to send the welcome series, paid media whether to exclude him from acquisition audiences, and loyalty whether to offer enrollment. Those are three questions answered from the same facts, not three flags.

The fix is not a better flag. It is to stop asking a boolean to carry a definition, and to send the facts that every definition is built from.

IS THIS A NEW CUSTOMER? ASK THE FACTS, NOT A FLAGweb sign-upemail + passwordapp installSign in with Applestore POSreceipt emailsweepstakesform entryguest checkoutorder emailcall centeragent-createdidentity + historywho is this, really?user.signed_upcustomer_facts: first_seen_channel prior_channels[] orders_lifetime customer_status facts_as_ofbrand newwelcome + first-order offerknown prospectwelcome, skip the introexisting customeraccount perks, no discountTHE FLAG WAYevery channel writes is_new_customer = true | false | null on its own schedule; the last write wins
One question, six front doors. Hover or tap any channel to see what it knows and what it would write to a flag, then follow the event to the three branches.

3. The event-based alternative: facts in the payload

If journeys are triggered by events, the event is the natural place for the facts the journey needs. When the accounts service publishes user.signed_up, it asks the identity and history lookup one question, “what do we already know about this person?”, and includes the answer:

{
  "specversion": "1.0",
  "type": "user.signed_up.v1",
  "source": "accounts-service",
  "subject": "c_20931",
  "time": "2026-11-14T10:04:00Z",
  "data": {
    "channel": "app",
    "method": "apple_id",
    "email_marketing_status": "opted_in"
  },
  "customer_facts": {
    "first_seen_at": "2024-06-02",
    "first_seen_channel": "store_pos",
    "prior_channels": ["store_pos", "sweepstakes"],
    "orders_lifetime": 2,
    "last_order_at": "2025-12-20",
    "customer_status": "customer",
    "facts_as_of": "2026-11-14T10:04:00Z"
  }
}

Three rules make this work:

  • Facts, not conclusions, wherever possible. orders_lifetime and prior_channels are facts; any team can build its own definition of new from them.
  • If you include a conclusion, make it an enum with a definition, not a boolean. customer_status: new | prospect | customer | lapsed says which kind of not-new someone is; is_new_customer: false does not.
  • Stamp the time. facts_as_of says when the facts were true. A stored flag never tells you how old it is.

Branching on the payload

The router (or your backend) sends the facts to Braze as properties of the custom event, and the Canvas reads them from the event that started it:

{% case canvas_entry_properties.${customer_status} %}
  {% when 'new' %}
    Welcome! Here is 15% off your first order.
  {% when 'prospect' %}
    Welcome back. You entered our sweepstakes; here is what is new.
  {% when 'customer' %}
    Your account is ready. Here is what members get.
{% endcase %}

SFMC can do the same thing through an API Event entry: the facts become columns in the entry data, and a decision split reads them. It is still a data extension underneath, but it is filled at event time instead of by a nightly flag job.

API Event Data (entry data extension columns):
  CustomerStatus    Text(20)   new | prospect | customer | lapsed
  OrdersLifetime    Number
  FirstSeenChannel  Text(30)
  FactsAsOf         Date

Journey: decision split on Entry: CustomerStatus
  new       -> Welcome + first-order offer
  prospect  -> Welcome, skip intro
  customer  -> Account perks
SAME PERSON, SAME MINUTE, DIFFERENT EMAIL10:0410:0510:0602:00 next dayFLAGMike signs upapp, new profilewelcome emailis_new_customer = true15% off first orderhe has 2 ordersnightly jobflag set to falseFACTuser.signed_uporders_lifetime: 2journey branchesstatus: customeraccount perksno first-order offernothing to fixThe flag was wrong for 16 hours. The email went out in the first minute.
A stored flag is only as current as the last job that wrote it. Facts in the event are current as of the event. Hover or tap each step.

4. Why stored flags go wrong

A flip becomes a send

When a journey enters on a change in an attribute's value, a data bug and a real change look identical. A reverse ETL model joins to the wrong table, 400,000 profiles flip, and 400,000 people get “Welcome to VIP.” An event like loyalty.tier_upgraded is published only when an upgrade actually happens, so a bad sync elsewhere cannot fake it.

WHEN A FLAG FLIPS BY ACCIDENTbad syncvip: false -> true x 400Kentry: attribute changedvip changed to true400,000 emails"Welcome to VIP"entry: loyalty.tier_upgradedan event, from the loyalty service0 sendsguard pauses the syncthe other directiontrue -> falsesilent suppression
A flag used as a trigger turns a data bug into a send. A flag used as a filter does not. Hover or tap each step.

The opposite flip is worse because it is quiet. When a flag accidentally goes from true to false, people stop getting messages they should get, and nothing alerts, because a missing send does not page anyone. Monitor expected volumes, not just errors.

Every flag is a copy of a decision, and somebody has to own it

A flag is cheap to create and expensive to keep. It needs an owner, a definition, a source of truth, a refresh schedule and, eventually, a deletion that nobody schedules. Without that thought up front, you get vip, is_vip, VIP_FLAG_2023 and vip_new on the same profile, each read by a different journey. That is data fragmentation created one reasonable-looking build at a time.

And the smaller cuts

  • Stale by design. A stored flag is as current as the last job that wrote it; journeys act in minutes.
  • No reason. false does not say why, so every exclusion becomes a ticket.
  • Three-valued booleans. Teams treat null as false, as unknown or as never computed, often in the same program.
  • Race conditions. A flag computed before the event lands answers the wrong question at the wrong moment.
  • Cost. In Braze every attribute update consumes data points; recomputing flags nightly for millions of profiles pays for no new information.
  • Sensitive inferences. Flags like likely_pregnant or financial_hardship are easy to create and hard to govern.
  • Hidden logic. The threshold behind a flag is invisible to the marketer using it.

5. When flags are necessary

Some flags are the right answer. Whether a derived flag is worth it depends on two things: what your deployment system can compute at the point of use, and who operates it.

By deployment system

SystemWhat it can evaluateFlags make sense for
SFMC (data-extension based)Columns in a sendable data extension; heavy logic lives in SQL activities anywayDerived eligibility computed in the warehouse and loaded with a contract. Prefer API Event entries carrying facts over journeys that react to data changes.
Braze or another real-time engagement platformProfile attributes, event properties and Liquid at send timeFact and compliance flags. Most derived logic can be facts in the event plus Canvas branching.
Composable: CDP or warehouse plus a delivery ESPEverything upstream; the ESP mostly renders and deliversGoverned computed traits in the CDP, sent as facts or as a final audience. The ESP should hold almost no flags.
Homegrown messagingWhatever your services can look up at request timeVery few. Look facts up when the message is built.

By operating model

Operating modelWhy it changes the answer
Central data team, self-serve marketersA governed enum or flag with an owner is worth it: marketers get one safe field instead of rebuilding logic.
Marketers build logic in the platformFlags multiply fastest here. Prefer facts plus platform segments, and make an admin enforce the checklist below.
Agency or outsourced campaign opsPrecomputed eligibility reduces operator error, because the operator should not rebuild the rules. Every one needs an owner and an expiry.
Regulated (health, finance)Explicit compliance flags are required, and so is a record of who was eligible at send time. Store that as a log, not as a flag that gets overwritten.

Legitimate reasons, then, are: recording a fact the system of record owns, compliance and suppression, platform limits that make point-of-use evaluation impossible, protecting operators from rebuilding logic, and audit. “It was faster to add a column” is not on the list.

6. Before you create a flag

This is the pre-thought, not a build-it mentality. Answer every question, or do not create the flag:

QuestionGood answerRed flag
Is it a fact or a decision?A fact the system of record owns“People who should get the promo”
Who owns it?A named team with an escalation pathWhoever built the campaign
What do true, false and null mean?Written down, including what it does not mean“It's obvious”
Where does it come from, and how fresh is it?One source, a refresh SLA, a timestampSeveral writers, last write wins
Can it trigger anything?No. Filters only.A journey enters when it changes
What happens if it flips by accident?A guard pauses the sync; nothing sendsNobody knows
When is it deleted?An expiry date or a review dateNever
Could it be facts in the event instead?We checked; the platform cannot evaluate itWe did not ask

If it passes, it is a contract, not a flag. Write it down the way MarTech Data Contracts describes.

7. Getting rid of the flags you have

  1. Inventory. Export custom attributes from Braze, or data extension columns from SFMC, and find the booleans: names with is_, has_, flag, eligible, _ind.
  2. Classify. Fact, compliance, governed derived, campaign or manual. Find every journey, segment and template that reads each one.
  3. Remove triggers first. Replace every attribute-change entry with an event entry. This is the change that prevents accidental blasts.
  4. Move facts into events. Add a customer_facts block to the events that start journeys, and branch on it.
  5. Govern what remains. Derived flags you keep get an owner, a definition, reason codes and a refresh SLA.
  6. Delete the rest. Stop writing campaign and manual flags, blocklist them, and count the data points you stop paying for.

8. The takeaway

Profiles did not make flags obsolete. They made stored decisions obsolete. Keep the flags that record facts and the few governed ones your platform genuinely needs. Trigger on events, carry the facts the journey needs in the payload, and decide at the point of use. Then a flag that flips by accident can only filter, never send, and every flag you keep has someone who answers for it.

Related: Marketing Eventing, Launching a Journey in Braze vs SFMC and Object Oriented Marketing.

Sources

Names, IDs and volumes are illustrative.