---
title: "This schedule's recent run history, including skipped `dispatcher-stale` runs (DVT-4797)."
method: GET
path: "/v1/dashboards/{id}/email-schedules/{sid}/runs"
tags: ["dashboards"]
---

# This schedule's recent run history, including skipped \`dispatcher-stale\` runs (DVT-4797).

`GET /v1/dashboards/{id}/email-schedules/{sid}/runs`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/dashboards/{id}/email-schedules/{sid}/runs`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Newest-fired-first history of this schedule's unattended occurrences —
one entry per `export_runs` row, each the outcome of one quantized cron
occurrence the consumer's dispatcher task offered. A manual **Run
now** does NOT appear here — it sends through the shared interactive
send core directly and writes no `export_runs` row, so it has no
history entry on this route. An occurrence the dispatcher offers more
than 60 minutes after it should have fired is recorded as a skipped
run (`skipReason: dispatcher-stale`) rather than run late — see
`docs/07-operations/unattended-scheduled-sends.md`, "Stale
occurrences are skipped, not sent".

Requires `dashboard:read` — a read of past outcomes is no more
sensitive than the schedule row itself, and nothing here is mutated or
widened. Same `404`-feature-disabled gate as the rest of this group.

Never includes recipient addresses or delivery details — only the
outcome, timing and (truncated) error text. For recipient-level
delivery status, read the `dashboard.email` audit row instead.

## Response `200`

The schedule's recent runs, newest first.

- object
  - `data` object, required
    - `runs` EmailScheduleRun[], required
      - `id` string, uuid, required
      - `status` 'pending' | 'rendering' | 'delivering' | 'done' | 'failed' | 'skipped', required
      - `skipReason` string: Present only on a `skipped` run — e.g. `dispatcher-stale` for an occurrence the dispatcher offered more than 60 minutes after it should have fired.
      - `firedAt` string, date-time, required: The quantized cron occurrence this run answers for (never "now").
      - `attempt` integer, required: The retry attempt number (starts at 1).
      - `error` string: This run's recorded failure text, truncated to 300 runes. Absent when the run has no recorded error.
      - `createdAt` string, date-time, required
      - `finishedAt` string, date-time: When this run reached a terminal state (`done`, `failed` or `skipped`). Absent while still `pending`/`rendering`/`delivering`.
  - `meta` Meta, required: Response metadata.
    - `requestId` string, required: Correlates with the X-Request-Id header.
    - `total` integer: Item count, present on collection responses.
    - `nextCursor` string, nullable: Opaque keyset-pagination cursor, present on paginated responses (e.g. GET /v1/dashboards) when more rows exist beyond this page. Pass it back as the `cursor` query param to fetch the next page; absent or null on the last page.
    - `roleResolveDegraded` boolean: Snowflake mode only (DVT-2260), on GET /v1/dashboards: present and true when the caller's live Snowflake role resolution failed or was unavailable — role-shared dashboards may be missing from this page. Omitted (not merely false) whenever resolution succeeded or the role-share arm does not apply (off snowflake mode, an org admin/owner, or off the ingress listener).

## Other responses

- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Either the schedule does not exist (or is not the caller's to read), or automatic sending is disabled on this deployment (`feature-disabled`, see `GET .../setup`'s description). Deliberately the same status for both, and identical for a real schedule id and an invented one.

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
