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 backend | Browser-only |
|---|---|
first_location_country, last_location_country | device_type, browser_name, os |
first_location_city, last_location_city | first_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 reasonresult = 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.set | Set or overwrite properties |
people.setOnce / people.set_once | Set only if not already present |
people.increment | Add to a numeric property |
people.append | Append to a list property |
people.union | Add unique values to a list |
people.remove | Remove a value from a list |
people.unset | Delete properties |
people.trackCharge / people.track_charge | Record revenue on the user |
Properties vs. metadata
| Lives on | Scope | |
|---|---|---|
| User property | The user | Persistent, across all runs and events |
| Metadata | A run or span | That single execution |
Next
- Metadata for per-run and per-span tags
- Identification to bind runs to a user