# Customize a user sequence

> example_sequence.json fields, ActionName indices, history padding to history_seq_len, and --top_k_retrieval / --top_k_display knobs.

- Repository: xai-org/x-algorithm
- GitHub: https://github.com/xai-org/x-algorithm
- Human docs: https://grok-wiki.com/public/docs/xai-org-x-algorithm-23c09c39074c
- Complete Markdown: https://grok-wiki.com/public/docs/xai-org-x-algorithm-23c09c39074c/llms-full.txt

## Source Files

- `phoenix/run_pipeline.py`
- `phoenix/README.md`
- `phoenix/runners.py`
- `phoenix/recsys_model.py`
- `home-mixer/query_hydrators/scoring_sequence_query_hydrator.rs`
- `home-mixer/query_hydrators/retrieval_sequence_query_hydrator.rs`

---

---
title: "Customize a user sequence"
description: "example_sequence.json fields, ActionName indices, history padding to history_seq_len, and --top_k_retrieval / --top_k_display knobs."
---

`phoenix/run_pipeline.py` is the only local entry point that encodes a user action sequence from JSON. It reads `{artifacts_dir}/example_sequence.json` (or `--sequence_file`), left-pads the `history` array to the retrieval checkpoint's `history_seq_len`, hashes `user_id` / `post_id` / `author_id` into a `RecsysBatch`, retrieves `--top_k_retrieval` posts from `sports_corpus.npz`, and prints `--top_k_display` rows of the ranked table.

`phoenix/run_ranker.py` and `phoenix/run_retrieval.py` do **not** read this file. They build a random `create_example_batch` and are not the customization surface.

<Note>
The shipped `example_sequence.json` lives inside the Git LFS archive `phoenix/artifacts/oss-phoenix-artifacts.zip`. Extract it before editing. The pointer file is not a zip.
</Note>

## Prerequisites

<Steps>
<Step title="Extract artifacts">
The sequence file is created by unzipping the Phoenix archive:

```bash
cd phoenix
unzip artifacts/oss-phoenix-artifacts.zip -d artifacts/
```

Expected path: `artifacts/oss-phoenix-artifacts/example_sequence.json`. The shipped sample is three sports posts (NFL, NBA, NHL) that the user favorited and dwelled on.
</Step>
<Step title="Confirm the pipeline still runs">

```bash
uv run run_pipeline.py --artifacts_dir artifacts/oss-phoenix-artifacts
```

Success is a `PIPELINE RESULTS — User <id>` table. See [Run the inference pipeline](/run-inference-pipeline) for checkpoint layout and [Installation](/installation) if the zip is still an LFS pointer.
</Step>
</Steps>

## Sequence file schema

The loader does `json.load` and then requires two top-level keys. Extra keys are ignored. `history_product_surface` and `history_continuous_actions` are **not** read from JSON; the pipeline zeros product-surface IDs and omits continuous channels.

```json
{
  "user_id": 1234567890123456789,
  "history": [
    {
      "post_id": 1900000000000000001,
      "author_id": 44196397,
      "actions": {
        "1": 1.0,
        "11": 1.0
      }
    }
  ]
}
```

<ParamField body="user_id" type="integer" required>
Hashed with the retrieval and ranker `hash_params` into `RecsysBatch.user_hashes`. Value `0` is reserved as padding by `_hash_ids` and produces a masked user token.
</ParamField>

<ParamField body="history" type="array" required>
Ordered list of interaction objects. Only the prefix `history[:history_seq_len]` is encoded. Later items are dropped.
</ParamField>

<ParamField body="history[].post_id" type="integer" required>
Item ID hashed into `history_post_hashes`. Value `0` stays a pad slot; `block_history_reduce` treats `history_post_hashes[:, :, 0] == 0` as padding.
</ParamField>

<ParamField body="history[].author_id" type="integer" required>
Author ID hashed into `history_author_hashes`. Same pad rule as `post_id`.
</ParamField>

<ParamField body="history[].actions" type="object">
Map of `ActionName` index (JSON string key) to a float value. Missing object is treated as `{}`. Keys with `int(key) >= num_actions` are dropped. Typical present value is `1.0`.
</ParamField>

<RequestExample>
```bash
uv run run_pipeline.py \
  --artifacts_dir artifacts/oss-phoenix-artifacts \
  --sequence_file /tmp/my_sequence.json \
  --top_k_retrieval 200 \
  --top_k_display 30
```
</RequestExample>

<ResponseExample>
```text
PIPELINE RESULTS — User 1234567890123456789
History: 1 items | Corpus: <N> posts
Retrieved top 200 → Ranked by engagement model
```
</ResponseExample>

The `History:` line prints `len(history)` from the JSON, not the padded tensor length.

## ActionName indices

`actions` keys are proto `ActionName` integers, the same indices `run_pipeline.py` uses for both history encoding and the printed ranker columns:

| Index | Constant | Comment in `run_pipeline.py` | Role in the demo |
|---|---|---|---|
| `1` | `IDX_FAV` | `SERVER_TWEET_FAV` | History bit + weighted score `× 1.0` + `Fav` column |
| `4` | `IDX_REPLY` | `SERVER_TWEET_REPLY` | History bit + weighted score `× 0.5` + `Reply` column |
| `5` | `IDX_QUOTE` | `SERVER_TWEET_QUOTE` | History bit only. Defined, not used in the demo weighted sum |
| `6` | `IDX_RT` | `SERVER_TWEET_RETWEET` | History bit + weighted score `× 0.3` + `RT` column |
| `11` | `IDX_DWELL` | `CLIENT_TWEET_RECAP_DWELLED` | History bit + weighted score `× 0.2` + `Dwell` column |
| `13` | `IDX_VQV` | `CLIENT_TWEET_VIDEO_QUALITY_VIEW` | History bit + `VQV` column. Not in the demo weighted sum |

`num_actions` comes from `retrieval/config.json` (published mini table: **19**). Index `0` is never written by the documented sample keys.

The action embedding is a signed multi-hot: `(2 * actions - 1)`, then zeroed when the entire vector is `0` (a pad or empty `actions` object). A real history item with only `{"1": 1.0}` therefore contributes `+1` at favorite and `-1` at every other action slot.

<Warning>
Do not reuse `runners.ACTIONS` positions (`favorite_score` at 0, `reply_score` at 1, …) as `example_sequence.json` keys. That list is the `RecsysInferenceRunner` output layout for `run_ranker.py`, not the proto `ActionName` encoding. Full mapping lives on [Action indices](/action-indices).
</Warning>

## History padding and truncation

`hist_len` is `ret_cfg["history_seq_len"]` from `artifacts/.../retrieval/config.json`. The published mini-model table documents **127**. Dataclass defaults on `PhoenixModelConfig` / `PhoenixRetrievalModelConfig` (`128`) are not used once a checkpoint config is loaded.

```text
JSON history length N, encoder width H = history_seq_len

  history[:H] copied left-to-right
  remaining slots stay 0 (pad post_id, author_id, actions)

  N <= H :  [item0, item1, ..., itemN-1, 0, 0, ..., 0]
  N >  H :  [item0, item1, ..., itemH-1]   # suffix discarded
```

Pad slots hash to `0` and are masked out (`history_post_hashes[:, :, 0] != 0`). They do not attend as real history.

<Info>
This prefix keep is the opposite of a last-N trim. Put the interactions you want the model to see at the **front** of `history`, or pre-trim the array yourself.
</Info>

The same padded `history_post_ids` / `history_author_ids` / `history_actions` tensors are reused for retrieval and ranking. Retrieval and ranker hash functions are built separately from each checkpoint's `hash_params`.

## How the sequence becomes a RecsysBatch

After padding, `run_pipeline.py` builds:

| `RecsysBatch` field | Source |
|---|---|
| `user_hashes` | `hash_user([user_id])` |
| `history_post_hashes` | `hash_item(history_post_ids)` shaped `[1, H, num_item_hashes]` |
| `history_author_hashes` | `hash_author(history_author_ids)` |
| `history_actions` | `[1, H, num_actions]` float32 from `actions` |
| `history_product_surface` | zeros `[1, H]` |
| `candidate_*` | zeros at retrieval time; filled with retrieved IDs at ranking time |

Embeddings are looked up from the reconstructed unified table **before** the transformer. Candidate isolation (candidates attend to user + history + self only) is unchanged by sequence edits. See [Hash embeddings](/hash-embeddings) and [Candidate isolation](/candidate-isolation).

## CLI knobs

These flags change retrieval depth and printed rows. They do not change encoder width, `num_actions`, or the JSON schema.

<ParamField body="--artifacts_dir" type="path">
Default `./artifacts`. Must contain `retrieval/`, `ranker/`, and (unless overridden) `example_sequence.json` plus `sports_corpus.npz`.
</ParamField>

<ParamField body="--sequence_file" type="path">
Default `{artifacts_dir}/example_sequence.json`. Point this at an edited copy instead of overwriting the shipped sample.
</ParamField>

<ParamField body="--corpus_file" type="path">
Default `{artifacts_dir}/sports_corpus.npz`. Retrieved `post_id`s come from this corpus, not from `history`.
</ParamField>

<ParamField body="--top_k_retrieval" type="int">
Default `200`. Effective `TOP_K = min(top_k_retrieval, len(corpus_post_ids))`. Dot-product of the user representation against `candidate_representations`, then `argpartition` + sort. Ranking walks this list in chunks of `candidate_seq_len` (published mini table: **64**).
</ParamField>

<ParamField body="--top_k_display" type="int">
Default `30`. Effective `DISPLAY = min(top_k_display, TOP_K)`. Only the print loop is truncated; every retrieved candidate is still scored.
</ParamField>

<Tabs>
<Tab title="Deeper retrieval">

```bash
uv run run_pipeline.py \
  --artifacts_dir artifacts/oss-phoenix-artifacts \
  --sequence_file /tmp/my_sequence.json \
  --top_k_retrieval 500
```

Ranks 500 corpus hits (or the whole corpus if smaller). The table still shows 30 rows unless you also raise `--top_k_display`.
</Tab>
<Tab title="Longer table">

```bash
uv run run_pipeline.py \
  --artifacts_dir artifacts/oss-phoenix-artifacts \
  --sequence_file /tmp/my_sequence.json \
  --top_k_display 50
```

Prints 50 ranked rows from the default 200 retrieved candidates.
</Tab>
</Tabs>

Demo rank order is **not** the production `WeightedScorer` table. The pipeline uses:

```text
weighted = P(fav)*1.0 + P(reply)*0.5 + P(rt)*0.3 + P(dwell)*0.2
```

Quote and VQV probabilities are printed and can be driven by history bits, but they do not move this demo rank. Production weights are on [Multi-action scoring](/multi-action-scoring) and [Scorers and weights](/scorers-and-weights).

## Edit and verify

<Steps>
<Step title="Copy the shipped sequence">

```bash
cp artifacts/oss-phoenix-artifacts/example_sequence.json /tmp/my_sequence.json
```
</Step>
<Step title="Set user_id and history">
Keep `user_id` non-zero. Each history object needs integer `post_id` and `author_id` plus an `actions` map using the `ActionName` keys above. Multiple keys on one item are allowed (`"1"` and `"11"` together is the shipped “liked and dwelled” pattern).
</Step>
<Step title="Stay inside encoder width">
If `len(history)` exceeds `history_seq_len` from `retrieval/config.json`, only the prefix is encoded. Trim or reorder first.
</Step>
<Step title="Run against the sports corpus">

```bash
uv run run_pipeline.py \
  --artifacts_dir artifacts/oss-phoenix-artifacts \
  --sequence_file /tmp/my_sequence.json \
  --top_k_retrieval 200 \
  --top_k_display 30
```
</Step>
<Step title="Check the banner">
Confirm `User` matches `user_id` and `History: N items` matches the JSON array length. Then inspect `Fav` / `Reply` / `RT` / `Dwell` / `VQV` and the `Score` column. Changing only `--top_k_display` must not change the first printed rows; changing `--top_k_retrieval` can, because a different candidate pool is ranked.
</Step>
</Steps>

History IDs do not have to appear in `sports_corpus.npz`. Corpus posts are retrieved by embedding similarity; history only conditions the user tower.

## Production analog

Local JSON replaces two Home Mixer query hydrators that `PhoenixCandidatePipeline` registers first:

| Local field | Production field on `ScoredPostsQuery` | Hydrator | Consumer |
|---|---|---|---|
| Same `history` tensor for both stages | `retrieval_sequence` (+ `columnar_retrieval_sequence`) | `RetrievalSequenceQueryHydrator` | `PhoenixSource` (errors with `PhoenixSource: missing retrieval_sequence`) |
| Same `history` tensor for both stages | `scoring_sequence` (+ `columnar_scoring_sequence`) | `ScoringSequenceQueryHydrator` | `PhoenixScorer` (returns default candidates if `scoring_sequence` is `None`) |

Both hydrators call `UserActionAggregationClient.fetch_aggregated_sequence` with `UAS_WINDOW_TIME_MS`. Retrieval uses `MaxSeqLengthRetrieval` and aggregation type `PhoenixRetrievalAggregationType` (fallback `Dense`). Scoring uses `MaxSeqLengthScoring` and `PhoenixAggregationType` (fallback `DenseWithNotInterestedIn`). Those params live in unpublished `home-mixer` crates; this checkout cannot fetch a live sequence.

<Warning>
Editing `example_sequence.json` does not change For You / Scored Posts gRPC behavior. Production scoring still requires a hydrated `scoring_sequence`. See [Assemble a Home Mixer request](/assemble-home-mixer-request) and [Troubleshooting](/troubleshooting).
</Warning>

## Constraints and errors

| Condition | Behavior |
|---|---|
| Missing `--sequence_file` and no `{artifacts_dir}/example_sequence.json` | `FileNotFoundError` on `open` |
| JSON missing `user_id` or `history` | `KeyError` |
| History item missing `post_id` or `author_id` | `KeyError` |
| `user_id`, `post_id`, or `author_id` is `0` | Slot hashes to pad `0` and is masked |
| `len(history) > history_seq_len` | Silent prefix truncate |
| Action key `>= num_actions` | Silent drop |
| Empty `actions` on a real `post_id` | Action embedding zeroed (`jnp.any` mask) |
| `--top_k_retrieval` larger than the corpus | Capped to corpus length |
| `--top_k_display` larger than `TOP_K` | Capped to `TOP_K` |
| Artifacts still an LFS pointer | Unzip fails; see [Troubleshooting](/troubleshooting) |

`run_pipeline.py` does not validate snowflake ID ranges, topic membership, or that history posts exist in the corpus.

## Next

<CardGroup>
<Card title="Run the inference pipeline" href="/run-inference-pipeline">
Load retrieval and ranker checkpoints, encode the sequence, retrieve from `sports_corpus.npz`.
</Card>
<Card title="Action indices" href="/action-indices">
`ActionName` vs `IDX_*` vs `runners.ACTIONS` vs scorer weight keys.
</Card>
<Card title="Multi-action scoring" href="/multi-action-scoring">
Demo weighted sum versus production `WeightedScorer` / `RankingScorer`.
</Card>
<Card title="Phoenix model configuration" href="/phoenix-model-configuration">
`history_seq_len`, `candidate_seq_len`, `num_actions`, and published `config.json` keys.
</Card>
</CardGroup>
