# 11. App-owned, backend-executed tasks carry their marker in `task_source`

Date: 2026-07-08

## Status

Accepted

## Context

Bulk document download follows the app-created-task convention (ADR-0008): Next.js
inserts a `tasks` row whose `inputs` column carries the selection config, then hands
the backend only `{ task_id }`. Unlike a Modelling run — where the distributor
receives the whole config in the dispatch payload and never looks at the task again —
the document worker gets *only* the id, re-reads the `tasks` row, and resolves the
selection from `inputs`. It therefore needs a durable, in-row way to recognise "this
is a bulk-document-download task I'm allowed to act on" before it assembles anything.

Modelling runs put that marker in `service` (`modelling_run`) and leave `task_source`
human-readable (`"Modelling run – 2 scenarios"`). Reusing `service` here would work,
but it invites a future engineer to "unify" the two conventions and move the marker —
silently breaking the worker's guard.

## Decision

- **The marker is `task_source = "app:bulk_document_download"`** — a single, stable
  string the worker guards on. The `app:` prefix denotes an app-created task (as
  opposed to a backend-created one). It is engineer-facing only (Settings → Logs), so
  a machine-shaped string is acceptable; no friendly-label mapping is added.
- **`service` is left null.** For a task whose marker already lives in `task_source`,
  a second marker in `service` is redundant and can drift. In-app status polling
  scopes by `id = task_id AND source_id = <portfolio> AND task_source =
  "app:bulk_document_download"`.
- **Portfolio attachment is unchanged and uniform:** `source = "portfolio_id"`,
  `source_id = <portfolio_id>`. This is what the Portfolio Logs page filters on, so
  the run shows up there for free — the same as every other app-owned task.

## Consequences

- Do **not** move this marker into `service` to match Modelling runs. The asymmetry is
  deliberate: the marker lives where the executor reads it — `service` for a
  payload-fed distributor, `task_source` for a task-re-reading worker.
- A large selection never travels in an HTTP body: it lives in `inputs`, and the only
  thing dispatched is `task_id` (ADR-0008 lineage).
- Future app-owned, backend-executed tasks should follow this shape:
  `task_source = "app:<name>"`, config in `inputs`, `source`/`source_id` for scoping.
