---
title: "Update a dashboard (optimistic concurrency)."
method: PATCH
path: "/v1/dashboards/{id}"
tags: ["dashboards"]
---

# Update a dashboard (optimistic concurrency).

`PATCH /v1/dashboards/{id}`

## Request

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

Updates `title`, `visibility`, and/or the full `spec`. Requires the current `version`
for optimistic concurrency; a stale version returns 409. If a new spec is provided it is
re-validated before persistence.

## Query parameters

- `reason` string

## Request body

- PatchDashboardRequest: Provide `version` plus any subset of the updatable fields.
  - `version` integer, required: The current version, from the latest GET. Required for optimistic concurrency.
  - `title` string: A title-only update (no `spec`) sets this denormalized column without writing a dashboard revision. Because `spec.meta.title` is the source of truth, restoring any earlier revision will overwrite such a rename.
  - `visibility` 'private' | 'shared' | 'public'
  - `spec` object
  - `queryRole` string: Snowflake mode only (DVT-1388): pin the Snowflake role this dashboard's queries run under for every viewer, or pass an empty string to clear the pin. Requires share access (like a visibility change), and you can only pin a role your own Snowflake session holds (validated live against CURRENT_AVAILABLE_ROLES()). Unquoted role names only (letters, digits, _ or $ — canonicalized to upper case); reserved pseudo-roles (PUBLIC) are rejected. Omit the field to leave the pin unchanged. Outside snowflake mode the field is rejected with 400.
  - `changeSummary` string: Optional human-readable note describing this edit (DVT-1426) — e.g. "Swapped revenue bar for cohort heatmap per Maria's request". Human-authored, agent-authored, or — via dvt web's Save-confirmation modal (DVT-2686) — prefixed by a client-generated summary of the spec diff against the prior revision, with the author's typed note appended after ' — ' when one was supplied (the same composition rule as the server's own mergeReason, server/internal/repo/element.go) — truncated, or dropped entirely in favor of the note alone, when a long note leaves too little room for it. Other callers may of course send any string, or omit the field entirely. Recorded verbatim on the new revision and surfaced verbatim in the org activity feed; never summarized or transformed. Over 280 characters is rejected with 400.

## Response `200`

Dashboard updated.

- DashboardEnvelope
  - `data` Dashboard, required
    - `id` string, uuid, required
    - `folderId` string, uuid, nullable, required: The containing folder, or null when unfoldered. Set via PUT /v1/dashboards/{id}/folder.
    - `ownerId` string, uuid, required: The user who created the dashboard. Only the owner (or an org admin/owner) may move it between folders.
    - `title` string, required
    - `description` string: Optional human/agent-authored summary of the dashboard (from the dashboards.description column, falling back to spec meta.brief on list). Omitted when neither source is populated.
    - `visibility` 'private' | 'shared' | 'public', required
    - `queryRole` string: Snowflake mode only (DVT-1388): the optional pinned Snowflake role this dashboard's queries run under for every viewer (canonicalized to Snowflake's case fold). A viewer who does not hold the role gets a clean no-access query error — Snowflake enforces the pin, it can never elevate. Omitted when unpinned: each viewer's own default (primary) role is used, or, for a viewer reading via a role share, the shared role.
    - `version` integer, required: Optimistic-concurrency version. Pass this back on PATCH.
    - `schemaVersion` integer, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
    - `spec` object, nullable, required: The full dvt dashboard spec from the current revision snapshot. Always `null` on collection reads (`GET /v1/dashboards`, `GET /v1/me/favorites`), which omit the spec to stay cheap — fetch `GET /v1/dashboards/{id}` for it. Populated on single-dashboard reads and writes.
    - `governance` DashboardGovernance: Secret-free governance-disclosure summary for the connections this dashboard references (ADR-0012 §"Governance disclosure rule"). Present on GET responses when the auth-method kind can be resolved; carries ONLY the auth-method enum, never any credential material. Drives the shared-role notice in the UI.
      - `sharedRole` boolean, required: True when at least one referenced connection uses a service_* auth method (one shared service identity — all viewers see the same role's data). False for per-user/OAuth-backed dashboards, which display no notice.
      - `roleMapped` boolean, required: True when every shared-role source is role-mapped (the 2026-06-14 ADR-0012 amendment): the notice then reads "data shown as your mapped role" rather than "all viewers see the same data".
      - `connections` ConnectionGovernance[], required: Per-connection auth-method kind for each source the spec references.
        - `sourceId` string, required: The panel's sourceId exactly as the spec spells it — a connection id (matched case-insensitively) or name. A connection referenced both ways appears once per distinct reference.
        - `authMethod` 'service_secret' | 'service_keypair' | 'oauth_passthrough' | 'cloud_iam' | 'spcs_token', required: The connection's auth-method kind. Never any credential material.
        - `type` 'snowflake' | 'bigquery' | 'databricks_sql' | 'redshift' | 'postgres' | 'mysql' | 'sqlite' | 'parquet_s3' | 'csv' | 'google_sheets': The connection's warehouse type (e.g. snowflake, postgres, bigquery) — the same value Connection.type carries. A non-secret enum; lets a viewer-tier client name the warehouse without the editor-only connections list.
        - `roleMapped` boolean: A non_secret_config.roleMapping policy is in force (service_* only).
        - `resolvedRoles` string[]: Roles the acting viewer's role-mapping resolution yields for this connection: the primary role first, then any secondary roles sorted ascending. Omitted when the connection is not role-mapped or the resolution is unavailable (best-effort disclosure). Role names are non-secret metadata (ADR-0012/DVT-11 contract).
    - `url` string, uri: User-facing deep link into the dvt web app for this dashboard (DVT-5113), `<app URL>/o/<org>/dashboards/<id>`, built from APP_URL, else APP_BASE_URL (default https://app.dvt.dev). Present on single-dashboard reads (`GET /v1/dashboards/{id}`) and on create/update responses; omitted on collection reads and when the host is a placeholder (`.invalid`).
    - `conformance` 'core' | 'full': The spec's portability layer (ADR-0014), computed from the spec: `core` when it uses only the renderer-neutral dvt Core profile (portable, the "industry standard" layer), `full` when it uses any escape hatch (ECharts `option` passthrough, raw `html`, `media`, `$dvtRef`, `chart:custom`, or an arbitrary-CSS canvas background). A derived, read-only property — never authored. Optional for back-compat.
    - `provenance` ProvenanceSuggestion[]: Advisory provenance suggestions (DVT-249, ADR-0004 Amendment 1) computed from the persisted spec, present on create/update responses so an agent applying without a preview sees the same `meta.brief`/`purpose`/`audience`/ `keyQuestions` guidance the apply preview returns. NON-BLOCKING — persistence already succeeded. Omitted when there is nothing to suggest.
      - `field` string, required: Dotted spec path the suggestion concerns (e.g. `meta.brief`, `meta.panels.p1.serves_question`).
      - `severity` 'gate' | 'warn' | 'info', required: `gate` — the Tier-1 authoring gate (`meta.brief` on a 3+ panel dashboard); advisory at the server, treated as blocking by the authoring boundary. `warn` — a Tier-2 gap (missing `purpose`/`audience`/`keyQuestions` at 3+ panels, or a `serves_question` index past `keyQuestions`). `info` — a Tier-3 nudge (no `dataAsOf` on a data-bound dashboard, no per-panel provenance, an agent-asserted-but-unvalidated claim, or an orphan `meta.panels` key).
      - `message` string, required: Human/agent-readable guidance for resolving the suggestion.
    - `provenanceSummary` DashboardProvenanceSummary: Bounded discovery summary projected from the spec's `meta` provenance (DVT-250), present on dashboard LIST results so an agent can find the right dashboard by purpose/audience/question without fetching each full spec. Distinct from the apply-time `provenance` suggestions (DVT-249): this is recorded *content*, not advisory lint. Omitted when the dashboard carries no provenance. `findings` is a COUNT (not the array) to keep the summary cheap.
      - `brief` string: meta.brief — the one-line thesis.
      - `findingsCount` integer: Number of meta.findings entries.
      - `audience` 'executive' | 'analyst' | 'operator': meta.audience.
      - `keyQuestions` string[]: meta.keyQuestions — the questions this dashboard answers.
      - `tags` string[]: meta.tags.
      - `documentedPanelCount` integer: Number of panels that have at least one documentation field populated (`meta.panels[id].purpose` / `intent` / `assumptions` / `notes`). Computed in SQL from the manifest JSONB; non-zero means the dashboard has agent-facing panel docs. Absent (treated as 0) when the dashboard carries no panel provenance at all (ADR-0045 §7).
      - `hasPageDocs` boolean: True when at least one page carries a `doc` object with at least one populated field (`description` / `intent` / `assumptions` / `notes`). Absent when no page has docs. Use `dvt_dashboard_get(view="docs")` to read the full per-page doc bodies (ADR-0045 §7).
    - `certification` Certification: A dashboard's derived certification standing (DVT-5223), computed at read time — dashboards carry no tier column. `tier` is the EFFECTIVE tier (highest-ranked live grant, else draft). `state` and the certified* fields describe the highest-ranked ACTIVE grant, which may be stale or expired; the effective* fields describe the grant that determined `tier`. For headless render actors only `tier` and `state` are returned.
      - `tier` 'draft' | 'published' | 'certified' | 'canonical', required
      - `state` 'live' | 'stale' | 'expired': State of the highest-ranked active grant; absent when there is none.
      - `certifiedTier` 'published' | 'certified' | 'canonical'
      - `domain` string
      - `certifiedBy` string
      - `certifiedAt` string, date-time
      - `certifiedRevisionId` string, uuid
      - `certifiedVersion` integer
      - `editsSince` integer: Revisions written since the certified one.
      - `grantedVia` string
      - `expiresAt` string, date-time
      - `queryRole` string
      - `effectiveDomain` string
      - `effectiveGrantedBy` string
      - `effectiveGrantedAt` string, date-time
      - `effectiveExpiresAt` string, date-time
    - `permissions` DashboardPermissions: Server-computed, per-actor permission hint (DVT-676) on a dashboard: which mutating actions the requesting actor may take, derived from the same authz resolution the enforcing handlers use (capability floor + per-resource ownership/visibility/ACL). An advisory UI hint so clients can show/hide affordances (e.g. a Delete menu row) without re-deriving per-resource ACL grants they cannot see — the server still enforces on every mutating call, so a client MUST NOT treat a true value as authorization. Present on dashboard LIST and GET responses. `canDelete` mirrors `canWrite` today (delete is gated on dashboard:write). `canShare` follows owner-can-share (DVT-665): the owner, org admins, and ACL-admin grantees may share — except in snowflake mode, where a caller whose LIVE tier is viewer gets `canShare: false` even as the owner or an ACL-admin grantee (DVT-3302/DVT-4721: the live tier caps them at viewer rank). Exception: on a Claude connector viewer-tier call the flags are projected to editor-tier enforcement (the tier writes run at) and `requiresConnectorTier` is set; see that field.
      - `canWrite` boolean, required
      - `canDelete` boolean, required
      - `canShare` boolean, required
      - `requiresConnectorTier` 'editor': Present (as "editor") only on a Claude connector viewer-tier call (DVT-4942): the flags above were projected to what an editor-tier connector call would be allowed, since writes run at that tier. The write is additionally refused (tier_insufficient) if the caller lacks the dvt_editor app role, which dvt-api cannot observe from a viewer-tier call.
    - `lastViewedAt` string, date-time: The calling user's own most recent view of this dashboard (DVT-1407), sourced from the dashboard_views telemetry table. Absent when the caller has never viewed this dashboard, or when there is no authenticated caller to scope by.
    - `favoritedAt` string, date-time: DVT-5306. When the caller starred this dashboard. Set only on GET /v1/me/favorites results; absent everywhere else.
    - `viewCount` integer: DVT-5306. The caller's own human views of this dashboard in the org over the last 30 days (`lastViewedAt` is all-time). Set only on GET /v1/me/favorites results; omitted when 0 and everywhere else.
    - `accessViaFolderId` string, uuid: Set on LIST results (DVT-1421) when the caller's access to this dashboard resolves via a folder-inherited ACL grant rather than ownership, an org-admin bypass, or a direct/group grant of equal or higher privilege. An advisory "via folder" hint for the UI — never authoritative, and never present for the owner or an org admin/owner (neither needs the badge). Omitted otherwise.
    - `creatorName` string: Display label for ownerId (DVT-1418): the owning user's display_name, else email, else "system". Absent only if the owner row is somehow unresolvable.
    - `lastEditedBy` string: Display label for the most recent create/update edit against this dashboard (DVT-1418), sourced from the latest matching audit_logs row for this org. For an agent-attributed edit, the API-key name or MCP client id (DVT-1196 "via {key}" convention) when present, else the editor's own display_name/email/ "system" fallback. Absent when the dashboard has no create/update audit history (e.g. pre-audit legacy rows).
    - `lastEditedByType` 'user' | 'agent': The actor_type of the audit_logs row behind lastEditedBy (DVT-1418), clamped to `user` | `agent` (audit_logs also allows `system`/`render`, folded into `user` here). Absent when lastEditedBy is absent.
    - `lastViewedBy` string: Display label (display_name/email/"system") of whoever — any actor, any type, not just the calling viewer — most recently viewed this dashboard org-wide (DVT-1418), sourced from the dashboard_views telemetry table. Distinct from `lastViewedAt` above (DVT-1407's caller-scoped read receipt). Absent when the dashboard has never been viewed by anyone in this org.
    - `lastViewedByAt` string, date-time: Timestamp paired with lastViewedBy above (DVT-1418). Named distinctly from `lastViewedAt` (DVT-1407, caller-scoped) to avoid colliding with that already-shipped, differently-scoped field. Absent when lastViewedBy is absent.
  - `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

- `400`: Malformed request, or a `python` panel this write seam refused (ADR-0084 D2/I7). Distinguished by the Problem `type`; every python refusal also echoes its slug in the `X-Dvt-Error-Code` response header: - `https://docs.dvt.dev/errors/bad-request` (the default) — a malformed or otherwise invalid request body. - `https://docs.dvt.dev/errors/python-source-unknown` — a python panel's `data.sourceId` names no live connection in this organization. A connection in ANOTHER organization reads identically, by design. List the ones you can use with `GET /v1/connections`. - `https://docs.dvt.dev/errors/python-source-not-snowflake` — the panel's source resolves but is not a Snowflake connection. A python panel executes as an anonymous procedure inside Snowflake and has nowhere else to run; express the panel as SQL against the current source instead. - `https://docs.dvt.dev/errors/python-source-not-pushdown` — the source is Snowflake but a `writeback`-kind connection (a Sheets flush target, not a queryable source). Point `data.sourceId` at a pushdown connection. Only a write that ADDS or CHANGES a python panel is checked: a title-, layout-, or geometry-only edit of one already stored is an ordinary dashboard write, and deleting one is never an authoring act.
- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action, or a `python` panel this write seam refused on identity grounds (ADR-0084 D2/I7). Distinguished by the Problem `type`: - `https://docs.dvt.dev/errors/forbidden` — the caller's role does not permit this action; for a python panel, it lacks the `python:author` capability (the editor floor), or an API key's scopes exclude it. - `https://docs.dvt.dev/errors/not-in-edition` — python panels are not available on this deployment at all (`editionCapabilities.pythonPanels` is false). **The 1.0.20 Snowflake native app ships `PYTHON_PANELS_ENABLED="true"` as of DVT-4453** (risk accepted 2026-09-19; ADR-0084 H5 remains a hard gate for the next cut, not a closed item), so this response is no longer expected there by default. On the native app the literal is baked into the package at build time, so it would fire again only after a provider-side package patch from dvt turns it back off — a consumer install cannot flip it itself; the in-account lever is the `python:author` editor floor via the app roles. On Gallery and Enterprise it fires whenever the operator's own env literal is off. Same shape as the linked-Sheet cell gate's refusal. Use a SQL-backed panel, or ask your dvt operator. - `https://docs.dvt.dev/errors/python-author-requires-admin` — the panel's source is a SHARED-credential connection (any `service_*` auth method, the internal `platform_managed` one, or `cloud_iam`, which is the server's own ambient workload identity), so the authored code would run under a credential no viewer holds: authoring it requires the admin/owner rank, over and above `python:author`. Also echoed in `X-Dvt-Error-Code`. Only the two caller's-rights methods keep the editor floor — `oauth_passthrough` and `spcs_token`, each of which opens the warehouse session as the acting user.
- `404`: Resource does not exist.
- `409`: The resource was modified since you last fetched it (optimistic-concurrency).
- `422`: The spec failed JSON Schema validation. `fields` lists each violation.

---

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