Manage Personalization Studio campaigns — web personalization campaigns that deliver targeted content to audiences based on profile attributes.
tdx ps p13n requests are served by the Personalization Studio Config API:
| Site | Endpoint |
|---|---|
| us01 | personalization-studio-config.us01.treasuredata.com |
| ap01 | personalization-studio-config.treasuredata.co.jp |
| eu01 | personalization-studio-config.eu01.treasuredata.com |
Not all sites are supported. If Personalization Studio is unavailable for your site, tdx ps p13n reports that and exits.
| Command | Description |
|---|---|
list | List campaigns for a parent segment |
show | Show detail for a single campaign |
launch | Launch a Personalization Studio campaign |
unlaunch | Unlaunch a Personalization Studio campaign |
creative-asset show | Show the HTML and CSS of one creative |
creative-asset create | Create a creative from local HTML/CSS |
creative-asset update | Replace a creative's HTML and/or CSS |
personalization list | List a campaign's personalizations |
personalization create | Wire a creative to an audience and spot |
personalization delete | Remove a personalization from a campaign |
personalization list is the command to reach for when you are working with personalizations: it prints one flat, greppable record each, led by the personalization ID, and it is where the IDs that the creative-asset commands take come from.
show --include-personalizations is the thorough look at a campaign: the same wiring grouped into a page → spot → creative tree, appended to the campaign's status, dates, audiences and an exact personalization count. Use it to read a whole campaign in one pass, not to pull IDs out of one.
--output <path> writes the result to a file instead of stdout, and picks the format from the extension when --format is not given: .json → JSON, .jsonl → JSON Lines, .tsv/.tab → TSV, .txt/.text and any unrecognized extension → the table view. An explicit --format always wins.
# Same as --format json, inferred from the extension
tdx ps p13n creative-asset update 5 --html-file hero.html -y --output updated-asset.json
# Explicit --format beats the extension: this file holds JSON
tdx ps p13n list --format json --output campaigns.txt--output always writes the file. On the creative-asset commands a path ending in .tsv is still rejected, for the reason given under those commands.
# Set parent segment context
tdx use parent_segment "My Audience"
# List campaigns
tdx ps p13n list
# List campaigns with explicit parent segment ID
tdx ps p13n list --parent-segment-id 291115
# List by parent segment name (positional arg)
tdx ps p13n list "My Audience"
# JSON output for scripting
tdx ps p13n list --parent-segment-id 291115 --format json
# Inspect one campaign
tdx ps p13n show 42
# Launch a campaign (prompts for confirmation)
tdx ps p13n launch 42
# Stop a campaign serving
tdx ps p13n unlaunch 42
# ...including every personalization mapping, grouped by page and spot
tdx ps p13n show 42 --include-personalizations
# Inspect the creative content the mapping points at
tdx ps p13n creative-asset show 5
# Upload a new variant as its own asset
tdx ps p13n creative-asset create --html-file hero-v2.html --css-file hero-v2.css
# Swap that content for a new variant
tdx ps p13n creative-asset update 5 --html-file hero-v2.html
# See how a campaign's audiences, spots, and creatives are wired together,
# one flat record each (this is where personalization IDs come from)
tdx ps p13n personalization list 42
# Wire the new variant into the campaign for one audience
tdx ps p13n personalization create 42 \
--audience-id 7 --content-spot-id 3 --creative-asset-id 91
# Push the change live
tdx ps p13n launch 42List Personalization Studio campaigns for a parent segment.
tdx ps p13n list [name] [options]| Argument | Description |
|---|---|
name | Parent segment name (optional — uses context if omitted) |
| Option | Description |
|---|---|
--parent-segment-id <id> | Filter by parent segment ID (numeric) |
--parent-segment <name> | Filter by parent segment name (global option) |
--format json | Output as JSON array |
--format jsonl | Output as JSON Lines |
--format tsv | Output as TSV |
The parent segment is resolved in this priority:
--parent-segment-id <id>(used directly, no lookup)- Positional
[name]argument (resolved to ID) --parent-segment <name>global option (resolved to ID)- Error with usage hint
Human-readable output shows each campaign with status indicator:
✔ Found 3 campaigns
🚀 Summer Sale Campaign (launched)
id: 1 | Jun 1, 2026 – Sep 30, 2026 America/Vancouver
📝 Winter Promo (draft)
id: 2 | Dec 1, 2026 – Jan 15, 2027 UTC
⚠️ Old Homepage Test (stale)
id: 3 | Jan 1, 2025 – Mar 31, 2025 America/VancouverStatus meanings:
- launched 🚀 — Campaign is active and serving personalized content
- draft 📝 — Campaign is configured but not yet launched
- scheduled 📅 — Campaign has a start date and is ready to launch, but is not yet serving
- stale ⚠️ — Campaign was launched but configuration changed since last launch
- paused ⏸️ — Campaign was unlaunched and is no longer serving
Every page of results is fetched, up to 10,000 campaigns. If the listing stops before the end of the collection, the count is reported as a floor — Found 10000+ campaigns (stopped before the end of the collection) — rather than as a total. --format json output stays a plain array either way; the notice goes to stderr.
# List all campaigns for a parent segment by name
tdx ps p13n list "Customer360"
# Use session context (after `tdx use parent_segment "Customer360"`)
tdx ps p13n list
# Machine-readable output for automation
tdx ps p13n list --parent-segment-id 291115 --format json
# Pipe to jq for filtering
tdx ps p13n list --parent-segment-id 291115 --format json | jq '.[] | select(.status == "stale")'Show detail for a single campaign: its status, dates, parent segment, assigned audiences, and personalization count.
tdx ps p13n show [campaign] [options]| Argument | Description |
|---|---|
campaign | Campaign name or ID (optional — uses context if omitted) |
| Option | Description |
|---|---|
--campaign-id <id> | Campaign ID (numeric); overrides the positional and context |
--parent-segment-id <id> | Parent segment to resolve a campaign name within (numeric) |
--include-personalizations | Also print the audience → page → spot → creative mapping, grouped by page, and lift the count cap |
--format json | Output as a single JSON object |
--format jsonl | Output as JSON Lines |
--format tsv | Output as TSV |
The campaign is resolved in this priority:
--campaign-id <id>- Positional
[campaign]argument tdx use p13n_campaign <campaign>session context- Error with usage hint
--campaign-id is an ID by definition, so it skips name resolution entirely — a non-numeric value is rejected rather than looked up. The session context always holds an ID too, because tdx use p13n_campaign resolves at set-time. Only the positional argument can cost a lookup.
A numeric argument is taken as a campaign ID and used directly. Anything else is a campaign name, and names are only unique within a parent segment — so the parent segment is resolved first, in this priority:
--parent-segment-id <id>--parent-segment <name>tdx use parent_segment <name>session context- Error with usage hint
Without any of those, pass the campaign ID instead. If two campaigns in the parent segment share the name, show lists their IDs rather than guessing. An ID that does not exist reports Campaign not found: <id> — the same wording a name that matches nothing gets.
✔ Found campaign 'Summer Sale'
🚀 Summer Sale (launched)
id: 42
description: Homepage hero test
parent segment: Customer360 (id: 291115)
dates: Jun 1, 2026 – Sep 30, 2026 America/Vancouver
key event: 7
version: 3
audiences: High Value, Cart Abandoners
personalizations: 3
created: 2026-01-01T00:00:00.000Z
updated: 2026-01-02T00:00:00.000ZThe parent segment is shown by name, resolved from CDP. If that lookup fails, the ID is shown on its own.
With --include-personalizations, the mapping is appended, grouped by page and content spot:
Personalizations (3)
Homepage https://example.com/
Spot: Hero #hero
High Value → creative 5
(default) → creative 6
Pricing https://example.com/pricing
Spot: Sidebar .cart-promo
Cart Abandoners → creative 7(default) marks the campaign's default personalization for a content spot — the creative shown to profiles that fall into none of the assigned audiences.
This view is grouped for reading a campaign end to end, which is what the flag is for. It does not print the personalization ID or the content spot ID, so it is not where you pull an ID from — for a flat, greppable record per personalization, use personalization list.
The Personalization Studio Config API publishes no personalization count, so show has to walk the collection to report one — the count itself is what costs the requests, not the flag. Pages hold 100 personalizations, so most campaigns cost a single request.
Passing a campaign name rather than an ID costs more than one extra request: names are unique only within a parent segment, so resolving one walks that segment's whole campaign collection before it can call the name absent or ambiguous. Campaign pages also hold 100, so a segment with a few hundred campaigns costs a handful of requests, and the walk's own 10,000-item ceiling bounds it at roughly 100. Pass the campaign ID to skip the walk entirely.
To keep a plain show fast, the personalization walk stops after 5 pages. A campaign with more than 500 personalizations therefore reports a floor rather than a total:
personalizations: 500+ (use --include-personalizations for an exact count)In --format json this is explicit: personalizationCount holds the number reached and personalizationCountTruncated is true.
Passing --include-personalizations lifts the 5-page cap, but a hard ceiling of 10,000 personalizations still applies. A campaign above that ceiling still reports a floor, and the hint says so rather than pointing back at the flag you already passed:
personalizations: 10000+ (capped at 10000 personalizations)A stale campaign was launched, but its configuration changed afterwards, so what it serves is out of date. show calls this out on stderr, which keeps it visible even when stdout is piped:
⚠️ This campaign is stale: it was launched, but its configuration has
changed since. Relaunch it for those changes to take effect.# Show a campaign by ID
tdx ps p13n show 42
# Show a campaign by name, as printed by `tdx ps p13n list`
tdx use parent_segment Customer360
tdx ps p13n show "Summer Sale"
# Or name the parent segment inline
tdx ps p13n show "Summer Sale" --parent-segment-id 291115
# Set the campaign once, then omit it
tdx use p13n_campaign 42
tdx ps p13n show
# Override the context for one call
tdx ps p13n show --campaign-id 43
# Clear the campaign context again
tdx unset p13n_campaign
# Full mapping
tdx ps p13n show 42 --include-personalizations
# Machine-readable detail (a single object, not an array)
tdx ps p13n show 42 --format json | jq '.personalizationCount'
# Every audience assigned to the campaign
tdx ps p13n show 42 --format json | jq -r '.audiences[].name'Launch a Personalization Studio campaign: publish its personalizations and start serving them.
tdx ps p13n launch [campaign] [options]The Config API deletes and re-creates the campaign's CDP personalization. Anything edited directly in CDP for this campaign is lost. launch states this and asks for confirmation before acting.
| Argument | Description |
|---|---|
campaign | Campaign name or ID (optional — uses context if omitted) |
| Option | Description |
|---|---|
--campaign-id <id> | Campaign ID (numeric); overrides the positional and context |
-y, --yes | Skip the confirmation prompt |
--parent-segment-id <id> | Parent segment to resolve a campaign name within (numeric) |
--format json | Output the resulting campaign as a single JSON object |
The campaign is resolved exactly as it is for show.
launch prints what it is about to replace, then asks:
📝 Summer Sale (draft)
id: 42
parent segment: Customer360 (id: 291115)
schedule: Feb 24, 2026 – Feb 24, 2027 UTC
audiences (2): High Value, Cart Abandoners
personalizations: 5
⚠ Launching will DELETE and REPLACE all CDP personalizations for this
campaign. Direct CDP edits will be lost.
Launch campaign 'Summer Sale'? [y/N]The summary, the warning and the prompt all go to stderr, so --format json stays pipeable. With -y the prompt is skipped, but a single summary line is still written to stderr so an automated launch is legible in a log:
⚠ Launching 'Summer Sale' (id 42): 2 audiences, 5 personalizations — replaces all personalizations for this campaign
✔ Launched 'Summer Sale' (version 4)Launching bumps version and sets cdpPersonalizationId. In a non-interactive shell (a CI job, a pipeline) launch refuses to act without -y rather than hanging on a prompt nobody can answer.
launch does not second-guess the service: it asks, and reports what comes back. A campaign that cannot launch is rejected with the service's own explanation.
| Situation | What you see |
|---|---|
| Campaign is already launched | Next step: unlaunch first, or edit it (which marks it stale) |
| Campaign has no audiences | Campaign has no audiences |
| An audience has no personalization | All campaign audiences must have at least one personalization |
| Key lacks the Personalization Studio permission | Forbidden, with the request-access hint |
| First launch for the account, key cannot create databases | Failed to provision metrics database |
| Parent segment has no root folder in CDP | Failed to launch campaign, then the CDP message |
| CDP rejected the personalization payload | The CDP validation errors, one per field (below) |
A stale campaign relaunches directly — no unlaunch first. Note that a launch which fails at CDP still leaves the campaign stale: the service marks it before calling CDP, so a failed launch does change the status.
When CDP is what rejected the launch, the Config API forwards CDP's own validation errors and tdx renders them against the field each one names:
Error: [INVALID_REQUEST] Could not launch campaign 42: Failed to launch campaign
✖ sections[0] → entry criteria → profile criteria → type: Type is not included in the list (RT_API_INVALID_INCLUSION_VALUE)
✖ sections[0] → entry criteria → profile criteria → conditions: Conditions is invalid (RT_API_INVALID_FORMAT_OR_ASSOCIATION)
This action was rejected by CDP. Check the campaign and its
personalizations, then try again:
tdx ps p13n show 42 --include-personalizationsWhere an error names a field, the path comes from its JSON pointer — so array indices are 0-based and refer to the CDP personalization payload rather than to anything tdx prints. CDP also fails for reasons that name no field at all (a parent segment with no root folder, for instance); those show as a bullet with just the message. Run with --debug to see the raw API response.
Errors are written to stderr and exit 1; no payload is written to stdout, --format json included.
# Launch by ID, with confirmation
tdx ps p13n launch 42
# Launch by name within a parent segment
tdx ps p13n launch "Summer Sale" --parent-segment-id 291115
# Unattended, for automation
tdx ps p13n launch 42 -y
# Capture the launched campaign
tdx ps p13n launch 42 -y --format json | jq '.version'
# Use the session context
tdx use p13n_campaign 42
tdx ps p13n launchUnlaunch a Personalization Studio campaign. The campaign moves to paused and stops serving personalized content shortly after; its configuration in Personalization Studio is untouched, so launch restores it.
tdx ps p13n unlaunch [campaign] [options]| Argument | Description |
|---|---|
campaign | Campaign name or ID (optional — uses context if omitted) |
| Option | Description |
|---|---|
--campaign-id <id> | Campaign ID (numeric); overrides the positional and context |
-y, --yes | Skip the confirmation prompt |
--parent-segment-id <id> | Parent segment to resolve a campaign name within (numeric) |
--format json | Output the resulting campaign as a single JSON object |
The campaign is resolved exactly as it is for show.
🚀 Summer Sale (launched)
id: 42
parent segment: 291115
schedule: Feb 24, 2026 – Feb 24, 2027 UTC
audiences (2): High Value, Cart Abandoners
⚠ Unlaunching a campaign will result in it not serving personalized content.
Launch it again to restore it.
Unlaunch campaign 'Summer Sale'? [y/N] y
✔ Unlaunched 'Summer Sale' (status: paused)unlaunch is idempotent: a campaign that is not serving comes back unchanged rather than erroring, so re-running it is safe.
# Stop a campaign serving
tdx ps p13n unlaunch 42
# Unattended
tdx ps p13n unlaunch 42 -y
# Unlaunch, edit in Studio, relaunch
tdx ps p13n unlaunch 42 -y
# ...make changes...
tdx ps p13n launch 42 -yShow the HTML and CSS stored for one creative asset — the content a personalization renders into a content spot.
tdx ps p13n creative-asset show <id> [--full]| Argument | Description |
|---|---|
<id> | Creative asset ID (numeric) |
| Option | Description |
|---|---|
--full | Print HTML and CSS in full instead of the first 20 lines |
--format json | Output as a single JSON object |
--format jsonl | Output as JSON Lines |
--full affects the rendered view only. --format json always carries the complete htmlValue, cssValue, and grapesjsData, whether or not the flag is passed.
--format tsv is rejected here. HTML and CSS contain newlines, so a TSV row would break across lines and could not be read back.
✔ Found creative asset 5
Creative asset 5
grapesjs data: present (see --format json)
created: 2026-01-01T00:00:00.000Z
updated: 2026-01-02T00:00:00.000Z
HTML: 24 lines
<section class="hero">
<h1>Summer Sale</h1>
...
… 4 more lines (use --full)
CSS: 3 lines
.hero { background: #111; }
.hero h1 { font-size: 32px; }
.hero p { color: #eee; }
The service sanitizes HTML and CSS on write, so this is the stored version.
It may differ from the file it was uploaded from.A field with no content reads (empty). grapesjs data reports only whether the GrapesJS editor document is present — it is an editor-internal structure, so the value itself is left to --format json rather than printed.
# Show a creative asset by ID
tdx ps p13n creative-asset show 5
# Find an ID first
tdx ps p13n personalization list 42 --format json | jq -r '.[].creativeAssetId'
# Print the HTML and CSS unabridged
tdx ps p13n creative-asset show 5 --full
# Machine-readable detail
tdx ps p13n creative-asset show 5 --format jsonUpload creative content from local files as a new asset — the variant end of the optimization loop, where update is the in-place end.
tdx ps p13n creative-asset create --html-file <path> [--css-file <path>] [-y]| Option | Description |
|---|---|
--html-file <path> | File to read the HTML from (required) |
--css-file <path> | File to read the CSS from |
--full | Print HTML and CSS in full instead of the first 20 lines |
-y, --yes | Skip the confirmation prompt (global option) |
--format json | Output as a single JSON object |
--format jsonl | Output as JSON Lines |
--html-file is required: an asset with no HTML renders nothing. As with show, --format tsv is rejected because HTML and CSS contain newlines.
If the service rejects the submitted HTML or CSS, the command prints its validation details as a user error without a stack trace.
There is no name or label field on a creative asset — it is identified by the numeric ID this command prints, which is what you pass to creative-asset show, and what a personalization points at. Note the ID down, or capture it with --format json | jq -r .id.
grapesjs_data, the Personalization Studio visual editor's own copy of the content, is left unset on a new asset, so the editor builds its document from your HTML the first time the asset is opened in Studio.
Unlike update, nothing is cleared here: a new asset has no stale editor document to discard.
Creating creative asset
HTML: hero-v2.html (24 lines)
CSS: hero-v2.css (3 lines)
Create this creative asset? [y/N] y
✔ Created creative asset 7
Creative asset 7
grapesjs data: none
created: 2026-03-04T09:12:00.000Z
updated: 2026-03-04T09:12:00.000Z
HTML: 22 lines
<section class="hero">
...
CSS: 3 lines
.hero { background: #111; }
The service sanitizes HTML and CSS on write, so this is the stored version.
It may differ from the file it was uploaded from.
! Server may have modified HTML/CSS. Use tdx ps p13n creative-asset show 7 to verify the final version.The asset printed back is the stored one, after sanitization. Everything except the asset itself goes to stderr, so --format json emits the bare asset object — the same shape show and update emit, with no warnings key.
personalization list shows which personalizations point at an asset once the wiring is in place.
# HTML only
tdx ps p13n creative-asset create --html-file hero-v2.html
# HTML and CSS
tdx ps p13n creative-asset create --html-file hero-v2.html --css-file hero-v2.css
# Unattended, capturing the new ID
tdx ps p13n creative-asset create --html-file hero-v2.html -y --format json | jq -r '.id'
# Check what the sanitizer kept
tdx ps p13n creative-asset create --html-file hero-v2.html -y --format json | jq -r '.htmlValue'Replace a creative asset's HTML and/or CSS from local files without deleting and recreating it so every personalization pointing at the asset keeps working.
tdx ps p13n creative-asset update <id> [--html-file <path>] [--css-file <path>] [-y]| Argument | Description |
|---|---|
<id> | Creative asset ID (numeric) |
| Option | Description |
|---|---|
--html-file <path> | File to read the HTML from |
--css-file <path> | File to read the CSS from |
--full | Print HTML and CSS in full instead of the first 20 lines |
-y, --yes | Skip the confirmation prompt (global option) |
--format json | Output as a single JSON object |
--format jsonl | Output as JSON Lines |
At least one of --html-file and --css-file is required. As with show, --format tsv is rejected because HTML and CSS contain newlines.
If the service rejects the submitted HTML or CSS, the command prints its validation details as a user error without a stack trace. A missing asset is reported as NOT_FOUND.
A field you do not pass keeps its stored value — omitting --css-file never blanks the CSS. So a CSS-only change needs only --css-file, and the HTML is left exactly as it is.
A creative asset can carry grapesjs_data, the Personalization Studio visual editor's own copy of the content. Every update discards it, so the editor rebuilds from the new HTML.
This is deliberate. If the editor document were left behind it would no longer match the content you just uploaded, and the next time someone opened the asset in Studio the UI could re-render from the stale copy and silently revert your change. Clearing it cannot be undone.
Updating creative asset 5
HTML: hero-v2.html (24 lines)
unchanged: CSS
This discards the visual editor document, so the editor will rebuild it
from the new HTML. Launched campaigns using this asset become stale.
Update this creative asset? [y/N] y
✔ Updated creative asset 5
Creative asset 5
grapesjs data: none
created: 2026-01-01T00:00:00.000Z
updated: 2026-03-04T09:12:00.000Z
HTML: 22 lines
<section class="hero">
...
CSS: 3 lines
.hero { background: #111; }
The service sanitizes HTML and CSS on write, so this is the stored version.
It may differ from the file it was uploaded from.
! The visual editor document was cleared. The editor will rebuild it from the new HTML.
! Launched campaigns using this asset are now stale. Relaunch them to push the change: tdx ps p13n launch <campaign>The asset printed back is the stored one, after sanitization — so if the service stripped something from your file, you see it here. Note the line count above: 24 lines went up, 22 came back.
The summary is printed whether or not you are prompted, so a -y run still records what changed. Everything except the asset itself goes to stderr, keeping --format json pipeable.
# Replace the HTML, leaving the CSS as it is
tdx ps p13n creative-asset update 5 --html-file hero-v2.html
# Replace both
tdx ps p13n creative-asset update 5 --html-file hero-v2.html --css-file hero-v2.css
# Tweak only the stylesheet
tdx ps p13n creative-asset update 5 --css-file hero-v2.css
# Unattended: no prompt, but the summary is still printed
tdx ps p13n creative-asset update 5 --html-file hero-v2.html -y
# Check what the sanitizer kept
tdx ps p13n creative-asset update 5 --html-file hero-v2.html -y --format json | jq -r '.htmlValue'List how a campaign is wired: which audience sees which creative asset, in which content spot, on which page.
tdx ps p13n personalization list [campaign]| Argument | Description |
|---|---|
[campaign] | Campaign name or ID. Falls back to the session context if omitted |
| Option | Description |
|---|---|
--campaign-id <id> | Campaign ID; overrides both the positional and the context |
--parent-segment-id <id> | Parent segment to resolve a campaign name within |
--format json | Output as JSON array |
--format jsonl | Output as JSON Lines |
--format tsv | Output as TSV |
The campaign is resolved in this priority:
--campaign-id <id>(used directly, no lookup)- Positional
[campaign]argument (name or ID) tdx use p13n_campaign <campaign>session context- Error with usage hint
The campaign heads the listing, then one block per personalization, led by its ID:
✔ Found 4 personalizations
🚀 Summer Sale (launched)
id: YToxOjM
audience: High Value
page: https://example.com/
spot: #hero-banner
creative asset: 5
id: ZDoz
audience: (default)
page: https://example.com/
spot: #hero-banner
creative asset: 4(default) marks a default personalization: the fallback served to every profile that no audience-scoped personalization on that spot claims.
The rendered view shows the page URL and the spot's CSS selector, since those identify where the creative lands. The page and content spot names, and their IDs, are in --format json only.
A campaign with nothing wired yet prints the steps to wire one rather than an empty listing.
Personalizations carry no status of their own — the resource is an ID plus three relationships (audience, content spot, creative asset), with no attributes. Whether the wiring is serving is a property of the campaign, shown by show.
To read the same wiring as part of a whole campaign — grouped into a page → spot → creative tree, under the campaign's status, dates and audiences — use show --include-personalizations instead.
Every page of results is fetched, up to 10,000 personalizations. If the walk stops before the end of the collection, the count is reported as a floor — Found 10000+ personalizations (stopped before the end of the collection) — rather than as a total. --format json output stays a plain array either way; the notice goes to stderr.
# List a campaign's wiring by ID
tdx ps p13n personalization list --campaign-id 42
# By campaign name, within a parent segment
tdx ps p13n personalization list "Summer Sale" --parent-segment-id 291115
# Use session context (after `tdx use p13n_campaign "Summer Sale"`)
tdx ps p13n personalization list
# Find the content spot IDs on a campaign
tdx ps p13n personalization list 42 --format json | jq -r '.[].contentSpotId' | sort -u
# Which spots have a default personalization, and which do not
tdx ps p13n personalization list 42 --format json \
| jq -r 'group_by(.contentSpotId)[]
| "\(.[0].contentSpotName): default=\(any(.[]; .audienceId == null))"'Wire a creative asset to one of a campaign's audiences in one content spot — the write half of personalization list.
tdx ps p13n personalization create [campaign] --content-spot-id <id> --creative-asset-id <id> [--audience-id <id>]Everything this command references already exists: the campaign, the audience assigned to it, the content spot on the page, and the creative asset. Creating the personalization is what connects them. The pieces come from elsewhere:
| Flag | Where the ID comes from |
|---|---|
--content-spot-id | tdx ps p13n personalization list <campaign> |
--creative-asset-id | tdx ps p13n creative-asset create --html-file <file> |
--audience-id | tdx ps p13n show <campaign> |
| Argument | Description |
|---|---|
campaign | Campaign name or ID. Optional — uses p13n_campaign context when omitted. |
| Option | Description |
|---|---|
--content-spot-id <id> | Content spot to render into. Required. |
--creative-asset-id <id> | Creative asset to render. Required. |
--audience-id <id> | Audience to target. Omit for the default personalization — see below. |
--campaign-id <id> | Campaign ID; overrides the positional argument and the session context |
--parent-segment-id <id> | Parent segment to resolve a campaign name within |
-y, --yes | Skip the confirmation prompt |
--format <fmt> | json, jsonl, tsv, or table (default) |
--output <path> | Write to a file — see Writing output to a file |
The same as personalization list: --campaign-id → positional argument → tdx use p13n_campaign. A positional name needs a parent segment, from --parent-segment-id, --parent-segment, or tdx use parent_segment.
personalization delete is the exception to all of this: it takes --campaign-id and nothing else.
Omitting --audience-id wires the default personalization for the spot: the fallback rendered to every profile that no audience-scoped personalization on that spot claims. There is at most one per campaign and content spot, the same as for any single audience.
Because omitting a flag is also how you get there by accident, the confirmation summary says so plainly:
audience: (default) — the fallback for profiles in no assigned audienceThe confirmation summary states the wiring, names the audience where the campaign knows it, and warns when the campaign is launched:
Creating personalization
campaign: Summer Sale (id: 42) (launched)
audience: High Value (id: 7)
content spot: 3
creative asset: 91
! This campaign is launched, so the create will mark it stale.
Create this personalization? [y/N]After the create, the new personalization and what it did to the campaign:
✔ Created personalization YToxOjM
id: YToxOjM
audience: High Value
page: https://example.com/
spot: #hero-banner (id: 3)
creative asset: 91
! Campaign is now stale. Relaunch to apply changes: tdx ps p13n launch 42The fields are the ones personalization list prints, resolved the same two ways: the create asks the service to sideload the content spot and its page, and the audience name comes from the campaign the command re-reads for the staleness check. The creative asset stays a bare ID — the service cannot sideload creative assets, and they have no name anyway.
All of it degrades. A campaign that cannot be re-read costs the audience name, and a service that stops honouring the sideload costs the page and spot labels; both come back null in --format json and print as (unknown) in the rendered view. The IDs never depend on either, so jq -r .id, .contentSpotId and .creativeAssetId are always safe to script against.
Staleness is read back, not assumed. Creating a personalization changes what a launched campaign would serve, so the service marks such a campaign stale. A campaign in any other state is left alone, and is reported as it stands instead:
Campaign status: draft. Only a launched campaign is marked stale,
so there is nothing to relaunch yet — launch it when ready: tdx ps p13n launch 42Every line above goes to stderr, so --format json puts nothing but the personalization on stdout.
The service validates the whole wiring, and each rejection names the next command to run. There are no pre-flight requests: a bad reference costs one round trip, not five.
| What is wrong | Next step |
|---|---|
| No campaign with that ID | tdx ps p13n list |
| Audience not assigned to the campaign | tdx ps p13n show <campaign> |
| Content spot not in this account | tdx ps p13n personalization list <campaign> |
| Creative asset not in this account | tdx ps p13n creative-asset create --html-file <file> |
| A personalization already exists | tdx ps p13n personalization list <campaign> |
Two of these read less obviously than they look:
- The audience error covers two cases. The service reports an audience that does not exist and an audience that is not a member of the campaign identically, so the message names both and says how to look.
- "Already exists" fires when re-wiring a spot to a different creative asset. One content spot holds one creative asset per audience, per campaign, so re-wiring a spot to a different asset is rejected as a duplicate. There is no update for a personalization, so changing which asset a wired spot renders means
personalization deletefollowed by a fresh create — the error names the delete to run.
# Wire a creative to an audience in a spot
tdx ps p13n personalization create 42 \
--audience-id 7 --content-spot-id 3 --creative-asset-id 91
# The fallback for profiles in no assigned audience
tdx ps p13n personalization create 42 --content-spot-id 3 --creative-asset-id 91
# By campaign name, within a parent segment
tdx ps p13n personalization create "Summer Sale" --parent-segment-id 291115 \
--audience-id 7 --content-spot-id 3 --creative-asset-id 91
# Unattended: upload a variant, wire it in, relaunch
asset=$(tdx ps p13n creative-asset create --html-file hero-v2.html -y --format json | jq -r .id)
tdx ps p13n personalization create --campaign-id 42 \
--audience-id 7 --content-spot-id 3 --creative-asset-id "$asset" -y --format json
tdx ps p13n launch 42 -yRemove a personalization: stop serving one creative asset in one content spot, for one audience.
tdx ps p13n personalization delete <id> --campaign-id <id> [-y]Personalizations have no update. Deleting one and creating it again is the only way to change what a wired spot renders, so this command prints the create that puts it back.
| Argument | Description |
|---|---|
<id> | Personalization ID, from personalization list |
The ID is an opaque base64 token. It cannot be constructed or guessed, only listed.
| Option | Description |
|---|---|
--campaign-id <id> | Campaign the personalization belongs to. Required — see below. |
-y, --yes | Skip the confirmation prompt |
--format <fmt> | json, jsonl, tsv, or table (default) |
--output <path> | Write to a file — see Writing output to a file |
This is the one tdx ps p13n command that does not fall back to the session context set by tdx use p13n_campaign, and it takes no campaign name or positional argument either:
$ tdx use p13n_campaign 42
$ tdx ps p13n personalization delete YToxOjM
Error: --campaign-id is required for delete.A delete cannot be taken back, and the personalization ID does not say which campaign it belongs to — the same token is meaningful in more than one. A context pinned an hour ago would be enough to remove a personalization from a campaign you never looked at, so the campaign is named on the command line every time.
The record is read before anything is deleted, and the prompt describes it rather than the token:
Deleting personalization YToxOjM
campaign: 42
audience: High Value
page: https://example.com/
spot: #hero-banner (id: 3)
creative asset: 91
⚠ This will remove that creative asset from that spot in the campaign.
Delete this personalization? [y/N]With -y the prompt is replaced by a one-line record of what is about to go, so an unattended run still leaves a trace:
⚠ Deleting personalization YToxOjM on campaign 42: High Value → #hero-bannerAfter the delete, the record as it was, what it did to the campaign, and the way back:
✔ Deleted personalization YToxOjM
id: YToxOjM
audience: High Value
page: https://example.com/
spot: #hero-banner (id: 3)
creative asset: 91
! Campaign is now stale. Relaunch to apply changes: tdx ps p13n launch 42
To restore this personalization, or to point the same spot at a different
creative asset, recreate it:
tdx ps p13n personalization create --campaign-id 42 --content-spot-id 3 --creative-asset-id 91 --audience-id 7The recreate command is built from the record that was just removed, so it restores the same wiring as typed. For a default personalization it carries no --audience-id, since adding one would recreate the fallback as something else.
Staleness is read back, not assumed — exactly as on personalization create. Deleting a personalization changes what a launched campaign would serve, so the service marks such a campaign stale; a campaign in any other state is left alone and is reported as it stands:
Campaign status: draft. Only a launched campaign is marked stale,
so there is nothing to relaunch yet — launch it when ready: tdx ps p13n launch 42Everything above goes to stderr. --format json puts the deleted personalization on stdout and nothing else — the same object shape personalization list emits, so jq -r .contentSpotId works the same either way.
| What is wrong | Next step |
|---|---|
| No campaign with that ID | tdx ps p13n list |
| The campaign has no personalization with that ID | tdx ps p13n personalization list <campaign> |
| The personalization listing stopped early | The record may still exist; nothing was deleted |
No --campaign-id | Pass it explicitly — the session context is ignored |
An unknown ID is caught by the read, before anything is deleted:
Error: Personalization not found on campaign 42: YToxOjM
See what is wired on this campaign:
tdx ps p13n personalization list 42The listing is bounded to avoid unending pagination. If it stops early, a missing ID is not treated as definitive: the error reports how many personalizations were searched and says the record may still exist. The delete is refused because the command cannot safely confirm an unseen record.
Error: Personalization not found in the 2 personalizations searched on campaign 42: YToxOjM
The search stopped before the end of the collection, so the personalization may still exist. No personalization was deleted.# Delete, with the confirmation prompt
tdx ps p13n personalization delete YToxOjM --campaign-id 42
# Unattended
tdx ps p13n personalization delete YToxOjM --campaign-id 42 -y
# Re-point a spot at a different creative asset: delete, then create
tdx ps p13n personalization delete YToxOjM --campaign-id 42 -y --format json \
| jq -r '"--content-spot-id \(.contentSpotId) --audience-id \(.audienceId)"'
tdx ps p13n personalization create --campaign-id 42 \
--content-spot-id 3 --audience-id 7 --creative-asset-id 92 -y
tdx ps p13n launch 42 -y
# Clear a campaign's wiring
tdx ps p13n personalization list --campaign-id 42 --format json \
| jq -r '.[].id' \
| xargs -I{} tdx ps p13n personalization delete {} --campaign-id 42 -yPin a campaign to the current shell session so tdx ps p13n commands can omit it:
tdx use p13n_campaign <campaign> # name or ID
tdx unset p13n_campaign # clear it againtdx use p13n_campaign accepts a name or an ID but stores the ID. It resolves the value against the API at set-time, which does two things: it fails immediately on a campaign that does not exist, and it echoes the campaign's name so you can confirm you pinned the right one.
$ tdx use p13n_campaign 42
Session p13n_campaign set to: 42 (Summer Sale) (session: 12345)
$ tdx use p13n_campaign 99999999
Error: Campaign not found: 99999999
Use "tdx ps p13n list" to see available campaigns.The ID is what gets stored because campaign names are unique only within a parent segment. Storing a name would mean every later tdx ps p13n call also needed parent_segment in context to make sense of it; storing the ID keeps those calls free of any lookup.
For the same reason, setting the context by name needs a parent segment at set-time — via the global --parent-segment <name> flag or tdx use parent_segment <name>. Note that --parent-segment-id is not available here: it is a per-command option on the commands that take a campaign, not a global one. Setting the context by ID needs nothing.
tdx unset p13n_campaign clears only this key; other session context (database, parent segment, and so on) is left alone. To clear everything at once, use tdx use --clear.
Both tdx use (with no arguments) and tdx status list the current p13n_campaign.
One command ignores this context: personalization delete requires --campaign-id every time, because a stale pin would aim a delete at a campaign you never looked at.