---
title: "Export a panel's query result as CSV or XLSX."
method: POST
path: "/v1/data/export"
tags: ["data"]
---

# Export a panel's query result as CSV or XLSX.

`POST /v1/data/export`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/data/export`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Re-runs the panel's query through the SAME pushdown path as `POST /v1/data/query`
(ADR-0011, parameter-bound per DVT-7 — an export is a re-run, never a privileged
side channel), then streams the result rows serialized as CSV or XLSX. Honors the
same tenant gate and connection/role RBAC as a normal panel query: a cross-tenant
`sourceId` returns 404, and the query runs under the same warehouse identity (no
privilege escalation via export).

Requires the `data:export` capability (DVT-127) — enforced server-side, not only in
the UI. Results are never served from the result cache: an export is a deliberate
data-egress event, so it always reflects live rows and is recorded as an `audit_logs`
entry (who/what/when/row-count).

View-state (v1): send the panel's CURRENT `params` (dashboard filter selections +
drill state) so what-you-see-is-what-you-export at the filter level. The exported
rows are the FLAT, query-ordered result — client-side table column sort/filter
(DVT-135) and grouped/pivot rollups (DVT-113) are not replayed server-side in v1.
The result inherits the engine's row cap; `X-DVT-Truncated: true` marks a cut-off.

XLSX cells are typed (numbers and booleans are native cells, not stringified); the
header row uses the resolved column labels.

## Request body

- ExportRequest
  - `sourceId` string, required: Connection name or UUID (resolved per connection-as-code.md).
  - `query` string, required: SQL to execute against the source via pushdown.
  - `params` object: Optional named query parameters — send the panel's current dashboard-filter and drill state here so the export matches what is displayed (DVT-137 v1).
  - `format` 'csv' | 'xlsx', required: Serialization format for the exported result.
  - `filename` string: Optional download filename (without extension; the extension is forced to match `format`). Sanitized server-side. Defaults to "export".
  - `columns` object[]: Optional per-column presentation hints (DVT-4174), keyed by the result's raw field name. Matching is exact first, then a UNIQUE case-insensitive match (Snowflake returns UPPERCASE names for unquoted identifiers while a saved panel's hints are typically lowercase); an ambiguous fold (two result columns differing only by case) drops the hint rather than guessing. A hint for a field absent from the live query result is dropped; a result field with no hint is still exported under its raw name. Order of the hinted entries controls the OUTPUT column order (hinted fields first, in hint order, then the rest of the result columns).
    - `field` string, required: The result column name this hint applies to.
    - `label` string: Display header for this column. Empty/absent → use `field`.
    - `format` object: Number/date rendering hint for XLSX cells only (CSV values are always the raw stringified value). An unrecognized `type` is ignored rather than rejected.
      - `type` 'currency' | 'percentage' | 'number' | 'date' | 'duration' | 'compact' | 'custom', required: Rendering family. "duration" and "custom" round-trip but apply no XLSX number format.
      - `currency` string: ISO 4217 code for `type: currency`. Defaults to "USD".
      - `decimals` integer: Fixed decimal-place count. Absent → the type's own default (1 for percentage, 0 for currency/number).
      - `pattern` string: Date display hint for `type: date`; a pattern containing "MMM" renders as a month-and-year XLSX date format.
    - `align` 'left' | 'center' | 'right': Text alignment for this column's data cells (DVT-4196). Formatted style only; ignored for `style: data`.
    - `textStyle` ExportTextStyle: Mirrors the web ColumnTextStyle (DVT-4196) — the per-column base text style, layered under a column's colorScale/conditionalFormat. Every enum and bound below is enforced (400 on violation) for `style: formatted`; for `style: data` the whole object is ignored and left unvalidated. `font` and `transform` round-trip but have no XLSX equivalent (the DVT-4195 fidelity ceiling) and are ignored; `weight: medium` is accepted but not bold.
      - `weight` 'normal' | 'medium' | 'bold'
      - `align` 'left' | 'center' | 'right'
      - `size` number: Font size in pixels.
      - `font` string: Font family. Accepted but ignored — not an Excel font.
      - `color` string: Text color (hex / `rgb()` / `hsl()` / CSS named color / `{token}`).
      - `transform` 'none' | 'uppercase' | 'lowercase' | 'capitalize': Accepted but ignored — a CSS-only rendering effect.
      - `decoration` 'none' | 'underline' | 'line-through'
    - `conditionalFormat` ExportConditionalFormatRule[]: This column's own conditional-format rules (TableColumn.conditionalFormat, DVT-4196). Formatted style only.
      - `where` ExportCellPredicate, required: Mirrors the web CellPredicate (DVT-4196) evaluated by a formatted export's conditional-format rules and target matching: either a leaf comparison (`field`/`op`/`value`) or an `all`/`any` combinator over nested predicates. A rule with more than 500 predicate nodes is rejected with 400 (see the request-level budgets on `conditionalFormat`). Nesting deeper than 5 levels is not rejected: nodes past that depth simply never match, exactly as in the renderer.
        - `field` string: Result column this leaf tests. Absent → the field the rule is being evaluated for (the owning column hint's field, or — for a table-wide rule applied per column — the column currently under test).
        - `op` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in' | 'notIn' | 'contains' | 'isNull' | 'isNotNull' | 'topN' | 'bottomN': Comparison operator for this leaf. Absent/empty on a leaf never matches.
        - `value` unknown
        - `all` ExportCellPredicate[]: True only when every nested predicate matches (AND).
        - `any` ExportCellPredicate[]: True when any nested predicate matches (OR).
      - `apply` ExportCellStyle, required: Mirrors the web CellStyle (DVT-4196) — the paint a matching conditional- format rule applies. Baked to a static XLSX fill/font on export; never an Excel conditional-formatting rule (the DVT-4195 decision).
        - `fill` unknown
        - `textColor` unknown
        - `weight` 'normal' | 'medium' | 'bold': Only "bold" renders bold in XLSX; "medium" is accepted but not bold.
        - `italic` boolean: Tri-state, mirroring the renderer's object spread: absent leaves whatever an earlier rule (or the column's textStyle) set, `true` turns it on, and an EXPLICIT `false` turns it back off.
        - `underline` boolean: Tri-state — see `italic`.
        - `strikethrough` boolean: Tri-state — see `italic`.
        - `align` 'left' | 'center' | 'right'
      - `target` string: "row" (paint the whole row), "cell" (the default — paint only the field the rule tested), or an explicit field name to paint a different column than the one the predicate tested.
      - `stopIfMatched` boolean: Stop evaluating later rules in the same list once this rule matches a given cell/row.
    - `colorScale` ExportColorScale: Mirrors the web ColorScale (DVT-4196) — a per-column heatmap fill. Baked to a static per-cell color on export; never an Excel color-scale rule (the DVT-4195 decision). FIDELITY NOTE: the degradations below (`bins` clamped to 1000, a >64-stop `palette` downsampled, a non-increasing 3-point `domain` reduced to [min, max]) are applied by the EXPORT only — the renderer imposes no bins ceiling or palette ceiling and applies a 3-point domain as given. For those inputs the baked colors can therefore differ from what is on screen. It is the deliberate trade against the alternative of refusing to export a panel the app renders; every other input bakes exactly what the renderer paints.
      - `method` 'numeric' | 'bin' | 'quantile'
      - `domain` unknown
      - `palette` unknown
      - `bins` integer: Bucket count for `method: bin`. Defaults to 5; a value below 2 is clamped to 2 and one above 1000 is clamped to 1000 (the dashboard schema sets no maximum, so an over-large count is degraded, never rejected).
      - `nullColor` string: Fill used for a cell whose value is null or non-finite.
    - `width` number: Column width in pixels (DVT-4196), converted to Excel's character-width units. Formatted style only; absent → width is estimated from content, as in the DVT-4174 path.
    - `wrap` boolean: Wrap text within this column's data cells. Formatted style only.
  - `sheetName` string: Optional XLSX worksheet name (ignored for CSV). Sanitized and truncated to Excel's 31-character sheet-name limit; defaults to "Data" when empty or made entirely of characters Excel forbids in a sheet name.
  - `style` 'data' | 'formatted': "formatted" (DVT-4196) BAKES the panel's rendered colors/fonts as static cell styles — never Excel conditional-formatting rules, color-scale rules, or formulas, per the DVT-4195 decision (the workbook is a faithful snapshot of the render, not a live model). "data" (the default) is the flat DVT-4174 workbook: labels, number formats, frozen header, autofilter, widths. Ignored for `format: csv`. `style` itself is always validated (400 on any other value), but the formatted-only fields below — `conditionalFormat`, `frozenColumns`, `tokens`, and each column's `align`/`textStyle`/`conditionalFormat`/ `colorScale`/`width`/`wrap` — are only validated for `style: formatted`, because that is the only style that reads them: a `style: data` request carrying a saved panel's presentation slices returns the DVT-4174 bytes rather than a 400.
  - `conditionalFormat` ExportConditionalFormatRule[]: The panel's TABLE-WIDE conditional-format rules (TableSpec.conditionalFormat); column-level rules ride on each entry in `columns`. Formatted style only — ignored for `style: data` and for CSV. Budgets (400 when exceeded): at most 100 rules per list and 500 across the table and every column; at most 1000 predicate nodes in the table-wide list (it is evaluated for every column) and 5000 across the request. For those two node budgets a leaf with an ARRAY `value` counts one node per element, because `in`/`notIn` scan the whole array against every cell — a 1000-element `in` rule is 1000 nodes, not 1. Every leaf costs at least one node, so an empty or single-element array still counts 1. The weighting is by shape, not by operator, so an array handed to any other op is billed the same way. A request whose rules × rows work exceeds the server's evaluation budget for the actual result is refused with 422 (`export-too-complex`) — export with `style: data` instead.
    - `where` ExportCellPredicate, required: Mirrors the web CellPredicate (DVT-4196) evaluated by a formatted export's conditional-format rules and target matching: either a leaf comparison (`field`/`op`/`value`) or an `all`/`any` combinator over nested predicates. A rule with more than 500 predicate nodes is rejected with 400 (see the request-level budgets on `conditionalFormat`). Nesting deeper than 5 levels is not rejected: nodes past that depth simply never match, exactly as in the renderer.
      - `field` string: Result column this leaf tests. Absent → the field the rule is being evaluated for (the owning column hint's field, or — for a table-wide rule applied per column — the column currently under test).
      - `op` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in' | 'notIn' | 'contains' | 'isNull' | 'isNotNull' | 'topN' | 'bottomN': Comparison operator for this leaf. Absent/empty on a leaf never matches.
      - `value` unknown
      - `all` ExportCellPredicate[]: True only when every nested predicate matches (AND).
      - `any` ExportCellPredicate[]: True when any nested predicate matches (OR).
    - `apply` ExportCellStyle, required: Mirrors the web CellStyle (DVT-4196) — the paint a matching conditional- format rule applies. Baked to a static XLSX fill/font on export; never an Excel conditional-formatting rule (the DVT-4195 decision).
      - `fill` unknown
      - `textColor` unknown
      - `weight` 'normal' | 'medium' | 'bold': Only "bold" renders bold in XLSX; "medium" is accepted but not bold.
      - `italic` boolean: Tri-state, mirroring the renderer's object spread: absent leaves whatever an earlier rule (or the column's textStyle) set, `true` turns it on, and an EXPLICIT `false` turns it back off.
      - `underline` boolean: Tri-state — see `italic`.
      - `strikethrough` boolean: Tri-state — see `italic`.
      - `align` 'left' | 'center' | 'right'
    - `target` string: "row" (paint the whole row), "cell" (the default — paint only the field the rule tested), or an explicit field name to paint a different column than the one the predicate tested.
    - `stopIfMatched` boolean: Stop evaluating later rules in the same list once this rule matches a given cell/row.
  - `frozenColumns` integer: Freezes the first N output columns (TableSpec.frozenColumns). Formatted style only; clamped server-side to the output column count.
  - `tokens` object: The client's RESOLVED theme token map (`{token.name}` → literal), so token refs inside rules/textStyle/palettes resolve to exactly what was on screen. The server has no theme/preset resolver of its own. Formatted style only. At most 1000 entries, keys and values at most 200 characters — a larger map is rejected with 400.

## Response `200`

The serialized export file (CSV or XLSX) as an attachment.

## Other responses

- `400`: Malformed request.
- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Resource does not exist.
- `409`: `https://docs.dvt.dev/errors/not-queryable-connection` (DVT-2540) — the resolved connection is configured for writeback only (`kind='writeback'`) and cannot be used as an export source. No self-heal retry; bind a pushdown connection to this `sourceId` instead.
- `413`: `https://docs.dvt.dev/errors/payload-too-large` — the request body exceeds 2 MiB (the query plus every hint, rule, and token). Trim the formatted-export fields or use `style: data`.
- `422`: `https://docs.dvt.dev/errors/export-too-complex` (DVT-4196) — the formatted export's conditional-format rules × the result's row count exceed the server's style-evaluation budget. Narrow the query, simplify the rules, or export with `style: data`.
- `429`: Two causes, distinguished by the Problem `type` (DVT-3464 split this from a single shared `errors/rate-limited`, which made an engine-capacity shed indistinguishable from an actual rate limit): - `https://docs.dvt.dev/errors/rate-limited` — a rate or concurrency limiter for this operation rejected the request before it reached the engine. See `components/responses/RateLimited` for the general per-actor SEC-2 tier; some operations stack an additional operation-specific limiter (the query submit rate limit, the per-org running-job cap, or the trial query ceiling). - `https://docs.dvt.dev/errors/engine-at-capacity` — the engine's own admission control (ADR-0053) shed the request before it reached the warehouse — a dvt engine limit, not a warehouse or rate problem (DVT-765). `suggestion` names the actual wait in seconds and, when the engine reports a scope, whether the per-org or global engine concurrency cap was hit. Both carry `Retry-After` (seconds); back off for at least that long before retrying.
- `502`: The analytics engine could not be reached (`https://docs.dvt.dev/errors/engine-unavailable`) or returned an error (`https://docs.dvt.dev/errors/engine-error`). One 502 on this shape is NOT an engine fault and says so: when Snowflake refuses the caller's-rights LOGIN, the Problem type is `https://docs.dvt.dev/errors/caller-connect-refused` — no session was established and the caller's SQL was never reached, so the remediation is a fresh dvt session rather than the engine logs (DVT-4345).

---

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