User attributes
Store and update custom fields on the signed-in user, with a full change history.
User attributes are custom key-value fields on a user account — onboarding state, plan preferences, anything your app needs to remember about a person rather than a workspace.
import { useUserAttributes } from '@buildbase/sdk/react';
function OnboardingGate({ children }: { children: React.ReactNode }) {
const { attributes, loading, updateAttribute } = useUserAttributes();
if (loading) return <Spinner />;
if (attributes?.onboarded) return <>{children}</>;
return <OnboardingFlow onDone={() => updateAttribute('onboarded', true)} />;
}Before you start
Define your attribute keys in the BuildBase console so they are available for audience segmentation and workflow conditions. Writing an undefined key still stores it, but it will not appear as a filterable field.
What the hook returns
| Field | Type | Description |
|---|---|---|
attributes | Record<string, string | number | boolean> | Current values |
loading | boolean | True while the attributes fetch is in flight |
error | string | null | Fetch error, if any |
refetch() | () => Promise<void> | Re-read attributes from the server |
updateAttribute() | (key, value) => Promise<IUser> | Write one attribute |
updateAttributes() | (updates) => Promise<IUser> | Write several at once |
Values are limited to string, number, and boolean. Store an object by
serializing it yourself, and keep in mind that segmentation cannot filter inside
a serialized blob.
Both update methods resolve to the full updated user, not just the attributes, so you can read other profile fields from the same response.
Warning
useUserAttributes() throws if it is called outside the provider tree, with
"must be used within a UserProvider". If you see that error, the component is
mounted above SaaSOSProvider.
Two returned fields are deprecated: isLoading combines the attributes and
features pipelines, so a features fetch flips it; and refreshAttributes is
the old name for refetch. Use loading and refetch.
Writing one value versus several
updateAttribute is a convenience wrapper. Prefer updateAttributes when you
have more than one change — it is a single request, so there is no window where
half the changes are visible:
const { updateAttributes } = useUserAttributes();
await updateAttributes({
onboarded: true,
plan_interest: 'scale',
seats_estimate: 25,
});Change history
Every attribute write is recorded. The history is readable per audience contact
at GET /api/audience/:id/attributes-history, which is what the console renders
on a contact's timeline.
That makes attributes usable as an audit trail — "when did this user actually finish onboarding" is answerable after the fact, not just "are they onboarded now".
Attributes versus feature flags
They look similar and are not interchangeable:
| User attributes | User feature flags | |
|---|---|---|
| Written by | Your app, at runtime | The console |
| Purpose | Remember something about the user | Decide what the user may see |
| Read with | useUserAttributes() | useUserFeatures(), WhenUserFeatureEnabled |
If your app decides the value, it is an attribute. If you want to change behavior without a deploy, it is a flag.
Next Steps
- Audience and lists — segment on these attributes.
- Feature flags — gate features per user.