Custom Properties

Attach persistent traits to a user — plan, company, signup date — that travel across every run and session.

User properties are persistent traits on a user's profile: plan, company, signup date, feature flags. Unlike per-run metadata, they live on the user and apply across every run and event for that person.

Set properties

The shortest path is users.upsert — it creates the user if they don't exist yet and applies the traits in one idempotent call. Safe to run on every request, which makes it the natural fit for a stateless backend.

await trodo.users.upsert('user-42', {
  properties: { plan: 'pro', company: 'Acme' },
});
trodo.upsert_user('user-42', properties={'plan': 'pro', 'company': 'Acme'})

The people.* methods still work and do the same thing to the same profile — use them when you want a single operation like increment or append:

const user = trodo.forUser('user-42');
await user.people.set({ plan: 'pro' });
await user.people.increment('login_count', 1);
user = trodo.for_user('user-42')
user.people.set({'plan': 'pro'})
user.people.increment('login_count', 1)

You don't need to call identify() first. Setting a property on a user who has never had a run or a session creates them. Earlier versions rejected that with IDENTIFY_REQUIRED — a precondition a backend had no way to satisfy.

Attaching traits to a run

wrapAgent and startRun take an optional user bag, so a request handler can attribute a run and enrich the profile in one round trip rather than two calls it has to sequence:

await trodo.wrapAgent('support', fn, {
  distinctId: session.userId,
  user: { properties: { plan: 'pro' } },
});
with trodo.wrap_agent('support', distinct_id=user_id,
                      user={'properties': {'plan': 'pro'}}) as run:
    ...

Applied server-side and non-fatally — a failed trait write never costs you the trace. Use users.upsert when you need to know what landed.

Fixed properties

Everything above lands in the user's custom properties — free-form keys you choose. Separately there are fixed properties: the real profile columns the dashboard offers as built-in filters.

Most of those are browser observations. A backend can set the ones it honestly knows:

Settable from a backendBrowser-only
first_location_country, last_location_countrydevice_type, browser_name, os
first_location_city, last_location_cityfirst_referrer, last_referrer, utm_*
const result = await trodo.users.upsert('user-42', {
  fixedProperties: { last_location_country: 'India' },
});
result.skipped.fixedProperties;   // keys the server declined, with a reason
result = trodo.upsert_user('user-42',
                           fixed_properties={'last_location_country': 'India'})
result['skipped']['fixedProperties']

A browser-only key is not an error — it comes back in skipped as BROWSER_ONLY_PROPERTY and the rest of the batch still applies. The reasoning: a backend supplying a browser name is guessing, and a wrong guess silently corrupts attribution reporting for anyone also running the web SDK.

Where both a browser and a backend write the same column, the browser wins — it observed the value, the server supplied one — and the server write comes back as BROWSER_OWNED_PROPERTY.

Nothing is written implicitly. A backend SDK never infers a user property from a request; country and everything else moves only when you ask for it.

Reserved keys

$name, $email and $phoneno are reserved and rejected. Use unprefixed keys (email, name) instead.

Methods

Method (Node / Python)What it does
people.setSet or overwrite properties
people.setOnce / people.set_onceSet only if not already present
people.incrementAdd to a numeric property
people.appendAppend to a list property
people.unionAdd unique values to a list
people.removeRemove a value from a list
people.unsetDelete properties
people.trackCharge / people.track_chargeRecord revenue on the user

Properties vs. metadata

Lives onScope
User propertyThe userPersistent, across all runs and events
MetadataA run or spanThat single execution

Next

On this page