Skip to content
Last updated

Personalization Studio Commands

Manage Personalization Studio campaigns — web personalization campaigns that deliver targeted content to audiences based on profile attributes.

Backend endpoint

tdx ps p13n requests are served by the Personalization Studio Config API:

SiteEndpoint
us01personalization-studio-config.us01.treasuredata.com
ap01personalization-studio-config.treasuredata.co.jp
eu01personalization-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.

Commands

CommandDescription
listList campaigns for a parent segment
showShow detail for a single campaign
launchLaunch a Personalization Studio campaign
unlaunchUnlaunch a Personalization Studio campaign
creative-asset showShow the HTML and CSS of one creative
creative-asset createCreate a creative from local HTML/CSS
creative-asset updateReplace a creative's HTML and/or CSS
personalization listList a campaign's personalizations
personalization createWire a creative to an audience and spot
personalization deleteRemove a personalization from a campaign

Two views of a campaign's personalizations

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.

Writing output to a file

--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.

Typical Usage

# 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 42

list

List Personalization Studio campaigns for a parent segment.

tdx ps p13n list [name] [options]

Arguments

ArgumentDescription
nameParent segment name (optional — uses context if omitted)

Options

OptionDescription
--parent-segment-id <id>Filter by parent segment ID (numeric)
--parent-segment <name>Filter by parent segment name (global option)
--format jsonOutput as JSON array
--format jsonlOutput as JSON Lines
--format tsvOutput as TSV

Resolution order

The parent segment is resolved in this priority:

  1. --parent-segment-id <id> (used directly, no lookup)
  2. Positional [name] argument (resolved to ID)
  3. --parent-segment <name> global option (resolved to ID)
  4. Error with usage hint

Output

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/Vancouver

Status 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.

Examples

# 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

Show detail for a single campaign: its status, dates, parent segment, assigned audiences, and personalization count.

tdx ps p13n show [campaign] [options]

Arguments

ArgumentDescription
campaignCampaign name or ID (optional — uses context if omitted)

Options

OptionDescription
--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-personalizationsAlso print the audience → page → spot → creative mapping, grouped by page, and lift the count cap
--format jsonOutput as a single JSON object
--format jsonlOutput as JSON Lines
--format tsvOutput as TSV

Resolution order

The campaign is resolved in this priority:

  1. --campaign-id <id>
  2. Positional [campaign] argument
  3. tdx use p13n_campaign <campaign> session context
  4. 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:

  1. --parent-segment-id <id>
  2. --parent-segment <name>
  3. tdx use parent_segment <name> session context
  4. 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.

Output

✔ 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.000Z

The 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.

Cost of the count

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)

Stale campaigns

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.

Examples

# 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

Launch a Personalization Studio campaign: publish its personalizations and start serving them.

tdx ps p13n launch [campaign] [options]
Launching replaces what CDP holds

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.

Arguments

ArgumentDescription
campaignCampaign name or ID (optional — uses context if omitted)

Options

OptionDescription
--campaign-id <id>Campaign ID (numeric); overrides the positional and context
-y, --yesSkip the confirmation prompt
--parent-segment-id <id>Parent segment to resolve a campaign name within (numeric)
--format jsonOutput the resulting campaign as a single JSON object

The campaign is resolved exactly as it is for show.

Output

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.

Errors

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.

SituationWhat you see
Campaign is already launchedNext step: unlaunch first, or edit it (which marks it stale)
Campaign has no audiencesCampaign has no audiences
An audience has no personalizationAll campaign audiences must have at least one personalization
Key lacks the Personalization Studio permissionForbidden, with the request-access hint
First launch for the account, key cannot create databasesFailed to provision metrics database
Parent segment has no root folder in CDPFailed to launch campaign, then the CDP message
CDP rejected the personalization payloadThe 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-personalizations

Where 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.

Examples

# 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 launch

unlaunch

Unlaunch 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]

Arguments

ArgumentDescription
campaignCampaign name or ID (optional — uses context if omitted)

Options

OptionDescription
--campaign-id <id>Campaign ID (numeric); overrides the positional and context
-y, --yesSkip the confirmation prompt
--parent-segment-id <id>Parent segment to resolve a campaign name within (numeric)
--format jsonOutput the resulting campaign as a single JSON object

The campaign is resolved exactly as it is for show.

Output

🚀 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.

Examples

# 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 -y

creative-asset show

Show 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]

Arguments

ArgumentDescription
<id>Creative asset ID (numeric)

Options

OptionDescription
--fullPrint HTML and CSS in full instead of the first 20 lines
--format jsonOutput as a single JSON object
--format jsonlOutput 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.

Output

✔ 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.

Examples

# 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 json

creative-asset create

Upload 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]

Options

OptionDescription
--html-file <path>File to read the HTML from (required)
--css-file <path>File to read the CSS from
--fullPrint HTML and CSS in full instead of the first 20 lines
-y, --yesSkip the confirmation prompt (global option)
--format jsonOutput as a single JSON object
--format jsonlOutput 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.

A creative asset has no name

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.

The visual editor document

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.

Output

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.

Examples

# 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'

creative-asset update

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]

Arguments

ArgumentDescription
<id>Creative asset ID (numeric)

Options

OptionDescription
--html-file <path>File to read the HTML from
--css-file <path>File to read the CSS from
--fullPrint HTML and CSS in full instead of the first 20 lines
-y, --yesSkip the confirmation prompt (global option)
--format jsonOutput as a single JSON object
--format jsonlOutput 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.

Updates are partial

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.

The visual editor document is always cleared

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.

Output

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.

Examples

# 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'

personalization list

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]

Arguments

ArgumentDescription
[campaign]Campaign name or ID. Falls back to the session context if omitted

Options

OptionDescription
--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 jsonOutput as JSON array
--format jsonlOutput as JSON Lines
--format tsvOutput as TSV

Resolution order

The campaign is resolved in this priority:

  1. --campaign-id <id> (used directly, no lookup)
  2. Positional [campaign] argument (name or ID)
  3. tdx use p13n_campaign <campaign> session context
  4. Error with usage hint

Output

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.

Examples

# 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))"'

personalization create

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:

FlagWhere the ID comes from
--content-spot-idtdx ps p13n personalization list <campaign>
--creative-asset-idtdx ps p13n creative-asset create --html-file <file>
--audience-idtdx ps p13n show <campaign>

Arguments

ArgumentDescription
campaignCampaign name or ID. Optional — uses p13n_campaign context when omitted.

Options

OptionDescription
--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, --yesSkip the confirmation prompt
--format <fmt>json, jsonl, tsv, or table (default)
--output <path>Write to a file — see Writing output to a file

Resolution order

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.

Default personalizations

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 audience

Output

The 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 42

The 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 42

Every line above goes to stderr, so --format json puts nothing but the personalization on stdout.

Errors

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 wrongNext step
No campaign with that IDtdx ps p13n list
Audience not assigned to the campaigntdx ps p13n show <campaign>
Content spot not in this accounttdx ps p13n personalization list <campaign>
Creative asset not in this accounttdx ps p13n creative-asset create --html-file <file>
A personalization already existstdx 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 delete followed by a fresh create — the error names the delete to run.

Examples

# 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 -y

personalization delete

Remove 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.

Arguments

ArgumentDescription
<id>Personalization ID, from personalization list

The ID is an opaque base64 token. It cannot be constructed or guessed, only listed.

Options

OptionDescription
--campaign-id <id>Campaign the personalization belongs to. Required — see below.
-y, --yesSkip the confirmation prompt
--format <fmt>json, jsonl, tsv, or table (default)
--output <path>Write to a file — see Writing output to a file

--campaign-id is always required

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.

Output

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-banner

After 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 7

The 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 42

Everything 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.

Errors

What is wrongNext step
No campaign with that IDtdx ps p13n list
The campaign has no personalization with that IDtdx ps p13n personalization list <campaign>
The personalization listing stopped earlyThe record may still exist; nothing was deleted
No --campaign-idPass 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 42

The 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.

Examples

# 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 -y

Session context

Pin 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 again

tdx 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.