Skip to content

Add admin-only cross-account aggregate mode to GET /api/tasks/runs #1818

Description

@sweetmantech

Tracking issue for adding an admin-only cross-account aggregate mode to the task-runs endpoint. Today the "recent task runs" tab is per-account only; there is no way for an admin to see the latest runs across all accounts in one call. Feasibility is already proven against the live Trigger.dev API (see Context).

Goal

An admin can fetch the latest task runs across all accounts from GET /api/tasks/runs, with each run attributed to its owning account — instead of being limited to a single account_id. The per-account behavior the UI already uses stays unchanged.

Touch points (all in api):

  • api/lib/trigger/fetchTriggerRuns.ts — Trigger.dev fetch + TriggerRunFilter type
  • api/lib/tasks/validateGetTaskRunQuery.ts — auth + query validation
  • api/lib/tasks/getTaskRunHandler.ts — list handler

Consumer for reference (out of scope here): chat/lib/tasks/getTaskRuns.tschat/hooks/useTaskRuns.tschat/components/TasksPage/*.

PRs (updated 2026-06-29)

PR Item Base State
TBD fetchTriggerRuns: allow no-filter (all-accounts) fetch test ⏳ not opened
TBD validateGetTaskRunQuery: admin-only scope=all list mode test ⏳ not opened
TBD getTaskRunHandler: aggregate fetch + per-run account attribution test ⏳ not opened

No PRs opened yet. The three items are interdependent and only end-to-end testable together, so they may ship as one api PR (base test) rather than three; kept as separate line items for review clarity. Merge path: api PRs target test, then testmain.

Context — feasibility verified

Task runs are Trigger.dev runs (the tasks flow), each tagged account:<id>. The current endpoint hard-scopes to one account via filter[tag]=account:<id>. Calling the same Trigger.dev endpoint without the tag filter (GET https://api.trigger.dev/api/v1/runs?page[size]=N) returns runs across all accounts, each still carrying its account:<id> tag for attribution.

Verified live on 2026-06-29: 137 task runs that day across 49 distinct accounts — 135 customer-prompt-task, 1 send-pulses-task, 1 pro-artist-social-profiles-scrape (136 COMPLETED / 1 QUEUED). Morning batch concentration: 09:00 UTC → 56, 10:00 UTC → 51. Trigger.dev caps page[size] at 100 and paginates by cursor (pagination.nextpage[after]), so a full-day sweep loops the cursor.

Open — admin aggregate mode (3 items; in implement order)

  • fetchTriggerRuns: support a no-filter (all-accounts) fetch.

    • Why: TriggerRunFilter is typed { "filter[tag]": string } | { "filter[schedule]": string } and the function always sets a filter[*] param, so there is no path to query runs across all accounts. (source: api/lib/trigger/fetchTriggerRuns.ts)
    • Fix: allow an empty filter ({} / make the filter optional); when empty, build the request with no filter[tag]/filter[schedule] param. Preserve page[size] and cursor pagination (page[after]).
    • Done when: fetchTriggerRuns({}, 20) issues a request with no filter[*] param and returns the latest 20 runs across all accounts — unit test asserts the URL omits the filter param.
  • validateGetTaskRunQuery: add an admin-only scope=all list mode.

    • Why: list mode always resolves to a single accountId; non-admins must stay scoped to their own/org accounts. There is no sanctioned way to request all accounts. (source: api/lib/tasks/validateGetTaskRunQuery.ts)
    • Fix: accept a sentinel (scope=all) that, only when checkIsAdmin(authResult.accountId) is true, returns a new { mode: "list-all", limit }. A non-admin passing it gets 403. Existing account_id=<id> and self/org paths unchanged.
    • Done when: admin Bearer + ?scope=all → resolves to list-all; non-admin + ?scope=all403; normal per-account requests behave exactly as before — covered by tests.
  • getTaskRunHandler: aggregate fetch + per-run account attribution.

    • Why: the handler only services single-account list mode. (source: api/lib/tasks/getTaskRunHandler.ts)
    • Fix: in list-all mode call fetchTriggerRuns({}, limit); parse each run's account:<id> tag into an explicit field (e.g. accountId) so the response attributes every run to an account. Return { status: "success", runs }.
    • Done when: admin GET /api/tasks/runs?scope=all&limit=20 returns the latest 20 runs across accounts, each with its accountId — preview result matches Trigger.dev ground truth (e.g. 2026-06-29: 137 runs / 49 accounts).

Out of scope

  • Chat UI consumer (an admin "all accounts" view in TasksPage) — separate follow-up. These three items are the api enablement only.
  • Joining runs to their delivered emails (run → email value view) — possible future follow-up.

Architecture decisions

  • Task runs live in Trigger.dev, tagged account:<id>. The /api/tasks/runs list endpoint is a thin proxy over Trigger.dev's GET /api/v1/runs (TRIGGER_SECRET_KEY, set in Vercel). Account attribution rides on the run tag, so aggregate mode reads the tag back rather than needing a new store.
  • Aggregate is strictly admin-gated. Returning all accounts' runs is sensitive; reuse the existing checkIsAdmin check — no new auth surface.

Source references

  • Endpoint chain: chat/lib/tasks/getTaskRuns.ts (client, GET /api/tasks/runs?account_id=&limit=) → api/app/api/tasks/runs/route.tsapi/lib/tasks/getTaskRunHandler.tsapi/lib/trigger/fetchTriggerRuns.ts.
  • Trigger.dev Management API: GET https://api.trigger.dev/api/v1/runs (filters: filter[tag], filter[schedule]; page[size] ≤ 100; cursor page[after]).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions