Attributes
Attributes are a customer-declared semantic layer on top of your event stream. You declare a typed schema once in the dashboard - persona: single_choice of {teacher, student}, goal: free_text, topics: multi_choice… - and then values land on each person automatically, either auto-promoted from capture() events or set explicitly via the SDK.
1. What are attributes?
Every person in APL Analytics has three orthogonal layers of data. Understanding where attributes fit helps you decide when to reach for them.
| Primitive | Defined by | Lifecycle | Used for |
|---|---|---|---|
Events (capture) | Developer in code | Append-only timeline | Funnel, retention, raw history |
Person properties (identify with $set) | Developer in code | Mutable state | Account state (plan, email, signup_source) |
| Attributes | Customer in dashboard + auto-promoted from events | Mutable state, last-write-wins | Semantic profile: who the user is, what they want |
The key distinction: person properties are your open-ended developer state bag, written in code. Attributes are the semantic schema your product team declares in the dashboard - with a declared type, optional choices, and optional LLM hints. They travel a separate, schema-validated path and surface as first-class filter chips in funnels and the people list.
2. When to use attributes vs identify $set
Both write mutable state onto a person profile, but they serve different owners and purposes:
$setfor: account state the developer controls -plan,email,signup_source,is_paying. Free-form key/value, no declared schema, no validation.- Attributes for: semantic profile facts the product team cares about -
persona,goal,biggest_problem,topics. They have a declared schema (type, options, LLM hint, sensitivity flag) and are first-class citizens in the analytics UI.
$setis the developer’s open-ended state bag. Attributes are the customer’s declared semantic schema for the AI / analytics layer.3. Declaring an attribute
Attributes are declared in the dashboard before any values can be written. The SDK will reject writes to undeclared keys with attribute_not_found.
- Go to Attributes in the sidebar (under your app).
- Click + New attribute.
- Fill in
key(snake_case, e.g.persona) anddisplayName(e.g. “Persona”). The key is permanent - see the FAQ before you name it. - Pick a type:
single_choice,multi_choice,free_text,number, orboolean. - Fill in type-specific config - for
single_choice/multi_choiceprovide the allowed options; forfree_textset amaxLength; fornumberoptionally setmin/max. - Optionally toggle Use in LLM on and write an LLM hint - e.g. “User identifies as a {value}. Tailor accordingly.”
- Save.
createAttribute() for the programmatic route.4. Setting values - three paths
Every attribute can be populated in two independent ways: automatically from a captured event, and explicitly via an SDK call. They’re not a choice - use one, the other, or both at the same time. All paths write through the same schema-validation layer, so invalid values (wrong type, option not in list) are rejected identically regardless of source.
Path 1: Auto-promotion from events (recommended)
When you create an attribute, the form has a “Where does this come from?” section with a single toggle - Auto-populate from a captured event, on by default. Turn it on to wire the attribute to a capture() call in one step - no separate source-setup screen. Your application code stays focused on emitting events; the product team configures which events carry which meaning in the same form that defines the attribute.
In the New Attribute form, after picking the type, fill in:
- Event name:
onboarding_step_answered. - Filters (optional):
stepeqpersona. - Extract value from property:
value.
Save the form once - the attribute definition and its source are created together. From then on, this capture automatically sets persona on the person:
await vevee.analytics.capture({
distinctId: user.id,
event: 'onboarding_step_answered',
properties: { step: 'persona', value: 'teacher' },
});
// Server-side: persona = 'teacher' lands on this person.
// No extra SDK call needed.Path 2: Direct write via the SDK (always available)
setAttribute()works on every attribute, regardless of whether the auto-promotion toggle is on. Use it for cases the auto path can’t cover - backfilling from another system, deriving from a third-party webhook, or setting a value at signup before any analytics event has fired. It also pairs naturally with auto-promotion: configure the event source for the steady-state path, then call setAttribute() on the rare occasion you need to override a value or seed one from out-of-band data.
await vevee.analytics.setAttribute({
distinctId: user.id,
attribute: 'persona',
value: 'teacher',
});Requires a sk_live_* (or sk_test_*) secret key unless the workspace has enabled client-side attribute writes (see FAQ). If you don’t want auto-promotion at all, just leave the toggle off when creating the attribute - the SDK path will still work.
Path 3: Bulk import
Set multiple attributes in one call - useful for an onboarding summary write or a backfill job.
const result = await vevee.analytics.setAttributes({
distinctId: user.id,
attributes: {
persona: 'teacher',
goal: 'lesson_planning',
biggest_problem: 'spending hours on lesson prep',
},
});
// result: { applied: 2, rejected: [{ key: 'persona', reason: '...' }] }
// Invalid values are reported per-row, never fatal.The response includes an applied count and a rejected array. Invalid values (wrong type, unknown key, option not in list) are reported per-row and never cause the whole call to fail. Check rejected in development to catch schema mismatches early.
5. Filtering by attribute
Attributes surface as first-class filters in three places:
Funnel cohort filter
On any funnel, add a Cohort filterentry - e.g. “persona is teacher” - and the funnel counts only persons who match all filters. Multiple filters AND together. The same filter is available in the funnel API:
{
"name": "Onboarding completion - teachers only",
"personFilters": [
{ "attribute": "persona", "op": "eq", "value": "teacher" }
],
"steps": ["signed_up", "onboarding_step_answered"]
}Supported operators: eq, neq, in. For multi_choice attributes use in with an array value to match any person who has at least one of the listed values.
People list filter chips
On Apps → People, click + Filter to add chip-based filters by attribute value. Chips AND together; each chip shows a dropdown of the declared options for single_choice / multi_choice attributes, and a free-text input for free_text, number, and boolean types.
LLM compose (preview)
Attributes with useInLlm: true are automatically injected into future composeContent() calls using the configured LLM hint as a prompt fragment. Declare them now with useInLlm: true and configure the hint - the surface ships in a later release. See the FAQ for current status.
6. Privacy
Attributes are first-class citizens in APL’s GDPR alignment:
- Art. 15 / 20 (Right to access / portability).
exportPerson()includes anattributesblock alongside events and the consent audit log. All declared attribute values for the person are exported as a key/value map. - Art. 17 (Right to erasure).
deletePerson()deletes attribute values immediately and redacts the audit-log entries to[REDACTED]- the row remains as legal evidence of the write but contains no PII. - Art. 21 (Right to object). Opted-out persons receive no further attribute writes. Auto-promotion from events is silently skipped for opted-out persons; the event capture itself still succeeds.
isSensitive: trueflag. When a declaration is marked sensitive, the audit-log entry stores a SHA-256 hash of the value rather than the raw value. The attribute works normally everywhere else - queries, filters, and exports use the real value. Only the audit trail is hashed.
See the Privacy & GDPR guide for how to wire exportPerson, deletePerson, and optOutinto your app’s settings pages.
7. FAQ
Can I rename a key after creation?
No. Keys are permanent identifiers - existing stored values reference the key by its original string. Archive the attribute and create a new one with the new key. The archived attribute’s values stay in the DB but are excluded from getAttributes(), exportPerson(), and all dashboard pickers.
Can I change a type?
No - same answer. Changing a type from single_choice to free_text would silently invalidate every stored value. Archive and recreate.
What happens to historical values when I archive an attribute?
Values remain in the database. They are excluded from getAttributes(), exportPerson(), funnel cohort filters, and the dashboard pickers, so they stop affecting anything in practice. Audit-log rows persist. You can unarchive at any time to restore full visibility.
Will a browser-side setAttribute call work?
Only when the workspace has enabled Client-side attribute writes in Settings → Security. The default is OFF. From a backend you can always write attributes with a secret key (sk_live_* / sk_test_*). Enabling the client-side flag accepts that end-users can write arbitrary attribute values for themselves - appropriate for self-reported data, not trusted for access-control decisions.
What happens if a source pattern no longer matches?
Auto-promotion is best-effort. If an event arrives that matches no source rule, nothing happens - the event is captured normally and no attribute write occurs. There is no error and no indication to the SDK caller. Review your source rules in the dashboard if promotion stops working after a refactor.
Is auto-promotion atomic with the event insert?
No - they are sequential. The event insert commits first; promotion runs immediately after in the same server-side handler. If promotion fails (validation error, DB error), the event capture still succeeds and the promotion failure is logged server-side. The SDK caller sees a normal success response either way.
Does the LLM compose surface exist yet?
Not in this release - it is declared as Preview. The useInLlm flag and hint text are stored and validated now so you can populate them ahead of launch. They take effect once the composeContent() surface ships in a later release.
Related
setAttribute()- write a single attribute value.setAttributes()- bulk write with per-row rejection reporting.getAttributes()- read the current attribute values for a person.createAttribute()- programmatically declare an attribute schema.- Behavioral analytics & funnels - how
capture(), funnels, and cohort filters work end to end. - Privacy & GDPR - erasure, export, and opt-out integration.