Skip to content

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.

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.”

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.

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.

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 a track call are stored with that event only. They never change the shopper’s profile, and they go only to your data warehouse.

Add a custom_traits key to a custom event’s data:

Shopify.analytics.publish("quiz_completed", {
score: 8,
custom_traits: { skin_type: "dry" },
});

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.

RuleDetail
Key namesConverted 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.
ValuesText, numbers, true or false, lists, and objects. Lists and objects can be mixed and nested up to 4 levels deep.
TextUp to 512 characters per piece of text, anywhere in a value. Text containing a null character or a broken emoji is rejected.
ListsUp to 50 items, at every level.
Per callUp to 50 traits, up to 200 names counting every nested object, and 8 KB.
Per profileUp 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 storeUp 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.
ClearingSet 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.

An identify call needs analytics consent. Traits on a track call follow the consent of their event. See Consent for the details.

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.

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.

Traits are delivered to your connected data warehouse, if you have one connected:

  • A custom_traits column on the identifies table, the custom_events table, and the per-event tables.
  • A profile_custom_traits table with one row per profile per trait.
  • A profile_custom_trait_keys table 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:

ColumnWhat it holds
idThe row’s unique ID.
shopYour store.
profile_idThe profile the trait belongs to.
key_valueThe trait name.
valueThe trait value, as JSON.
value_typeThe type of the value: string, number, boolean, string_array, array, or object.
sourceWhere the trait came from.
observed_atWhen the value was last observed.
cleared_atWhen the trait was cleared, if it was.
merged_toThe profile this one was merged into, if any.
created_atWhen the row was created.
updated_atWhen the row last changed.
deleted_atWhen 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:

ColumnWhat it holds
idThe row’s unique ID.
shopYour store.
key_valueThe trait name.
value_typesThe types seen for this name, as a JSON list. Empty if the trait has only ever been cleared.
first_seen_atWhen the name was first used. A call that arrives late can move it earlier.
last_seen_atWhen 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_atWhen the row was created.
updated_atWhen 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.