Agent skill
rt-personalization
Create RT 2.0 Personalization services AND entities that return real-time personalized API responses. Creates both the service configuration (via tdx ps pz) and the actual Personalization entity (via API) with payload definition. Use after RT configuration is complete when user wants to "create RT personalization" or "build personalization API".
Install this agent skill to your Project
npx add-skill https://github.com/treasure-data/td-skills/tree/main/realtime-skills/rt-personalization
SKILL.md
RT 2.0 Personalization - Service and Entity Creation
Creates complete RT personalization: service configuration + Personalization entity with payload deployed to Console.
Prerequisites
- RT configuration complete (
rt-configskill orrt-setup-personalizationorchestrator) - RT status: "ok"
- Key events created
- RT attributes configured
- Parent segment folder exists
- TD_API_KEY environment variable set
Two-Step Creation Process
Step 1: Create Personalization Service (YAML workflow)
- Defines sections, criteria, attributes
- Pushed via
tdx ps push - Creates service configuration
Step 2: Create Personalization Entity (API workflow)
- Creates actual Personalization in Console
- Defines entry criteria (key event)
- Defines payload (response attributes)
- Visible in TD Console UI
Both steps required for complete personalization setup.
Migration from Previous Version
If you previously created a personalization service (before this update):
The old skill only created the service configuration (Step 1). The entity (Step 2) was missing, making personalization invisible in Console.
Check if Entity Exists
# Detect region
REGION=$(tdx config get endpoint 2>/dev/null | grep -o '[a-z][a-z][0-9][0-9]' | head -1)
REGION="${REGION:-us01}"
# List existing personalizations
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/entities/parent_segments/<ps_id>/realtime_personalizations" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "200" ]; then
echo "$BODY" | jq '.data[] | {id, name}'
else
echo "❌ Failed to list personalizations (HTTP $HTTP_CODE)"
echo "$BODY" | jq '.errors[]? | .detail' 2>/dev/null || echo "$BODY"
fi
If Entity is Missing
Option 1: Run Step 2 Only (Recommended)
- Skip Step 1 (service already exists)
- Follow Step 2 (sections 2a-2e) to create entity
Option 2: Recreate Everything
- Delete old service:
tdx ps pz delete <ps_id> <service_name> - Run complete skill (Step 1 + Step 2)
Verify Migration
After creating entity, test API endpoint:
curl -X GET \
"https://${REGION}.p13n.in.treasuredata.com/audiences/<ps_id>/personalizations/<pz_id>?td_client_id=test_user" \
-H "Authorization: TD1 ${TD_API_KEY}"
Should return personalized attributes (not 404).
Verify Prerequisites
# Detect region from tdx config
REGION=$(tdx config get endpoint 2>/dev/null | grep -o '[a-z][a-z][0-9][0-9]' | head -1)
REGION="${REGION:-us01}"
echo "Using region: $REGION"
# Check RT status
RT_STATUS=$(tdx ps rt list --json | jq -r --arg ps "<ps_id_or_name>" '.[] | select(.id==$ps or .name==$ps) | .status')
[ "$RT_STATUS" = "ok" ] || { echo "❌ RT status: $RT_STATUS (expected: ok)"; exit 1; }
# List key events
tdx api "/audiences/<ps_id>/realtime_key_events" --type cdp | jq '.data[] | {id, name}'
# List RT attributes
tdx api "/audiences/<ps_id>/realtime_attributes?page[size]=100" --type cdp | \
jq '.data[] | {id, name, type}'
Step 1: Create Personalization Service
Gather Requirements
Ask user:
-
"What's your personalization use case?"
- Product recommendations
- Cart recovery
- User profile API
- Content personalization
-
"Which key event should trigger personalization?"
- List available key events from RT config
-
"Which attributes should be returned?"
- Multi-select from RT attributes + batch attributes
-
"Do you need audience-based sections?"
- Yes: Different responses for VIP vs. regular users
- No: Same response for all users
Generate Service YAML
For simple use case (single section):
parent_segment_id: '<ps_id>'
parent_segment_name: '<ps_name>'
personalization_service:
name: 'product_recommendations'
description: 'Product recommendation service'
trigger_event: 'page_view'
sections:
- name: 'Default'
criteria: '' # Matches all users
attributes:
- last_product_viewed
- browsed_products_list
- page_views_24h
batch_segments: []
For multi-section use case:
parent_segment_id: '<ps_id>'
parent_segment_name: '<ps_name>'
personalization_service:
name: 'vip_personalization'
description: 'VIP-aware personalization service'
trigger_event: 'page_view'
sections:
- name: 'VIP Customers'
criteria: 'loyalty_tier = ''VIP'''
attributes:
- vip_exclusive_products_list
- vip_discount_percentage
- loyalty_points
batch_segments:
- vip_members
- name: 'Regular Customers'
criteria: '' # Default fallback
attributes:
- browsed_products_list
- standard_discount
batch_segments: []
Criteria syntax:
- Comparison:
>,<,>=,<=,=,!= - Logical:
AND,OR - Pattern:
LIKE,IN ('val1', 'val2') - Null:
IS NULL,IS NOT NULL
Push Service
cat > pz_service.yaml << 'EOF'
<YAML_CONTENT>
EOF
tdx ps push pz_service.yaml -y
# Verify service created
SERVICE_ID=$(tdx ps pz list <ps_id> --json | jq -r '.[] | select(.name=="<service_name>") | .id')
echo "Service ID: $SERVICE_ID"
Step 2: Create Personalization Entity (Critical!)
This step creates the actual Personalization visible in Console UI.
Validate API Key
# Validate API key is set
if [ -z "$TD_API_KEY" ]; then
echo "❌ TD_API_KEY environment variable not set"
echo "Set it with: export TD_API_KEY=your_master_api_key"
exit 1
fi
2a. Get Parent Segment Folder ID
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/audiences/<ps_id>/folders" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Failed to get folders (HTTP $HTTP_CODE)"
echo "$BODY" | jq '.errors[]? | .detail' 2>/dev/null || echo "$BODY"
exit 1
fi
FOLDER_ID=$(echo "$BODY" | jq -r '.[0].id')
if [ "$FOLDER_ID" = "null" ] || [ -z "$FOLDER_ID" ]; then
echo "❌ No folders found for parent segment"
echo "Parent segment must have at least one folder"
exit 1
fi
echo "Folder ID: $FOLDER_ID"
2b. Get Key Event ID
KEY_EVENT_NAME="<trigger_event_name>"
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/audiences/<ps_id>/realtime_key_events" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Failed to get key events (HTTP $HTTP_CODE)"
echo "$BODY" | jq '.errors[]? | .detail' 2>/dev/null || echo "$BODY"
exit 1
fi
KEY_EVENT_ID=$(echo "$BODY" | jq -r ".data[] | select(.name==\"$KEY_EVENT_NAME\") | .id")
if [ "$KEY_EVENT_ID" = "null" ] || [ -z "$KEY_EVENT_ID" ]; then
echo "❌ Key event '$KEY_EVENT_NAME' not found"
echo "Available key events:"
echo "$BODY" | jq '.data[] | {id, name}'
exit 1
fi
echo "Key Event ID: $KEY_EVENT_ID"
2c. Get RT Attribute IDs
# List all RT attributes with IDs
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/audiences/<ps_id>/realtime_attributes?page[size]=100" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Failed to get RT attributes (HTTP $HTTP_CODE)"
echo "$BODY" | jq '.errors[]? | .detail' 2>/dev/null || echo "$BODY"
exit 1
fi
echo "$BODY" | jq '.data[] | {
id,
name,
type,
aggregations: [.aggregations[]? | .identifier]
}' > rt_attributes.json
cat rt_attributes.json
For list attributes, note the aggregations[].identifier value (needed for subAttributeIdentifier).
2d. Build Attribute Payload
For single attributes:
{
"realtimeAttributeId": "<attr_id>",
"outputName": "last_product"
}
For list attributes (requires subAttributeIdentifier!):
{
"realtimeAttributeId": "<list_attr_id>",
"subAttributeIdentifier": "items",
"outputName": "browsed_products"
}
For counter attributes:
{
"realtimeAttributeId": "<counter_attr_id>",
"outputName": "page_view_count"
}
2e. Create Personalization Entity via API
# Generate unique payload node ID (matches frontend: crypto.randomUUID())
PAYLOAD_NODE_ID=$(uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]')
# Build payload JSON
cat > personalization_payload.json <<'EOF'
{
"attributes": {
"audienceId": "<ps_id>",
"name": "<personalization_name>",
"description": "<description>",
"sections": [
{
"name": "Default_Section",
"entryCriteria": {
"name": "Trigger on <event_name>",
"description": "Triggered when <event_name> occurs",
"keyEventCriteria": {
"keyEventId": "<key_event_id>",
"keyEventFilters": {
"type": "And",
"conditions": []
}
},
"profileCriteria": null
},
"payload": {
"<payload_node_id>": {
"type": "ResponseNode",
"name": "Response",
"description": "Personalization response payload",
"definition": {
"attributePayload": [
{
"realtimeAttributeId": "<single_attr_id>",
"outputName": "last_product"
},
{
"realtimeAttributeId": "<list_attr_id>",
"subAttributeIdentifier": "items",
"outputName": "browsed_products"
},
{
"realtimeAttributeId": "<counter_attr_id>",
"outputName": "page_views"
}
],
"segmentPayload": null,
"stringBuilder": []
}
}
},
"includeSensitive": false
}
]
},
"relationships": {
"parentFolder": {
"data": {
"id": "<folder_id>",
"type": "folder-segment"
}
}
}
}
EOF
# Replace placeholders with actual values
sed -i.bak \
-e "s/<ps_id>/$PS_ID/g" \
-e "s/<personalization_name>/$PZ_NAME/g" \
-e "s/<description>/$PZ_DESC/g" \
-e "s/<event_name>/$KEY_EVENT_NAME/g" \
-e "s/<key_event_id>/$KEY_EVENT_ID/g" \
-e "s/<folder_id>/$FOLDER_ID/g" \
-e "s/<payload_node_id>/$PAYLOAD_NODE_ID/g" \
-e "s/<single_attr_id>/$SINGLE_ATTR_ID/g" \
-e "s/<list_attr_id>/$LIST_ATTR_ID/g" \
-e "s/<counter_attr_id>/$COUNTER_ATTR_ID/g" \
personalization_payload.json
# Create personalization entity
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
'https://api-cdp.treasuredata.com/entities/realtime_personalizations' \
-H "Authorization: TD1 ${TD_API_KEY}" \
-H 'Content-Type: application/vnd.treasuredata.v1+json' \
--data @personalization_payload.json)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "❌ Failed to create Personalization entity (HTTP $HTTP_CODE)"
echo ""
echo "Possible causes:"
echo " - Invalid folder ID (check parent segment has folders)"
echo " - Invalid key event ID (verify key event exists)"
echo " - Missing RT attribute IDs (check attributes are configured)"
echo " - Invalid attribute payload (check subAttributeIdentifier for list attrs)"
echo ""
echo "API Response:"
echo "$BODY" | jq '.errors[]? | .detail' 2>/dev/null || echo "$BODY"
exit 1
fi
# Extract Personalization ID
PERSONALIZATION_ID=$(echo "$BODY" | jq -r '.data.id')
if [ "$PERSONALIZATION_ID" = "null" ] || [ -z "$PERSONALIZATION_ID" ]; then
echo "❌ Failed to extract Personalization ID from response"
echo "$BODY" | jq '.'
exit 1
fi
echo "✅ Personalization entity created!"
echo "Personalization ID: $PERSONALIZATION_ID"
# Clean up
rm personalization_payload.json personalization_payload.json.bak 2>/dev/null
2f. Add Static Strings (Optional)
Add static strings to response using stringBuilder:
"stringBuilder": [
{
"values": [
{
"value": "Welcome to our personalized experience!",
"type": "String"
}
],
"outputName": "welcome_message"
}
]
2g. Add Profile Criteria (Optional)
Filter personalization based on profile attributes:
"profileCriteria": {
"type": "And",
"conditions": [
{
"type": "RealtimeAttribute",
"realtimeAttributeId": "<loyalty_tier_attr_id>",
"operator": {
"type": "Equal",
"rightValue": "VIP",
"not": false
}
}
]
}
Verify Personalization Created
# List all personalizations
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/entities/parent_segments/<ps_id>/realtime_personalizations" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "200" ]; then
echo "$BODY" | jq '.data[] | {
id,
name,
sections_count: (.attributes.sections | length)
}'
else
echo "❌ Failed to list personalizations (HTTP $HTTP_CODE)"
fi
# Get specific personalization
RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://api-cdp.treasuredata.com/entities/realtime_personalizations/$PERSONALIZATION_ID" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "200" ]; then
echo "$BODY" | jq '.data.attributes | {
name,
sections: [.sections[] | .name]
}'
fi
Console URL
https://console-next.<region>.treasuredata.com/app/ps/<ps_id>/e/<personalization_id>/p/de
Replace <region> with your region (us01, eu01, ap01, ap02, etc.).
Test API Endpoint
# Get API endpoint
API_ENDPOINT="https://${REGION}.p13n.in.treasuredata.com/audiences/<ps_id>/personalizations/<personalization_id>"
echo "API Endpoint: $API_ENDPOINT"
# Test call
RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \
"${API_ENDPOINT}?td_client_id=test_user_123&event_name=<trigger_event>" \
-H "Authorization: TD1 ${TD_API_KEY}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Personalization API working!"
echo "$BODY" | jq '.'
else
echo "❌ Personalization API failed (HTTP $HTTP_CODE)"
echo "$BODY"
fi
Expected response:
{
"last_product": "product_123",
"browsed_products": ["product_123", "product_456", "product_789"],
"page_views": 42
}
Integration Code
JavaScript (Browser):
const REGION = 'us01'; // Change to your region
const API_ENDPOINT = `https://${REGION}.p13n.in.treasuredata.com/audiences/<ps_id>/personalizations/<pz_id>`;
// Get TD client ID from cookie
const td_client_id = document.cookie.match(/_td=([^;]+)/)?.[1];
fetch(`${API_ENDPOINT}?td_client_id=${td_client_id}&event_name=page_view`)
.then(r => r.json())
.then(data => {
console.log('Personalization:', data);
// Use data.last_product, data.browsed_products, etc.
});
Node.js (Server):
const https = require('https');
const REGION = process.env.TD_REGION || 'us01';
const options = {
hostname: `${REGION}.p13n.in.treasuredata.com`,
path: `/audiences/<ps_id>/personalizations/<pz_id>?td_client_id=${userId}`,
headers: {
'Authorization': `TD1 ${process.env.TD_API_KEY}`
}
};
https.get(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
const personalization = JSON.parse(data);
console.log(personalization);
});
});
Verification Checklist
After setup completes, verify:
# 1. RT status is "ok"
tdx ps rt list --json | jq -r --arg ps "<ps_id_or_name>" '.[] | select(.id==$ps or .name==$ps) | .status'
# Expected: "ok"
# 2. Key events exist
tdx api "/audiences/<ps_id>/realtime_key_events" --type cdp | jq '.data | length'
# Expected: > 0
# 3. RT attributes exist
tdx api "/audiences/<ps_id>/realtime_attributes?page[size]=100" --type cdp | jq '.data | length'
# Expected: > 0
# 4. Personalization entity exists
curl -s "https://api-cdp.treasuredata.com/entities/parent_segments/<ps_id>/realtime_personalizations" \
-H "Authorization: TD1 ${TD_API_KEY}" | jq '.data | length'
# Expected: > 0
# 5. API endpoint responds
curl -X GET "https://${REGION}.p13n.in.treasuredata.com/audiences/<ps_id>/personalizations/<pz_id>?td_client_id=test_user" \
-H "Authorization: TD1 ${TD_API_KEY}"
# Expected: JSON with attributes (not 404)
If any check fails, review the corresponding setup step.
Summary Output
✅ RT Personalization Created Successfully!
Service:
- Name: <service_name>
- Trigger Event: <event_name>
- Sections: <section_count>
Entity:
- Personalization ID: <personalization_id>
- Sections: <section_names>
- Attributes: <attribute_count>
API Endpoint:
https://<region>.p13n.in.treasuredata.com/audiences/<ps_id>/personalizations/<personalization_id>
Console URL:
https://console-next.<region>.treasuredata.com/app/ps/<ps_id>/e/<personalization_id>/p/de
Response Fields:
- <output_name_1> (from <attr_name_1>)
- <output_name_2> (from <attr_name_2>)
- <output_name_3> (from <attr_name_3>)
Next Steps:
1. Test API endpoint with real user IDs
2. Integrate into web/mobile application
3. Monitor API response times and errors
Troubleshooting
Error: "Record not found" when creating entity
- Missing folder ID or invalid PS ID
- Verify:
curl "https://api-cdp.treasuredata.com/audiences/<ps_id>/folders"
Error: Missing subAttributeIdentifier for list attribute
- List attributes require
subAttributeIdentifier - Get from:
tdx api "/audiences/<ps_id>/realtime_attributes/<attr_id>" --type cdp | jq '.data.aggregations[].identifier'
Empty API response
- User not in parent segment (check ID stitching)
- User has no attribute values (check RT processing)
- Verify user exists: Query event tables for user ID
Service vs. Entity confusion:
- Service (YAML): Configuration, not visible in Console
- Entity (API): Actual Personalization, visible in Console
- Both required for complete setup
Key Differences: Service vs. Entity
| Aspect | Service (Step 1) | Entity (Step 2) |
|---|---|---|
| Creation | tdx ps push |
API POST |
| Visibility | Not in Console | In Console UI |
| Purpose | Configuration | Deployment |
| Output Names | Uses attribute names | Custom output names |
| Static Strings | No | Yes |
| Required | Optional | Required |
Always create both for production deployments.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
email-campaign
This skill should be used when the user asks to "create an email", "build an email campaign", "design an email template", "generate an email for a segment", "preview an email", or "push an email to Engage". Generates enterprise-grade HTML email templates with live preview in Treasure Studio and natural language editing, then pushes the final version to Treasure Engage.
action-report
YAML format reference for action reports rendered via preview_action_report. MUST be read before writing any action report YAML — defines the report structure (title, summary, actions array) and action item fields (as_is, to_be, reason, priority, category, impact) with incremental build workflow. Required by seo-analysis and any skill that produces prioritized recommendations.
grid-dashboard
YAML format reference for grid dashboards rendered via preview_grid_dashboard. MUST be read before writing any dashboard YAML — defines the page structure, 6 cell types (kpi, gauge, scores, table, chart, markdown), grid layout rules, cell merging syntax, and incremental build workflow. Required by seo-analysis and any skill that produces visual data dashboards.
seo-analysis
Runs SEO and AEO (Answer Engine Optimization) analysis on websites or specific pages. Use when the user mentions SEO, AEO, search rankings, search optimization, or wants to analyze how their pages perform in search engines and AI answers. Produces a data dashboard and action report with before/after recommendations.
aps-doc-core
Core documentation generation patterns and framework for Treasure Data pipeline layers. Provides shared templates, quality validation, testing framework, and Confluence integration used by all layer-specific documentation skills.
aps-doc-id-unification
Expert documentation generation for ID unification layers. Documents identity resolution algorithms, merge strategies, match rules, entity graphs, and multi-workflow orchestration. Use when documenting ID unification processes.
Didn't find tool you were looking for?