# Triggers, event data and segments (areas 2 and 3)

Most lifecycle defects start before the message: a browser event standing in for a committed fact, two producers, a retry that becomes a second occurrence, a backfill that enters live journeys, or a field that updates once a night feeding a check that runs every hour. Trace these for every journey with a FAIL or WARN in area 1, and review every segment used by a live journey or recurring campaign.

Platform facts below were read on 2026-11-01 (the Customer.io events page and Braze's /users/track page on 2026-11-02) unless marked "verify live".

## Trace one trigger

For each journey under review, record:

- the producing system and the path into the platform (SDK, CDP, server API, billing webhook, reverse ETL, CSV import)
- the occurrence ID the producer sends, if any
- how the producer records and replays per-object errors in a 2xx batch response: Braze's /users/track, for example, returns success with an errors array for objects it rejected and still processes the rest. A log that only says "batch sent", with no per-record errors, is TD02 WARN: replaying the whole batch duplicates the accepted objects, and the rejected ones show up as JC02 leakage.
- imports or backfills in the last 90 days that touched the trigger event or the fields in its filters
- every field used in entry filters, exits and send-time rechecks, with its writer and update schedule
- event volume by source for the window, and a sample of 20 entrants per FAIL (internal IDs only)

## Rules: triggers and event data

### TD01 authoritative-source

- WARN: a commercial or account journey is triggered client-side (browser or device SDK): checkout click instead of order committed, upgrade click instead of subscription changed, workspace created in the web app only.
- FAIL: the client trigger fires on intent, so failed attempts enter, or JC02 is above 1.10.
- Detect: inventory_audit.py TD01 (commercial and account-state types with trigger_source client or sdk); entries split by source; a JC02 ratio below 0.75 often means API or mobile paths never emit the event.
- Fix direction: trigger from the system that commits the fact. The event contract belongs to tracking-plan-builder.

### TD02 one-producer

- WARN: two paths can send the same event to one platform (SDK and server, CDP route and direct API).
- FAIL: duplicates observed: the same profile and occurrence within 60 seconds.

```sql
-- TD02 with an occurrence ID: copies of one occurrence
select profile_key, event_name, occurrence_id, count(*) as copies
from platform_events
where received_at >= current_date - interval '30 days'
group by 1, 2, 3
having count(*) > 1;

-- TD02 without one: same profile and event within 60 seconds
select a.profile_key, a.event_name, count(*) as near_duplicates
from platform_events a
join platform_events b
  on b.profile_key = a.profile_key
 and b.event_name = a.event_name
 and b.received_at >  a.received_at
 and b.received_at <= a.received_at + interval '60 seconds'
where a.received_at >= current_date - interval '30 days'
group by 1, 2;
```

### TD03 occurrence-identity

- WARN: trigger events carry no occurrence ID where the platform dedupes on one, or a Braze producer keeps no ledger.
- FAIL: retries created duplicate entries in the sample.
- Platform facts:
  - Customer.io dedupes events that carry an `id` for 30 days after receipt. Through Pipelines and the SDKs, `messageId` becomes the event `id`; a `messageId` over 191 characters drops the event, which shows up as JC02 leakage. Through the Track API, `id` must be an uppercase ULID; any other value is replaced and cannot dedupe. A value that repeats across events, such as the person's ID alone, discards every event after the first. CSV imports carry no ID.
  - Klaviyo dedupes on profile, metric and `unique_id`. Without `unique_id` the timestamp truncated to the second is used, so two genuine same-second events collide and one is dropped. A repeated `unique_id` inside one bulk request fails the whole request with a 400.
  - Braze stores every event object as a new event (from braze-implementation-qa). The producer must dedupe on its business key before sending.
  - HubSpot custom events and Iterable events: verify dedupe behaviour live.
- Rule of thumb: a retry reuses the occurrence ID; a new business occurrence gets a new one.

### TD04 import-gate

- WARN: an import or backfill in the last 90 days touched trigger events or filter fields and no gate is documented.
- FAIL: an entry spike on the import date for profiles created more than 30 days earlier.
- Platform facts:
  - Klaviyo: `backfill: true` records an event without triggering metric-based flows. It is set per event, or per entry in event-bulk-create. It does not govern every native connector, CSV import or segment-triggered flow. Klaviyo does not accept @example.com addresses into event or profile processing (custom integration FAQ, read 2026-11-02), so test events on them never arrive.
  - Customer.io: events timestamped within the past 72 hours can trigger automations. Attribute and segment triggers can admit matching historical profiles unless a time condition is added.
  - Braze: backdated timestamps make delays fire early (from braze-implementation-qa).
  - HubSpot and Iterable: verify live how imports interact with workflow and journey entry.

```sql
-- TD04: entries on the import date by profile age
select date(e.entered_at) as entry_date,
       sum(case when p.created_at < e.entered_at - interval '30 days' then 1 else 0 end) as old_profiles,
       count(*) as entries
from journey_entries e
join profiles p on p.profile_key = e.profile_key
where e.journey_id = 'cio-03'
  and e.entered_at >= current_date - interval '90 days'
group by 1
order by 1;
```

### TD05 freshness

- WARN: the update schedule of a field used in an entry filter, exit or recheck is unknown.
- FAIL: the field updates less often than the shortest delay that reads it (a nightly sync feeding a 1-hour recheck).
- Evidence: field, writer, schedule (reverse ETL, CDI, CRM sync), shortest delay that reads it. A recheck on a stale field passes people who already converted.

### TD06 schema-health

- WARN: more than 5% of entrants have a null or unknown value on a property used in filters or personalisation.
- FAIL: above 20%, or a renamed event left a journey empty (see JC03).

### TD07 identity-and-recipient

- WARN: duplicate profiles above 1% of reachable profiles; or an account journey sent to the person who acted instead of the recipient role (billing admin, workspace owner).
- FAIL: any duplicate cluster with mixed subscription states (one profile unsubscribed, another subscribed). Also report it as PC09.
- Platform facts:
  - Iterable: the project type decides whether two users can share an email. Hybrid projects enforce unique email and userId; userId-based projects allow shared emails; the type is chosen at project creation and cannot simply be changed later (Iterable project types article; verify live).
  - Braze: when profiles share an email address and one is in the Global Control Group, the email is still sent when the other profile is eligible (read 2026-11-01).

```sql
-- TD07: duplicate share and mixed-consent clusters. Hash the address inside the warehouse; never export it.
with clusters as (
  select email_hash,
         count(*) as profiles,
         count(distinct marketing_email_state) as states
  from crm_contacts
  where reachable = true
  group by 1
)
select sum(case when profiles > 1 then profiles - 1 else 0 end) * 1.0 / sum(profiles) as duplicate_share,
       sum(case when profiles > 1 and states > 1 then 1 else 0 end) as mixed_consent_clusters
from clusters;
```

### TD08 timezone-coverage

- WARN: more than 10% of reachable profiles have no timezone while quiet hours or local send times are in use. Those profiles fall back to the account or company timezone, so "quiet hours" run on head-office time.

## Klaviyo flow facts that change findings

- Metric trigger filters are checked only at entry. Profile filters are checked at entry and before every action.
- A failed profile filter skips that action and the person can continue to later actions (the flow report shows a skip reason; verify the label live).
- A flow's trigger cannot be changed after it is set; the fix is a copied flow, which resets history.
- Opens and clicks cannot trigger flows. Only top-level event properties work as trigger filters.

## Rules: segments

### SG01 segment-definitions

- WARN: a segment used by a live journey or recurring campaign has no written definition.
- FAIL: a rule references a field with no values written in 30 days.

### SG02 engagement-definition

- WARN: "engaged" or "active" is defined by email opens alone.
- Why: Apple Mail Privacy Protection downloads remote content in the background, so opens fire without reading. Braze reports Machine Opens and Estimated Real Opens. Klaviyo warns that opens can be recorded without reading, and its click tracking can be switched off per account, recipient or link, so a missing click is not proof of disinterest. HubSpot's marketing email bot filtering is on by default and configurable per account; record its state for every send compared.
- Better signals: clicks with known tracking coverage, replies, product sessions, purchases.

### SG03 sunset-rule

- WARN: bulk marketing email has no sunset or re-permission rule, or more than 20% of marketing sends in 30 days go to recipients with no click, reply, session or purchase in 180 days (an audit default, not a vendor threshold).

```sql
-- SG03: share of marketing sends to recipients with no positive engagement in the 180 days before the send.
-- A left join, not a correlated EXISTS in the select list, which Snowflake supports only in WHERE clauses.
-- send_id: any unique key per send row.
with per_send as (
  select s.send_id,
         max(case when e.profile_key is not null then 1 else 0 end) as engaged
  from sends s
  left join engagement_events e
    on e.profile_key = s.profile_key
   and e.event_type in ('click', 'reply', 'session', 'purchase')   -- never opens alone
   and e.event_at <  s.sent_at
   and e.event_at >= s.sent_at - interval '180 days'
  where s.message_class = 'marketing'
    and s.sent_at >= current_date - interval '30 days'
  group by s.send_id
)
select avg(1.0 - engaged) as share_to_unengaged
from per_send;
```

### SG04 internal-and-test-exclusion

- WARN: employees, seed mailboxes and test profiles are not flagged at creation and excluded from live marketing and from metrics.

### SG05 size-sanity

- WARN: a segment's count differs from the source population it claims to represent by more than 10%.
- FAIL: by more than 25% for a segment that drives a revenue or service journey.

### SG06 random-bucket-overlap

- WARN: test splits built on random buckets or hash ranges can overlap a global holdout. Braze documents that random bucket segments can overlap its Global Control Group. Check every split against the holdout definition.

## Evidence to keep

- Event volume by source for each traced trigger.
- The 20-entrant sample per FAIL, internal IDs only.
- The import log for 90 days and the gate setting used.
- Each field's sync schedule next to the shortest delay that reads it.
- Segment definitions as quoted from the platform, with counts and the source population query.
