# Backend ask — resolve a `tag_ids` Run-filter key

**For:** the distributor (`POST /v1/modelling/trigger-run`)
**From:** the portfolio tagging system (app), ADR-0013 (extends ADR-0008)
**Status:** app side built; backend change outstanding

## What's needed

The modelling **Run filter** gains one optional key, `tag_ids` — a list of
portfolio Tag ids. The distributor already receives
`{ task_id, portfolio_id, scenario_ids, filters }` and resolves `filters`
against the shared Postgres. It must now also resolve `tag_ids`.

```jsonc
// dispatch payload (unchanged except the new optional filters key)
{
  "portfolio_id": 814,
  "scenario_ids": [12, 34],
  "filters": {
    "postcodes": ["B93 8SU"],       // existing
    "property_types": ["House"],    // existing
    "built_forms": ["Detached"],    // existing
    "tag_ids": [7, 3]               // NEW — optional
  }
}
```

## Resolution rule

Tags are a **direct membership** lookup, not the override→EPC precedence used for
type/built-form. A property matches `tag_ids` when it has a row in
`property_tag` for any of the ids (**any-of**). Combine with the other filter
dimensions the same way they combine today: **AND across dimensions, OR within**.

```sql
-- add to the property-matching WHERE, only when tag_ids is present:
p.id IN (
  SELECT pt.property_id FROM property_tag pt
  WHERE pt.tag_id IN (:tag_ids)
)
```

Tables (already migrated — see PR #387 / migration `0271`):

- `portfolio_tag(id, portfolio_id, name, colour, …)`
- `property_tag(property_id, tag_id)` — PK `(property_id, tag_id)`, index
  `ix_property_tag_tag` on `tag_id` for exactly this reverse lookup.

Ids that don't belong to the portfolio simply match nothing (no validation
needed). An absent/empty `tag_ids` means unconstrained, like the other keys.

## Reference implementation (must stay in lock-step)

The app's in-app **preview count** is the reference implementation ADR-0008
requires — the distributor's resolution must match it. See
`previewModellingRun` in `src/lib/modellingRuns/server.ts`: the tag clause is

```ts
conditions.push(
  sql`resolved.id IN (SELECT pt.property_id FROM property_tag pt
       WHERE pt.tag_id IN (${inList(args.filters.tagIds)}))`,
);
```

Divergence between the app preview and the distributor is defined as a bug
(ADR-0008).

## Until this lands

The run UI can show and preview a Tag selection (the app resolves the preview),
but a triggered run won't scope by tags until the distributor resolves
`tag_ids`. No contract-shape change is needed on either side beyond reading the
new key.
