Overview
Computed Traits allow you to create new, derived user attributes by transforming or combining existing profile data. Instead of relying solely on raw collected data, you can define logic that automatically calculates new values, such as combining a first name and last name into a full name, converting text to uppercase, or applying conditional logic.
Computed Traits in Piano Audience support two modes:
-
Basic Mode: A visual, no-code UI that lets you create computed traits with a few clicks by selecting functions and arguments from dropdowns. See Computed Traits: Basic Mode for details.
-
Advanced Mode: A code-based editor using JavaScript for maximum flexibility and power.
This article focuses on Advanced Mode.
Getting Started
Switching to Advanced Mode
To use Advanced Mode, click the code editor icon (</>) in the top-right corner of the editor. This replaces the visual function builder with a JavaScript code editor.
Switching between Basic Mode and Advanced Mode will discard all values you have entered, including logic in the function builder as well as any fields on the right-hand panel (Name, Type, Triggers, Display Name, Description). It is strongly recommended to switch to Advanced Mode before entering any values.
Mandatory Fields
Once you have switched to Advanced Mode, you must provide the following mandatory fields on the right-hand panel:
|
Field |
Required |
Description |
|---|---|---|
|
Name |
Yes |
A unique identifier for the computed trait (e.g., |
|
Type |
Yes |
The data type of the resulting trait (e.g., Boolean, String, Float). See Supported Data Types below. |
|
Triggers |
Yes |
One or more profile attributes that trigger the execution of the computed trait whenever they are updated. See Triggers below. |
You may also optionally fill in:
-
Display Name: A human-readable label for the trait (e.g., "CT Advanced"). Max 255 characters.
-
Description: A brief explanation of what the computed trait does (e.g., "Classifies a user as a whale if their total spend exceeds $500."). Max 400 characters.
The following fields are automatically set and cannot be modified:
-
Category: Always set to
Computed. -
Source: Always set to
Audience API.
Triggers
In Advanced Mode, you must define one or more Triggers. A trigger is a profile attribute that, when updated (e.g., via an API call), causes the computed trait to be recalculated.
Unlike Basic Mode, where triggers are determined automatically based on the fields selected in the visual function builder, Advanced Mode requires you to explicitly specify which attributes should trigger execution. This is because the system cannot automatically infer dependencies from free-form JavaScript code.
How to add triggers:
-
Locate the Triggers field (marked with an asterisk *) on the right-hand panel, below the Type selector.
-
Click the + button to add a trigger attribute.
-
Select the profile attribute from the dropdown (e.g.,
Profile:vx/total_spent:FACT:STRING). -
Repeat to add additional trigger attributes if your logic depends on multiple fields.
Each trigger appears as a tag with an × button to remove it. You can also click the ... menu for additional options.
If you reference a profile attribute in your JavaScript logic but do not add it as a trigger, the computed trait will not recalculate when that attribute changes. Always ensure that every attribute your logic depends on is included as a trigger.
Supported Data Types
The following data types are available when creating a Computed Trait:
|
Type |
Description |
Example |
|---|---|---|
|
INT |
Integers. Can safely store integers in the range -(2⁵³ − 1) to 2⁵³ − 1. |
|
|
FLOAT |
Double-precision 64-bit IEEE 754 floating point numbers. |
|
|
BOOLEAN |
Boolean values. |
|
|
DATETIME |
Date-time with millisecond resolution. Must use ISO 8601 format. |
|
|
STRING |
Default strings (arbitrary), optimized for wildcard searches. |
|
|
KW |
Keywords, strings up to 255 characters, optimized for exact match searches. |
|
|
TEXT |
Arbitrary strings automatically tokenized upon indexing, suitable for long texts. |
|
The OBJECT and MIXED data types are not supported for Computed Traits.
When Are Computed Traits Evaluated?
Computed Traits are evaluated in real time as soon as an update to any of the trigger attributes is recognized. For example, when an API call updates a user's total spend, any computed trait that lists that field as a trigger is automatically recalculated.
There is no manual trigger or scheduled batch process required. As long as the trigger attributes are being updated through supported channels (such as the Audience API), your computed traits will stay current.
Example API call that triggers recalculation:
PUT /api/v1/profiles/{profileId}
Content-Type: application/json
{
"vx/total_spent": "1250.00"
}
When this API call is processed, any computed trait that has Profile:vx/total_spent:FACT:STRING listed as a trigger will be automatically recalculated with the new value.
Working with the Profile Object
In Advanced Mode, your JavaScript logic receives parameters via the params object. You destructure this to access the target (the user profile) and utils (utility functions):
const { target, utils } = params;
You can then access profile attributes through target.props using the full attribute path:
// Reading profile attributes const totalSpent = target.props['Profile:vx/total_spent:FACT:STRING']; const firstName = target.props['Profile:firstName:FACT:STRING']; const lastName = target.props['Profile:lastName:FACT:STRING'];
The utils object provides helper functions, such as utils.moment() for date/time operations:
// Getting the current timestamp in ISO format const now = utils.moment().toISOString();
Return Structure
Your JavaScript logic must return an object with a value property containing the computed result. You can optionally include a timestamp to record when the computation occurred:
return {
value: computedResult,
timestamp: utils.moment().toISOString()
};
If a referenced attribute has not been set or does not exist on the profile, target.props returns undefined. Always consider adding fallback defaults in your logic to handle missing data gracefully (e.g., using || '0' or || 0).
Logic Categories
When creating a Computed Trait, you will see three logic category tabs on the left-hand side of the editor: Processing, Init, and Merge. Each category determines when and how the computed trait logic is executed. In Advanced Mode, each tab provides its own dedicated JavaScript code editor, labeled "Executors for processing," "Executors for init," and "Executors for merge" respectively.
Processing
Processing logic runs automatically whenever an existing user profile is updated and one of the defined trigger attributes has changed. It recalculates the computed trait based on the updated attribute values. This is the most commonly used category and ensures your computed traits stay current as user data evolves.
Init
Init logic executes only when a new user profile is created. It sets the initial value of the computed trait using whatever attributes are available at the time of profile creation. Use this category when you need to establish a baseline value for new users.
Merge
Merge logic triggers when two user profiles are combined into a single profile. This is common in identity resolution scenarios. The merge logic aggregates or recalculates the computed trait to accurately reflect the consolidated data from both profiles.
You can write independent JavaScript logic for each category. For example, your Processing logic might perform a complex multi-step calculation, while your Merge logic handles conflict resolution between two profiles.
Creating a Computed Trait in Advanced Mode
Step 1: Switch to Advanced Mode
After switching to Advanced Mode and entering the required Name, Type, and Triggers, navigate to the logic category tab you want to configure.
Step 2: Write Your JavaScript Logic
In the code editor, write JavaScript that returns an object with the computed value. You access profile attributes through target.props and can implement any transformation logic, including:
-
Conditional branching (if/else, switch statements)
-
String manipulation and formatting
-
Mathematical calculations and aggregations
-
Multi-field logic with complex dependencies
-
Date parsing and formatting using
utils.moment()
The following section provides examples organized by output data type, mirroring the built-in functions available in Basic Mode.
Examples by Data Type
The following examples are organized by output data type, mirroring the built-in functions available in Basic Mode. Each example includes the JavaScript logic, the required trigger attributes, and where applicable, a sample API call that would trigger the recalculation.
String Examples
These examples correspond to the string functions available in Basic Mode: concat, substring, toLowerCase, toUpperCase, and toString.
Concatenating First and Last Name (concat)
The concat function in Basic Mode joins two or more strings (e.g., firstName + " " + lastName → "John Doe"). Here is the equivalent in Advanced Mode:
const { target, utils } = params;
const firstName = target.props['Profile:firstName:FACT:STRING'];
const lastName = target.props['Profile:lastName:FACT:STRING'];
let fullName = null;
if (firstName && lastName) {
fullName = firstName + " " + lastName;
} else if (firstName) {
fullName = firstName;
} else if (lastName) {
fullName = lastName;
}
return {
value: fullName,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:firstName:FACT:STRING, Profile:lastName:FACT:STRING
API call that would trigger this recalculation:
PUT /api/v1/profiles/{profileId}
Content-Type: application/json
{
"firstName": "John",
"lastName": "Johnson"
}
Result: computed trait recalculates to "John Johnson"
Extracting a Domain from an Email (substring)
The substring function in Basic Mode extracts part of a string using start index and optional length/end index. In Advanced Mode:
const { target, utils } = params;
const email = target.props['Profile:email:FACT:STRING'];
let domain = null;
if (email && email.indexOf("@") !== -1) {
domain = email.substring(email.indexOf("@") + 1);
}
return {
value: domain,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:email:FACT:STRING
Standardizing a String to Lowercase (toLowerCase)
The toLowerCase function in Basic Mode converts the string to lowercase. In Advanced Mode:
const { target, utils } = params;
const email = target.props['Profile:email:FACT:STRING'];
return {
value: email ? email.toLowerCase() : null,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:email:FACT:STRING
Integer / Float Examples
These examples correspond to the arithmetic functions available in Basic Mode: + (add), - (subtract), * (multiply), and / (divide).
Calculating Average Order Value (divide)
The / (divide) function in Basic Mode divides left by right. Division by zero returns null. In Advanced Mode:
const { target, utils } = params;
const totalRevenueStr = target.props['Profile:totalRevenue:FACT:STRING'] || '0';
const orderCountStr = target.props['Profile:orderCount:FACT:STRING'] || '0';
const totalRevenue = parseFloat(totalRevenueStr) || 0;
const orderCount = parseInt(orderCountStr) || 0;
return {
value: orderCount > 0 ? totalRevenue / orderCount : null,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:totalRevenue:FACT:STRING, Profile:orderCount:FACT:STRING
API call that would trigger this recalculation:
PUT /api/v1/profiles/{profileId}
Content-Type: application/json
{
"totalRevenue": "2500.00",
"orderCount": "20"
}
Result: average order value recalculates to 125.00
Calculating Lifetime Value with a Multiplier (multiply + add)
Combines the + (add) and * (multiply) functions from Basic Mode. The + function adds two or more numbers, and the * function multiplies numbers. In Advanced Mode:
const { target, utils } = params;
const totalSpendStr = target.props['Profile:totalSpend:FACT:STRING'] || '0';
const monthlyAvgStr = target.props['Profile:monthlyAvgSpend:FACT:STRING'] || '0';
const monthsRemainingStr = target.props['Profile:contractMonthsRemaining:FACT:STRING'] || '0';
const totalSpend = parseFloat(totalSpendStr) || 0;
const monthlyAvg = parseFloat(monthlyAvgStr) || 0;
const monthsRemaining = parseInt(monthsRemainingStr) || 0;
return {
value: totalSpend + (monthlyAvg * monthsRemaining),
timestamp: utils.moment().toISOString()
};
Triggers: Profile:totalSpend:FACT:STRING, Profile:monthlyAvgSpend:FACT:STRING, Profile:contractMonthsRemaining:FACT:STRING
Boolean Examples
These examples correspond to the boolean functions available in Basic Mode: isEqual, isGreater, isGreaterOrEqual, and, or, not, and ternaryOperator.
Classifying a User as a "Whale" (isGreaterOrEqual)
The isGreaterOrEqual function in Basic Mode compares two values and returns true if left ≥ right. This example is taken directly from the product interface:
const { target, utils } = params;
const totalSpentStr = target.props['Profile:vx/total_spent:FACT:STRING'] || '0';
const totalSpent = parseFloat(totalSpentStr) || 0;
const whaleThreshold = 500;
const isWhale = totalSpent >= whaleThreshold;
return {
value: isWhale,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:vx/total_spent:FACT:STRING
API call that would trigger this recalculation:
PUT /api/v1/profiles/{profileId}
Content-Type: application/json
{
"vx/total_spent": "750.00"
}
Result: isWhale recalculates to true (750 >= 500)
Checking if a User is a High Spender (isGreater + and)
The isGreater function returns true if left > right, and the and function returns true only if all conditions are true. In Advanced Mode:
const { target, utils } = params;
const totalSpendStr = target.props['Profile:totalSpend:FACT:STRING'] || '0';
const orderCountStr = target.props['Profile:orderCount:FACT:STRING'] || '0';
const totalSpend = parseFloat(totalSpendStr) || 0;
const orderCount = parseInt(orderCountStr) || 0;
return {
value: (totalSpend > 500 && orderCount > 5),
timestamp: utils.moment().toISOString()
};
Triggers: Profile:totalSpend:FACT:STRING, Profile:orderCount:FACT:STRING
Inverting a Boolean Flag (not)
The not function in Basic Mode inverts a boolean value (true → false, false → true). In Advanced Mode:
const { target, utils } = params;
const isOptedOut = target.props['Profile:emailOptOut:FACT:BOOLEAN'] || false;
return {
value: !isOptedOut,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:emailOptOut:FACT:BOOLEAN
Date / Time Examples
This example corresponds to the getISODate function available in Basic Mode, which converts a date/time value to ISO 8601 format for standardization.
Normalizing a Date Field to ISO 8601 (getISODate)
const { target, utils } = params;
const rawDate = target.props['Profile:lastPurchaseDate:FACT:STRING'];
let isoDate = null;
if (rawDate) {
isoDate = utils.moment(rawDate).toISOString();
}
return {
value: isoDate,
timestamp: utils.moment().toISOString()
};
Triggers: Profile:lastPurchaseDate:FACT:STRING
Conditional User Classification (Advanced)
This example demonstrates a use case that is difficult to achieve in Basic Mode: classifying users into tiers based on multiple conditions, with direct reference to the profile data that would be set via API.
Multi-Tier User Classification
Configuration:
-
Name:
user_tier -
Type:
String -
Triggers:
Profile:totalSpend:FACT:STRING,Profile:orderCount:FACT:STRING,Profile:isActiveSubscriber:FACT:BOOLEAN
const { target, utils } = params;
const totalSpendStr = target.props['Profile:totalSpend:FACT:STRING'] || '0';
const orderCountStr = target.props['Profile:orderCount:FACT:STRING'] || '0';
const isSubscriber = target.props['Profile:isActiveSubscriber:FACT:BOOLEAN'] || false;
const totalSpend = parseFloat(totalSpendStr) || 0;
const orderCount = parseInt(orderCountStr) || 0;
let tier = "bronze";
if (totalSpend > 1000 && orderCount > 10 && isSubscriber) {
tier = "platinum";
} else if (totalSpend > 500 && orderCount > 5) {
tier = "gold";
} else if (totalSpend > 100) {
tier = "silver";
}
return {
value: tier,
timestamp: utils.moment().toISOString()
};
API call that would trigger this recalculation:
PUT /api/v1/profiles/{profileId}
Content-Type: application/json
{
"totalSpend": "1250.00",
"orderCount": "14",
"isActiveSubscriber": true
}
Result: user_tier recalculates to "platinum"
This type of multi-condition branching with fallback tiers showcases the power of Advanced Mode for complex business logic.
Testing with Dry Run
Before saving your Computed Trait, it is strongly recommended to validate your logic using the built-in Dry Run capability. This testing feature is available in both Basic and Advanced modes.
Running a Test
At the bottom of the Computed Trait editor, you will see a "Test before saving" section with a Run test button. Click Run test to execute your computed trait logic against sample data.
After the test completes, you will see:
-
Evaluation status: Displays SUCCESSFUL (highlighted in green) if the logic executed without errors.
-
Last Run timestamp: Shows when the test was last executed (e.g., "Last Run: March 11, 2026 at 10:35 AM").
Viewing Detailed Results
Click Show result to expand the result panel and view the actual output of the computed trait. The system runs your logic against randomly selected test profiles from your customer account, displaying the resulting value.
For example, after running the "whale" classification trait, you might see:
true / false
This confirms that your JavaScript logic executed correctly and the profile met the whale threshold.
Click Hide result to collapse the result panel.
Preview & Test
For more comprehensive testing options, click the Preview & Test link located below the dry run results. This opens an extended testing interface where you can explore additional profiles and scenarios.
Saving Your Computed Trait
Once you have validated your logic via the dry run, click the Create button in the top-right corner of the editor to save your Computed Trait. The new trait will then be applied to user profiles according to the logic categories (Processing, Init, Merge) you have configured.
Quick Reference
|
Step |
Action |
|---|---|
|
1 |
Click the code editor icon ( |
|
2 |
Enter the mandatory Name and Type fields |
|
3 |
Add one or more Triggers (the profile attributes that should trigger recalculation) |
|
4 |
Select a logic category tab (Processing, Init, or Merge) |
|
5 |
Write your JavaScript logic using |
|
6 |
Click Run test to validate with the dry run |
|
7 |
Review results via Show result |
|
8 |
Click Create to save |