Customer.io attributes vs events: store changes as events

By·Published·Updated
Customer.io attributes vs events: store changes as events

In 1086 William of Normandy ordered a survey of the England he had conquered (Ariadne). The Domesday Book it produced is now held by the National Archives. For its 900th anniversary in 1986, the BBC asked schools to survey their own areas. The aim was "a database of how Britain looked to the British in 1986" (Ariadne).

The 1086 book survived. Sixteen years after publication, the discs were in doubt: in 2002 concerns emerged that they could become unreadable as the machines able to play them became rare (Wikipedia). At the launch of the Digital Preservation Coalition on 27 February 2002, Loyd Grossman gave them as an example of that risk. The CAMiLEON project demonstrated an emulator at Leeds on 2 December 2002. Adrian Pearce decoded the data structures with a hexadecimal editor, and it took sixteen months to produce a Windows version, which the National Archives installed at Kew (Ariadne).

Two surveys of one country, 900 years apart, are history only because the second did not replace the first. A Customer.io attribute update does exactly that: the new answer writes over the old one. An event is a dated entry that stays on the record.

TL;DR: A Customer.io attribute holds one value, and every update overwrites it. Segments and messages only ever see the current value. An event is a dated record that segments can query at any age, counted over a time window and filtered on its data. So the rule, one field at a time: if a value can change and you will ever ask what it used to be, how often it changed or when, send each change as an event. Then keep an attribute for the current value, because Liquid reads event data only in the automation that event triggered. For most fields that change, the answer is both. An attribute alone is enough for values that never change, values you only need right now, and values whose history already lives in your warehouse.

The client question

A client asked us on a call whether to store each customer's product interest as a profile attribute, updated whenever it changes. That works until someone asks what were they interested in before? or how many people switched this quarter? The attribute cannot answer either.

Our advice was to send each interest as an event, so the history stays on the record. This post's rule adds one step: keep an attribute for the current value as well.

What an attribute stores

Customer.io's docs define attributes as the things you know about your audience, each with a name and a value. Their own examples include product_interests (Customer.io docs, profile attributes). That is a fine attribute, as long as the current list is all you will ever need.

An update replaces the value. The classic JavaScript identify page puts it plainly: "If the attributes already exist on the person, we overwrite them." (identify people).

Where the old value goes

Into an activity log, one profile at a time. The Data Index notes: "We store activities like attribute updates for 30 days." (Data Index). A one-person export includes attribute changes alongside custom events (events). Its attributes.json file "contains all current attribute values" (export a person's data).

That is an audit trail you download per person. It is not something a segment, a trigger or a Liquid tag can read.

Segments have no memory either. People enter a data-driven segment when they match and leave when they stop matching. To act on someone who used to be in a segment, the docs warn, "not in" conditions are not enough (how segments work).

Customer.io no longer tracks when it changed

This is the constraint most teams miss. The legacy segment trigger used matchtime, which looked at when relevant attributes were last updated. The current Attribute or Segment trigger does not. Someone whose plan_type became trial yesterday now waits the full delay, not what is left of it. The docs' fix is a separate timestamp attribute such as trial_start_date, tested in a Wait until block (triggers, filters and frequencies).

If when matters, Customer.io expects you to record it, as a Unix timestamp in seconds. And send created_at only on the first identify call, or you overwrite it (identify people).

What an event keeps

Where an attribute is something you know, "an event represents point-in-time data about something a person did" (Customer.io docs, events). Each event has a name and a data object, and every occurrence is tracked.

Segments can ask three things of that history:

  • Whether and when. Conditions test what people have or have not done, in an optional time frame (events).
  • How often. The getting-started guide describes a segment of people who saw the same offer "five times in the past week" (send events and make segments).
  • With what data. You can refine on event properties, such as items[].type equals monitor for an abandoned cart (events).

How long events last

Profile activity shows only the last 30 days, but older events are not gone. The same page says data is evaluated for segment conditions and triggers "no matter how old it is" (profile activity).

One trap depends on a person's age in Customer.io. A has not performed X within the past 7 days condition only evaluates people who have existed in Customer.io for at least 7 days. New people do not match until then, unless you add an OR condition for them (past X days).

Events trigger on every change

By default a person enters an event-triggered automation every time they perform the event (triggers). An attribute trigger on every re-match is different: the person has to stop matching, then match again. If the condition is that current_interest exists and the value changes, nobody stopped matching, so nothing fires. An event fires on the change itself.

Why events alone fall short

Liquid cannot reach past events. Attributes work in any message, but event keys only work in event-triggered automations. The docs are blunt: "You can only pull in data from the event that triggered your automation" (Liquid). A newsletter that opens with your picks for cold brew needs the interest on the profile.

Event conditions describe history, not the present. A segment of people who performed interest_changed with to equal to espresso includes everyone who ever chose espresso. That includes the person who has since switched to cold brew. To target people whose interest is espresso today, you need the attribute.

The event answers what happened and when. The attribute answers what is true now.

The rule

If a value can change, and you will ever ask what it used to be, how often it changed, or when, send each change as an event. Keep an attribute for the current value.

Three questions settle a field:

  1. Can it change? If not, it is an attribute.
  2. Will anyone ask about the past? Before, how often, since when. If not, it is an attribute.
  3. Do messages outside one event-triggered automation need the current value? If so, it needs an attribute as well as the event.

For a field that changes, the honest default is yes, yes, yes: both. The mistake we see is choosing attributes by default, then asking the second question after months of history have been overwritten.

Attribute, event or both

Field Changes? History questions you will ask Store as
Email, phone, name Rarely Almost never Attribute
created_at Never None Attribute, sent once
Time zone, language Occasionally Almost never Attribute
Product interest or preference Often What before, how many switched, since when Both
Plan or subscription tier Sometimes Upgrades, downgrades, time on each plan Both
Lifecycle stage or loyalty tier Sometimes Who moved up or down, and when Both
Purchases Every order How many, how recent, what was bought Events; totals as attributes
Lifetime value Every order Trend over time Attribute, from purchase events
Logins or sessions Constantly How often in a window Events; last_login attribute if needed
Survey or satisfaction score Each survey Trend, change since last time Both
Cart contents Constantly What was left behind Event, with items in the data
Webhook response for one automation Per run None beyond that run Journey attribute

The pattern is not events for behaviour, attributes for facts. It is events for anything with a past you care about, attributes for the value you message on.

When an attribute is enough

  • Values that never change. created_at is the model: send it once, in Unix seconds.
  • Values you only ever need now. A time zone or language you use to send at the right hour.
  • Values whose history lives elsewhere. If your warehouse keeps every plan change, a reverse ETL sync into Customer.io only needs to send the current plan.
  • Values for one automation only. Journey attributes expire when the person exits the automation (set journey attributes). For a value that only matters inside one workflow, journey attributes beat a profile attribute.

Building both in Customer.io

There are two clean ways to build it.

Send both from your app

Your code sends an identify call with the new value and a track call for the change. This is the industry convention, not a Customer.io quirk. Segment's spec uses Identify to record traits, including when a user updates their info (Segment, Identify spec). Track records actions, named with a noun and a past-tense verb (Segment, Track spec).

Give the change event both values: interest_changed with from and to properties. Then who switched from espresso to cold brew this month is one event condition, refined on both values. If your integration might send an event twice, add an id: Customer.io deduplicates events for 30 days (events).

Send the event, let an automation set the attribute

If your developers can add only one call, send the event. Then build an event-triggered automation with one Create or update person action that sets current_interest from the event attribute (create or update person). Liquid there can also read event_timestamp, the event's Unix time, for an interest_changed_at attribute (Liquid).

One catch. An event does not trigger an automation if its timestamp is more than 72 hours before Customer.io processed it. The exception is an automation that opens with a longer delay (profile activity). Backfilled history lands as events but will not update the attribute, so send current values directly when you backfill.

Inside Customer.io, if an automation changes an attribute whose history matters, put a Send event block beside the update (send event). The same block drives the wider pattern of linking workflows with Send event.

Two workarounds to avoid

An array of past values in one attribute. The Track API caps an attribute value at 1,000 bytes (Track API limits). By our count, an entry such as {"interest":"cold brew","at":1790553600} is 40 bytes. The array holds 24 of them, commas and brackets included, before it passes the cap. That is arithmetic, not a measurement, but the ceiling is real.

A custom object per past value. People and objects count together towards your plan's profile limit (how we bill). A history modelled as objects adds billable profiles that events would not. Keep objects for what they model, as the collections versus custom objects comparison sets out.

Limits and cost

Item Limit
Attribute name 150 bytes
Attribute value 1000 bytes
Unique attributes per person 300
Event name 100 bytes
Event data 100000 bytes
Send event action 100 KB
Journey attributes 100 per journey, 128-byte names, 100 KiB values

Figures from the Track API reference and the Send event and journey attributes pages.

Event data gets a hundred times the room of an attribute value.

On cost, the billing page charges overages on profiles and emails. Profile overages cost $0.009 each on Essentials and $0.004 on Premium and Enterprise; email overages cost $0.12 per 1,000 (how we bill). Events and attributes are not billed items on that page.

Why other tools feel different

Teams coming from product analytics often assume the history is kept for them. In Amplitude it is: each event carries the user property values of its moment, and "older values remain in your historical data" (Amplitude docs). In Customer.io, the change event is how you get that behaviour.

Where this sits in a data plan

Naming and ownership of events across a workspace belong to an event schema for marketing teams. Once the events exist, they are the raw material for advanced segmentation in Customer.io.

For Kip, we merged two workspaces holding 200,000+ profiles and 80+ live campaigns into one, with one naming schema across all existing and new events and people attributes. The new segments included booking frequency and customer lifetime value, the two halves of this post. Frequency is a question for event history. Lifetime value is a current total that belongs on the profile.

William's survey is still in the National Archives, the institution that helped rescue its 1986 successor. Treat your attributes as the latest survey and your events as the record that keeps the earlier ones. Our Customer.io agency team reviews field lists like this. Send us yours and we will mark each field attribute, event or both.

Frequently asked questions

Should I store product interest as an attribute or an event in Customer.io?

Store it as both: an event each time the interest changes, and an attribute holding the current interest. The event keeps history segments can query; the attribute is what Liquid can read in any message (Liquid).

Does updating a Customer.io attribute overwrite the old value?

Yes: if the attribute already exists on the person, Customer.io overwrites it (identify people). The change is logged as activity, which the Data Index stores for 30 days, but segments and messages use the current value (Data Index).

How long does Customer.io keep event data for segments?

Customer.io evaluates event data for segment conditions no matter how old it is, although Profile activity only displays the last 30 days (profile activity).

Can I use event data in a Customer.io newsletter or broadcast?

Not from tracked events: Liquid event keys only work in event-triggered automations, and only for the triggering event (Liquid). An API-triggered broadcast can read values you pass in its trigger data; otherwise, copy the value to an attribute.

Does Customer.io charge for events or attributes?

Not on the published billing page, which charges overages on profiles and emails and does not list events or attributes (how we bill). The related cost is custom objects, which count as profiles.

What are the size limits for Customer.io attributes and events?

On the Track API, an attribute value can be up to 1,000 bytes and an event's data up to 100,000 bytes (Track API limits). Attribute names are capped at 150 bytes, event names at 100 bytes, and a person can hold 300 unique attributes.

How do I keep a Customer.io attribute in sync with the latest event?

Use an event-triggered automation with a Create or update person action that sets the attribute from the event's data (create or update person). Events backfilled more than 72 hours late will not trigger it (profile activity).

Can a Customer.io segment find people whose attribute changed from one value to another?

Not from the attribute, because an update replaces the old value and segments test the current one. Send a change event with from and to properties, and a segment can match people who performed it with a given pair of values (events).

Sources

Want this working in your Customer.io workspace?

It's what we do all day for SMBs—strategy, automations, deliverability and hands-on execution.

See how we work as a Customer.io agency →
David Crowther
Book a free consultation →

On our call, you'll be speaking with David Crowther, founder of NerveCentral.

Our initial consultation is not a sales call—you'll talk, I'll listen and ask questions—then I'll come back to you within 48 hours with our best ideas on how to grow your business.