New: no-code event targeting is live. See how →
Onbixo

User traits for onboarding targeting: what to send

Which user traits to send an onboarding tool, and when to call identify. Skip what the runtime already knows, keep types consistent, fix the ordering.

Most teams send their onboarding tool far too much. They pass the whole user object into identify() and hope the targeting rules sort it out.

Then the rule never matches, and nobody can say why.

A trait is not a copy of your user record. It is an input to a rule you have already decided to write. If no rule reads it, it is noise. Your onboarding tool does not need their avatar URL.

Write the rule first, then send the trait

Most installs go the other way. Someone drops in the snippet, dumps twenty fields into identify(), and the rules get written weeks later by someone else.

Try the other order.

Open the targeting screen and write the condition you actually want. Something like: on the billing page, plan is free, setup is not finished. Now you know you need exactly two traits, plan and setupComplete. That is the whole list for that flow.

In practice four or five traits cover most of a product's flows. If yours needs twenty, the rules are probably describing personas instead of moments. We wrote about that difference here.

Do not send what the runtime already knows

This is where the bloat comes from.

Onbixo targeting already has first and last seen, new versus returning visitor, session count, page URL, device, browser, country and locale. It also knows whether the user has seen or completed another flow.

So lastLogin is redundant. So is a hand-rolled isNewUser boolean, which goes stale the first time somebody comes back on Tuesday.

signupDate is the exception, and it is the one trait the runtime reads by name. It is what powers days since signup, so you set it by calling identify. Send it as an ISO date like 2026-01-04 or a millisecond timestamp. Anything the runtime cannot parse is ignored without an error, so Unix seconds or an ambiguous 04/01/2026 will quietly target the wrong window.

Leave it out and the runtime falls back to the first time that browser loaded a flow. That reads a brand-new signup correctly. But it is per-browser, so the same person on a second device looks new again, and anyone who signed up before you installed the snippet looks new on their next visit. The docs cover it under the one reserved trait.

Send the things only your database knows:

  • Signup date, as a real date
  • Plan name, and seat count if you gate on it
  • Role, if admins and members need different flows
  • Setup state: is the integration connected, does a first project exist

That is it, usually. Everything else is already in the runtime or is not driving a decision.

Call identify early, but never wait on it

The snippet queues. Calls you make before the runtime finishes loading are queued and run in order once it arrives, so pushing identify() high in your app is safe.

What is not safe is waiting for a slow /me request before you call it at all.

If identify() lands after the first flow evaluation, your trait rules have nothing to match against. The flow does not fire. The user sees nothing, reloads out of habit, and then it works. That is the "it only shows after a refresh" bug, and the cause is almost always ordering, not the rule.

Two things fix it:

  • Push identify() in the same tick you get the user, not in an effect three components down the tree
  • When a trait changes mid-session, call identify() again with the new value

That second one gets skipped a lot. A user upgrades, finishes setup, or invites a teammate, and the traits in the runtime are still from login. The runtime API also exposes loadFlows(), which fetches and evaluates published flows. That is the hook you want after a trait changes and you need the new rule to take effect in the same session.

Keep the types boring

One line in the targeting docs is worth reading twice. Numeric operators coerce values to numbers, and a non-numeric or missing value does not block the flow.

So a seats > 5 rule does not fail closed when seats never arrived. It just stops being the thing keeping the flow out. If you have ever seen a flow reach an audience it should not have, check whether the trait was there at all.

Fix it at the source:

  • Send numbers as numbers. seats: 12, not "12"
  • Send booleans as booleans. Not "true", not "yes", not 1
  • Pick one naming case and keep it. signupSource everywhere, or signup_source everywhere, never both

There is no naming convention that every tool agrees on. Segment's identify spec reserves a set of names; Onbixo does not force one on you. So the convention has to live in one place in your own code. If two files build the traits object, they will drift, and the drift shows up as a rule that quietly matches nobody.

User traits or company traits?

The identify call takes both. Traits second, company third.

onbixo.push(["identify", "user-123", {
  role: "admin",
  setupComplete: false
}, {
  companyId: "acme",
  plan: "pro",
  seats: 40
}]);

The split matters for one reason: which one changes when the person changes.

Plan, seat count and billing status belong to the company. Two admins at the same account should see the same billing flow. Role and personal setup state belong to the user, because the second admin to join has not done the setup the first one did.

Get this backwards and you write a rule that reads correctly and targets the wrong half of your accounts.

How to check it actually worked

Before you blame the selector or the rule, do this in order.

  1. Log in as a test user and confirm in the console that identify() ran before the flow evaluated. Ordering first, always.
  2. Check the trait value the runtime received, not the value in your database. Those are different questions.
  3. Strip the rule down to that single trait and publish it to a test audience. If it fires, add the other conditions back one at a time.
  4. Use reset(flowId) to clear local flow state so you can watch the same flow fire again without a new account.

Most "the flow is not showing" reports end at step one.

The move

Open your targeting screen. Write down every trait your published rules actually reference. That list is your identify() call. Delete the rest, then move the call up to the moment you know who the user is.

You can try it free on a single flow and watch the rule match.

Liked this? Get the next one by email

New posts on activation, pricing, and the product - roughly twice a month. No spam, unsubscribe anytime.

← All articles

Put this into practice.

Build a checklist, a tour, or an announcement in minutes - free to start, live in one line, no sales call.

Every plan includes OnbiAI - draft flows, targeting and audiences with AI