Custom traits
Custom traits are attributes you define yourself that describe who a shopper is: a loyalty tier, a quiz answer, a skin type, a list of customer tags. A trait’s value can be text, a number, true or false, a list, or an object with its own fields. You send them from your storefront, and Máximo delivers them to your connected data warehouse, and to the customer profiles it syncs to Klaviyo and Amplitude, so you can segment your shoppers there.
There are two ways to send a trait:
- On an identify call, the trait updates the shopper’s profile. Use this for things that stay true about a person.
- On a track call, the trait is stored with that one event only. Use this for things that describe a single moment.
Turning it on
Section titled “Turning it on”Custom traits use the same toggle as custom events. Go to Settings → Storefront events → Custom events and turn it on. There is no separate toggle.
Traits roll out to stores separately from custom events, so a store can have custom events without traits. When traits are available for your store, the card shows the line “The same toggle turns on custom traits.”
Traits on identify calls
Section titled “Traits on identify calls”An identify call updates the shopper’s profile. The latest value for a trait wins, and a trait set to null is cleared.
You can call identify as often as you like. If the traits and the customer are the same as the last successful call from that browser, Máximo skips the repeat for the next 24 hours, so calling it on every page view costs nothing. Any change to a trait, including clearing one, is sent right away.
Online Store theme and checkout
Section titled “Online Store theme and checkout”Publish a maximo_identify event with a traits object:
Shopify.analytics.publish("maximo_identify", { traits: { loyalty_tier: "gold" },});A trait can hold more than a single value:
Shopify.analytics.publish("maximo_identify", { traits: { loyalty_tier: "gold", quiz_answers: { skin_type: "dry", concerns: ["acne", "redness"] }, purchase_scores: [4, 5, 3], },});To segment on Shopify customer tags, send them as a list trait, such as customer_tags: ["vip", "wholesale"].
In a checkout or customer account UI extension, use the extension’s own lowercase shopify.analytics.publish with the same arguments.
Hydrogen storefronts
Section titled “Hydrogen storefronts”With the Máximo SDK set up, call identify() from the useMaximo() hook:
import { useMaximo } from "@mthd-studio/hydrogen";
function LoyaltyBadge({ tier }) { const maximo = useMaximo();
useEffect(() => { maximo.identify({ loyaltyTier: tier }); }, [tier]);
// ...}Identify works for logged-out shoppers too. Their traits follow them once they log in.
Traits on track calls
Section titled “Traits on track calls”Traits on a track call are stored with that event only. They never change the shopper’s profile, and they go only to your data warehouse.
Online Store theme
Section titled “Online Store theme”Add a custom_traits key to a custom event’s data:
Shopify.analytics.publish("quiz_completed", { score: 8, custom_traits: { skin_type: "dry" },});Hydrogen storefronts
Section titled “Hydrogen storefronts”Pass traits as the third argument to track(). It works on standard and custom events:
maximo.track("quiz_completed", { score: 8 }, { traits: { skinType: "dry" } });On events forwarded from Hydrogen’s own analytics bus, a custom_traits key in the payload works the same way.
| Rule | Detail |
|---|---|
| Key names | Converted to snake_case, so loyaltyTier becomes loyalty_tier. Two keys that convert to the same name are rejected. Keys inside an object are kept exactly as you wrote them. |
| Values | Text, numbers, true or false, lists, and objects. Lists and objects can be mixed and nested up to 4 levels deep. |
| Text | Up to 512 characters per piece of text, anywhere in a value. Text containing a null character or a broken emoji is rejected. |
| Lists | Up to 50 items, at every level. |
| Per call | Up to 50 traits, up to 200 names counting every nested object, and 8 KB. |
| Per profile | Up to 200 traits. This counts live traits, so a cleared trait stops counting, but a profile can hold at most 400 traits in total, cleared ones included. A new trait past either limit is dropped, while existing traits keep updating. |
| Per store | Up to 500 distinct trait names across all your shoppers. A new name past that is dropped, while existing names keep working. If you reach it, Máximo support can remove names that no profile still holds; they stay in your warehouse. |
| Clearing | Set a trait to null on an identify call to clear it. A null inside a list or object is just a value. |
Máximo records the type of each value (text, number, true or false, list of text, list, or object) and shows it in your warehouse, but it never enforces one. A trait can hold different types over time.
These names are reserved and rejected: email, phone, first_name, last_name, name, city, state, postal_code, zip, country, anything starting with address or hashed, user_id, anonymous_id, shopify_customer_id, consent, and click and cookie IDs such as gclid, fbclid, fbp, fbc, ttclid, ttp, epik, gbraid, wbraid, ga4_client_id, and ga4_session_id. They are rejected inside objects too, so { contact: { email: "..." } } is refused. The names __proto__, constructor, and prototype are rejected at every level.
A call that breaks a rule is handled differently depending on its type. An identify whose traits break a rule is rejected entirely. On a track call, only the traits are dropped, and the event is still recorded. Nothing is ever trimmed to fit.
Consent
Section titled “Consent”An identify call needs analytics consent. Traits on a track call follow the consent of their event. See Consent for the details.
Klaviyo
Section titled “Klaviyo”With Profile sync on (Klaviyo → Profiles), each customer profile Máximo sends to Klaviyo carries that shopper’s custom traits as profile properties, under the same names, so loyalty_tier is loyalty_tier in Klaviyo. Use them in segments and flows like any other profile property. A trait you clear is removed from the profile.
- The Custom traits row under Klaviyo → Profiles → Profile properties turns this on and off. It appears once traits are available for your store, and it starts on unless you had already changed that list.
- Traits sync nightly with the rest of Profile sync. A customer whose traits changed is updated even when nothing else about them did.
- Profile sync covers customers who have placed an order, with the same marketing consent rules as the rest of the profile.
- A trait whose name starts with
maximo_isn’t sent, because those names are Máximo’s own Klaviyo properties.
Amplitude
Section titled “Amplitude”With Identify customers on for Amplitude, the customer identify Máximo sends after each paid order carries that shopper’s custom traits as user properties, under the same names. A trait you clear is removed. A trait that shares its name with a property Máximo sets itself, such as order_count, total_spent or utm_source, isn’t sent.
Your data warehouse
Section titled “Your data warehouse”Traits are delivered to your connected data warehouse, if you have one connected:
- A
custom_traitscolumn on theidentifiestable, thecustom_eventstable, and the per-event tables. - A
profile_custom_traitstable with one row per profile per trait. - A
profile_custom_trait_keystable with one row per trait name your store has used.
Traits are not added to the shared tracks table.
The custom_traits column is restricted, because a trait can hold anything you put in it. If you turn on Restricted mode for a warehouse, the column is cleared there, along with the value column of profile_custom_traits.
The profile_custom_traits columns are:
| Column | What it holds |
|---|---|
id | The row’s unique ID. |
shop | Your store. |
profile_id | The profile the trait belongs to. |
key_value | The trait name. |
value | The trait value, as JSON. |
value_type | The type of the value: string, number, boolean, string_array, array, or object. |
source | Where the trait came from. |
observed_at | When the value was last observed. |
cleared_at | When the trait was cleared, if it was. |
merged_to | The profile this one was merged into, if any. |
created_at | When the row was created. |
updated_at | When the row last changed. |
deleted_at | When the row was removed, if it was. |
The profile_custom_trait_keys table lists the trait names themselves, never a shopper’s value, so it is not restricted. Its columns are:
| Column | What it holds |
|---|---|
id | The row’s unique ID. |
shop | Your store. |
key_value | The trait name. |
value_types | The types seen for this name, as a JSON list. Empty if the trait has only ever been cleared. |
first_seen_at | When the name was first used. A call that arrives late can move it earlier. |
last_seen_at | When it was last used, to within a day. It updates about once a day, so it can lag the latest use by up to a day. |
created_at | When the row was created. |
updated_at | When the row last changed. |
Both profile tables appear in your warehouse only while custom traits are on for your store.
The data-request reports Máximo prepares for a shopper list their trait names and dates.