# Blocklists

{% tabs %}
{% tab title="End users" %}
Blocklists are why an item can be planned but still not get added.

CrossWatch blocks retries when it has evidence an item is a bad idea.

#### What can block an add

* **Tombstones**: it was deleted recently (mostly two-way safety).
* **Blackbox**: it keeps failing, so it’s cooled down.
* **Unresolved**: it failed to apply and is parked.

#### If an item won’t add

1. Check if it was deleted recently (tombstones).
2. Check if it’s in blackbox.
3. Check unresolved files for that destination.

{% hint style="info" %}
Watchlist does not use this blocklist path in current wiring. It relies on Phantom Guard.
{% endhint %}

Related:

* Watchlist add flapping: [Phantom Guard](/blueprint-architecture/orchestrator/phantom-guard.md)
* File locations: [State](/blueprint-architecture/orchestrator/state.md)
  {% endtab %}

{% tab title="Power users" %}
This doc explains the shared blocklist logic used to prevent planned ADDs from reattempting known-bad keys.

<details>

<summary>Implementation notes</summary>

**Code:** `cw_platform/orchestrator/_pairs_blocklist.py`\
**Inputs from:** `_tombstones.py`, `_unresolved.py`, `_blackbox.py`\
**Used by:** one-way and two-way (mostly non-watchlist features)

</details>

***

### What it does

`apply_blocklist(state_store, items, dst, feature, pair_key, emit, dbg)`

It filters a list of planned items (usually ADDs) by removing items that match any of:

1. Tombstones (pair-scoped)
2. Unresolved keys (destination-scoped, optionally cross-feature)
3. Blackbox keys (destination+feature scoped or pair-scoped, depending on config)

It returns a filtered list and emits a `blocked.counts` event with breakdowns.

***

### What it does *not* do

* It does not block removals (in current pipeline wiring).
* It does not apply PhantomGuard (watchlist uses that separately).
* It does not mutate state; it only reads.

***

### Inputs

#### `items`

A list of item dicts planned for writing.

Each item should have:

* canonical key derivable via `canonical_key(item)`
* `ids` dict if you want token-based matching to work well

#### `dst`

Destination provider name (string), e.g. `"SIMKL"`.

#### `feature`

Feature name (`watchlist`, `ratings`, `history`, `playlists`).

#### `pair_key`

Providers sorted and joined (e.g., `PLEX-SIMKL`). This is used for tombstones and optional blackbox pair scoping.

***

### How matching works

`apply_blocklist` uses `filter_with(tokens_to_block, items)` from `_tombstones.py`.

That function treats a token as matching an item if:

1. token == canonical key
2. token matches any ID token derived from `item["ids"]`
3. token matches a normalized title-year token:
   * `type|title:<lower>|year:<year>`

So even if canonical keys differ, a tombstone written as `tmdb:123` can still block an item whose canonical key is `imdb:...` as long as it includes `ids.tmdb = 123`.

***

### Blocklist sources

#### 1) Tombstones

Loaded via:

* `keys_for_feature(state_store, feature, pair=pair_key)`

Then TTL-filtered (by whoever calls this; two-way does this explicitly).

In current `apply_blocklist` usage:

* it typically blocks adds for items that have been deleted recently.

#### 2) Unresolved

Loaded via:

* `load_unresolved_keys(dst, feature, cross_features=True)`

This is meant to block adds for keys that keep failing to apply.

Caveat:

* writer writes `*.unresolved.pending.json`
* loader expects `*.unresolved.json` (see [Unresolved](/blueprint-architecture/orchestrator/unresolved.md))

So unresolved may under-block unless you have a promotion step.

#### 3) Blackbox

Loaded via:

* `load_blackbox_keys(dst, feature, pair=pair_key)`

This returns a set of blocked keys (cooldown entries).

Applied as tokens, so it can match by canonical key or ID tokens too.

***

### Output and observability

`apply_blocklist` emits:

* `blocked.counts`

Typical payload:

```json
{
  "dst": "SIMKL",
  "feature": "ratings",
  "pair_key": "PLEX-SIMKL",
  "blocked_total": 12,
  "blocked_by": {
    "tombstones": 5,
    "unresolved": 3,
    "blackbox": 4
  }
}
```

Debug mode may also emit:

* previews of blocked keys
* sample item titles/ids

***

### Where it’s used (current wiring)

#### One-way

* applied only for `feature != "watchlist"` (watchlist is excluded)
* applied primarily to planned ADDs

#### Two-way

* applied similarly for non-watchlist ADDs on each destination side

So if you expect watchlist adds to be blocked by blackbox/unresolved/tombstones via apply\_blocklist:

* they won’t (today). Watchlist relies on PhantomGuard + two-way tombstone logic.

***

### Operational notes

If an item “won’t add” and you want to know why:

1. Check tombstones for the feature + pair key
2. Check blackbox files for destination + feature
3. Check unresolved files for destination + feature

If you’re implementing a provider and want blocklists to work:

* ensure IDs are populated in items (`ids.tmdb`, `ids.imdb`, etc.)
* so token matching can hit tombstones even if canonical key differs

***

### Related pages

* Tombstone tokens: [Tombstones](/blueprint-architecture/orchestrator/tombstones.md)
* Failure cooldown: [Blackbox](/blueprint-architecture/orchestrator/blackbox.md)
* One-run failure memory: [Unresolved](/blueprint-architecture/orchestrator/unresolved.md)
* Where blocklists are applied: [One-way sync](/blueprint-architecture/orchestrator/one-way-sync.md), [Two-way sync](/blueprint-architecture/orchestrator/two-way-sync.md)
  {% endtab %}
  {% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.crosswatch.app/blueprint-architecture/orchestrator/blocklists.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
