---
title: "The one-time, install-wide admin setup statements for the shared dispatcher, plus this schedule's observed task state — the manual fallback to one-click Activate."
method: GET
path: "/v1/dashboards/{id}/email-schedules/{sid}/setup"
tags: ["dashboards"]
---

# The one-time, install-wide admin setup statements for the shared dispatcher, plus this schedule's observed task state — the manual fallback to one-click Activate.

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

## Request

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

**This route answers `404` `feature-disabled` only when the surface is
off.** All seven `/v1/dashboards/{id}/email-schedules` verbs share the
`editionCapabilities.unattendedSchedules` cell, and DVT-4490 ships it TRUE
wherever `emailSchedule` is true — the normal Snowflake native-app case —
so a shipped install answers `200` here, not `404`. The `404` survives for
exactly two operator-facing reasons: `DVT_UNATTENDED_SCHEDULES_ENABLED=false`
set on BOTH dvt-api and dvt-engine (an explicit opt-out, not the shipped
default), or an edition where `emailSchedule` itself is false. Stored rows
are unaffected either way — this was always a switch, not a migration.
Clients must gate on the capability cell rather than assume a fixed answer
from this route's existence. `POST /v1/dashboards/{id}/email` ("Email me
this report now") is unaffected by this cell and works regardless.

Returns the statements an admin runs in THIS account (DVT-4300), plus what dvt
has observed about the resulting task. Requires `dashboard:write` — it is the
instruction sheet for pointing a recurring data egress at a recipient list,
which is the same bar ADR-0051 §3 sets for managing a schedule at all.

**DVT-4796: `adminSetupSql` is now the install-wide "Admin setup (one time)"
dispatcher block** — the same text on every schedule's response in this app, not
rendered per schedule. It creates and owns the ONE shared `dispatcherTaskName`
task every activated schedule in the install fires through; `createTaskSql` is
retired (`""`, deprecated — see the field's own description) because there is no
longer a per-schedule `CREATE TASK` to run. `grantsSql` carries the identical
text for compatibility. `dropTaskSql` and `legacyTaskName` are non-empty/non-null
only for a schedule still on the legacy per-report task (`taskDriver == "task"`);
a schedule that has never been activated, or is already on the shared dispatcher,
has nothing to drop.

**dvt does not create the dispatcher task, and cannot.** It lives in the
consumer's own database, owned by a Snowflake role their admin chooses
(`declaredRole`), and that is exactly what gives an unattended run a real
identity to read the data as — ADR-0069 declined to give dvt a standing
`IMPERSONATE ON USER` grant, so the identity belongs to the customer instead.
Until an admin runs these statements no schedule in the install can fire.

`taskState` is a record of OBSERVATION, not a state dvt controls:

- `absent` — dvt has seen no run. This covers BOTH "nobody created the task" and
  "an admin created it but the first occurrence has not come round yet"; dvt
  cannot tell those apart. Calling this route does NOT change it.
- `confirmed` — a run actually reached dvt. Only a real run sets this, so a
  customer who copies the SQL and never pastes it is never told their reports
  are sending.
- `declared` is reserved for a future explicit affordance and is never returned
  today; nothing sets it.

dvt cannot list, edit, disable or drop a task it does not own; deleting the
schedule here does not remove the task, which is why `dropTaskSql` is returned.

## Query parameters

- `warehouse` string

## Response `200`

The generated statements and the observed task state.

- object
  - `data` EmailScheduleSetup, required: **Returned wherever `editionCapabilities.unattendedSchedules` is true** — the normal Snowflake native-app case since DVT-4490. The route that produces this object answers `404` `feature-disabled` only when an operator has set `DVT_UNATTENDED_SCHEDULES_ENABLED=false` on both dvt-api and dvt-engine, or on an edition where `emailSchedule` itself is false; gate on the capability cell rather than assume a fixed answer. The statements a Snowflake admin runs to make one email schedule fire on its own, plus what dvt has observed about the resulting task (DVT-4300). Since DVT-4796 `adminSetupSql`/`grantsSql` is an install-wide, one-time dispatcher block — the same text on every schedule's response in this app, not rendered per schedule — returned ready to paste, so clients must render it verbatim rather than reformatting it.
    - `appName` string, required: This install's application (database) name, e.g. `DVT_APP_3191`.
    - `scheduleId` string, uuid, required
    - `taskName` string, required: DVT-4796: the LEGACY per-schedule task name, derived deterministically from the schedule id so re-pasting `dropTaskSql` targets the same task rather than missing a second one silently still firing the same report. Superseded by the shared `dispatcherTaskName` for every schedule on the dispatcher; present on every response for backward compatibility, but a schedule already on the shared dispatcher has no task of this name at all.
    - `cron` string, required: The schedule's 5-field cron. The task's SCHEDULE must match it.
    - `timezone` string, required: IANA timezone the cron is evaluated in.
    - `grantsSql` string, required: DVT-4796: kept for compatibility, identical byte-for-byte to `adminSetupSql` — see that field. Prefer `adminSetupSql`; this alias may be removed in a future major version.
    - `adminSetupSql` string, required: The one-time, install-wide "Admin setup" block: `CREATE ROLE` `declaredRole`, the `dvt_report_task` application role grant, `EXECUTE TASK`, `EXECUTE MANAGED TASK` (serverless task), warehouse usage granted to the app, the database the dispatcher task lives in, the data-read grants (commented, placeholder-filled), and the `CREATE TASK dispatcherTaskName ... RESUME` that creates and starts the ONE shared dispatcher task the whole install's activated schedules fire through — plus a separate "update the dispatcher" block to run on a dvt upgrade that ships a new dispatcher task body. Run once per account, not per schedule; the same text is returned for every schedule in this app.
    - `createTaskSql` string, required: DEPRECATED (DVT-4796) — always `""`. The dispatcher task is created once, install-wide, by `adminSetupSql`'s `CREATE TASK dispatcherTaskName` statement, not per schedule, so there is no longer a per-schedule `CREATE TASK` to render. Kept in the response only so existing clients built against DVT-4300 do not see a missing key.
    - `dropTaskSql` string, required: Non-empty only while this schedule still sends through its own legacy per-report task (`taskDriver == "task"`, `legacyTaskName` non-null) — how to retire that task AFTER activating the schedule onto the shared dispatcher (never before: dropping first leaves the schedule with nothing sending it). Once Activate succeeds this field goes `""` and Activate's `retiredTaskName` carries the name. `""` for a schedule that has never been activated, or is already on the shared dispatcher: dvt does not own the legacy task either way, so deleting the schedule in dvt does NOT drop it on its own.
    - `dispatcherTaskName` string, required: DVT-4796. The one, install-wide task name every consumer's account holds at most one of (`DVT_SCHEDULES.PUBLIC.DVT_REPORT_DISPATCHER`) — present on every response regardless of this schedule's own `taskDriver`, since Activate always moves a schedule onto this shared task.
    - `declaredRole` string, required: DVT-4796. The Snowflake role the consumer's own dispatcher task is owned by and runs as for this org, chosen by that org's admin (never dvt) — an admin's own declared role if one has been set, else the same default `adminSetupSql` itself falls back to. Reflects the exact value `adminSetupSql` was rendered with, and the role a dispatcher-driven send's mail footer discloses.
    - `legacyTaskName` string: DVT-4796. Set only while this schedule still sends through its own per-report task (`taskDriver == "task"`) — the fully qualified name `dropTaskSql`'s `DROP TASK` statement names. ABSENT (the key itself omitted, not JSON null) for a schedule that has never been activated, or is already on the shared dispatcher.
    - `taskState` 'absent' | 'declared' | 'confirmed', required: **Live wherever `editionCapabilities.unattendedSchedules` is true** (DVT-4490) — the normal Snowflake native-app case; see the schema's own description for the two operator-facing ways it can still be off. What dvt has OBSERVED, not what it controls. Only a real run arriving sets `confirmed`; everything else is `absent` or `declared`. `absent` therefore covers BOTH "nobody has set this up" and "an admin has run the statements but the first occurrence has not come round yet" — dvt cannot distinguish them, because it does not own the task and cannot look for it. Do not render `absent` as "not set up". Two mechanisms can drive this value (DVT-4794, ADR-0069 Amendment 5), matching ExportSchedule.taskState exactly (this endpoint's statements are the LEGACY path only, but the underlying column, and therefore this field, is shared): `declared` means either `POST .../activate` PINNED this schedule under the shared dispatcher — a row write, no Snowflake statement, `taskOwnerRole` left null under patch 10 (a dispatcher-driven send has no consumer-owned task to protect) — or a LEGACY consumer-owned task (one-click activation) has run the grant + `CREATE TASK` statements, with `taskOwnerRole` known once a run confirms it. Calling THIS endpoint (the setup GET) does NOT set it either way: a read that recorded a claim about a human action would be manufacturing evidence. Only `POST .../activate` and a real run do.
    - `taskOwnerRole` string, nullable: The Snowflake role this schedule's task actually ran as, recorded trust-on-first-use from the first run that arrived — dvt cannot see a consumer's roles, so the first run is the only trustworthy source. Once pinned, a run arriving under a different role is refused; before the first run the value is null and nothing is compared. In the native app the run's role is unobservable (`INVOKER_ROLE()` reports the app name, DVT-4641), so it is never pinned from a run and a run is not refused on it. A scheduled report's rows are read by THIS role, not by the schedule's creator, so a report can contain rows its creator cannot see interactively — and vice versa. Null until the first run.
    - `taskLastSeenAt` string, date-time, nullable: When that task last called in. Null means never.
    - `lastRunAt` string, date-time, nullable
    - `lastOutcome` 'sent' | 'failed' | 'unconfirmed' | 'not-sent-timeout' | 'not-sent-failed' | 'not-sent-too-large' | 'not-sent-not-attempted', nullable: The most recent fire's outcome, as one of SEVEN values with the same vocabulary, and the same meanings and caveats as `ExportSchedule.lastOutcome`: the first six are the frozen `dashboard.email` audit spellings (DVT-4213) and `not-sent-not-attempted` is the schedule field's own seventh value. Null until the schedule has a stamped run; on this route null is sent explicitly (the key is always present). A run refused for a caller-scoped reason (401/403/404) does not update it. Clients must map these to human words rather than showing them raw.
  - `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 this route's description). The two are deliberately the same status: with the feature off this route does not exist as far as a caller is concerned, and it must not answer differently for a real schedule id than for an invented one. The `feature-disabled` Problem `type` is what distinguishes them when the feature is the reason.
- `409`: Either this install is not a packaged Snowflake native app, so there is no application to qualify the generated statements with (`email-not-configured`), or this org's stored dispatcher role (DVT-4796) cannot be safely rendered as a Snowflake identifier (`dispatcher-role-invalid`) — the `type` distinguishes the two. The second is reachable only if an unsafe declared role has already been stored for this org; no dvt write path stores one today. Contact support to correct the stored role.
- `422`: `warehouse` does not match the Snowflake identifier grammar (`^[A-Za-z_][A-Za-z0-9_$]{0,254}$`, DVT-4490), or contains two consecutive `$` characters — refused even though the grammar alone admits a lone `$` anywhere after the first character, because an accepted value is spliced unquoted into the generated admin setup SQL, where `"$$"` could close a dollar-quoted block early (DVT-4796 r3). Problem `type` is `https://docs.dvt.dev/errors/validation-failed`, and the failing value is echoed in a `warehouse` extension field.
- `503`: This org's declared dispatcher role (DVT-4796) could not be read, so the admin setup statements cannot be shown safely (`dispatcher-state-unavailable`). Never rendered with a guessed role — a state-store read failure is a real `503`, not a disclosure page that might name the wrong role. Try again shortly.

---

[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)
