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

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

## Commands

| Command | Description |
|  --- | --- |
| [`list`](#list) | List campaigns for a parent segment |
| [`show`](#show) | Show detail for a single campaign |
| [`launch`](#launch) | Launch a Personalization Studio campaign |
| [`unlaunch`](#unlaunch) | Unlaunch a Personalization Studio campaign |
| [`creative-asset show`](#creative-asset-show) | Show the HTML and CSS of one creative |
| [`creative-asset create`](#creative-asset-create) | Create a creative from local HTML/CSS |
| [`creative-asset update`](#creative-asset-update) | Replace a creative's HTML and/or CSS |
| [`personalization list`](#personalization-list) | List a campaign's personalizations |
| [`personalization create`](#personalization-create) | Wire a creative to an audience and spot |
| [`personalization delete`](#personalization-delete) | Remove a personalization from a campaign |


### Two views of a campaign's personalizations

[`personalization list`](#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`](#show) 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.

```bash
# 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

```bash
# 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.

```bash
tdx ps p13n list [name] [options]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `name` | Parent segment name (optional — uses context if omitted) |


### Options

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


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

```bash
# 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.

```bash
tdx ps p13n show [campaign] [options]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `campaign` | Campaign name or ID (optional — uses context if omitted) |


### Options

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


### 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`](#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

```bash
# 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.

```bash
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

| Argument | Description |
|  --- | --- |
| `campaign` | Campaign name or ID (optional — uses context if omitted) |


### Options

| 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`](#resolution-order).

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

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

```bash
# 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`](#launch) restores it.

```bash
tdx ps p13n unlaunch [campaign] [options]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `campaign` | Campaign name or ID (optional — uses context if omitted) |


### Options

| 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`](#resolution-order).

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

```bash
# 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.

```bash
tdx ps p13n creative-asset show <id> [--full]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `<id>` | Creative asset ID (numeric) |


### Options

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

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

```bash
# 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.

```bash
tdx ps p13n creative-asset create --html-file <path> [--css-file <path>] [-y]
```

### Options

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

### 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`](#personalization-list) shows which personalizations point at an asset
once the wiring is in place.

### Examples

```bash
# 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.

```bash
tdx ps p13n creative-asset update <id> [--html-file <path>] [--css-file <path>] [-y]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `<id>` | Creative asset ID (numeric) |


### Options

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

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

```bash
# 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.

```bash
tdx ps p13n personalization list [campaign]
```

### Arguments

| Argument | Description |
|  --- | --- |
| `[campaign]` | Campaign name or ID. Falls back to the session context if omitted |


### Options

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


### 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`](#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`](#show) 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

```bash
# 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`](#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>` |


### Arguments

| Argument | Description |
|  --- | --- |
| `campaign` | Campaign name or ID. Optional — uses `p13n_campaign` context when omitted. |


### Options

| 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](#writing-output-to-a-file) |


### Resolution order

The same as [`personalization list`](#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`](#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`](#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 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 delete`](#personalization-delete) followed by a fresh create
— the error names the delete to run.


### Examples

```bash
# 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.

```bash
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

| Argument | Description |
|  --- | --- |
| `<id>` | Personalization ID, from [`personalization list`](#personalization-list) |


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

### Options

| 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](#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`](#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`](#personalization-list) emits, so
`jq -r .contentSpotId` works the same either way.

### Errors

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

```text
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

```bash
# 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:

```bash
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`](#personalization-delete) requires
`--campaign-id` every time, because a stale pin would aim a delete at a campaign you never looked
at.