# Views & exports

> Read the shared saved views of a workspace and the runs they select, create and change views owned by your key's creator, and export a workflow's runs as CSV.

Source: https://developers.processcart.com/guides/views-exports

Operations: [Views endpoints](https://developers.processcart.com/reference/views) · [Exports endpoints](https://developers.processcart.com/reference/exports).

## Saved views

A saved view is a board query — filters, sort and the board's column choice — with a name. Through the API a key sees every **shared** view of the workspace (`visibility: "org"` or `"groups"`) and never a private one: a private id is a 404, indistinguishable from a missing view.

```json
{
  "id": "v1e2…", "name": "Open rush orders",
  "query": { "workflowId": "3c1d…", "status": ["active", "on_hold"], "priority": ["urgent", "high"], "labels": ["lbl_rush"],
             "assignee": null, "due": null, "q": "", "qMode": "prefix", "sort": { "key": "due", "dir": "asc" },
             "columns": ["status", "priority", "due", "assignees"] },
  "visibility": "org", "groupIds": [],
  "owner": { "userId": "11111111-…", "name": "Ada Lovelace" },
  "createdAt": "2026-09-24T16:00:00.000Z", "updatedAt": "2026-09-24T16:00:00.000Z"
}
```

- `GET /v1/views` lists shared views by name (`updatedSince` for a sync); `GET /v1/views/{id}` reads one.
- `GET /v1/views/{id}/runs` applies the view's stored query exactly as the board does and returns **full Run resources**, paged like `GET /v1/runs` (up to 100 per page) and limited to the key's allowlist. Two queries cannot be resolved for a key: a view that filters by the viewer (`assignee: { "kind": "me" }`) — call `GET /v1/runs?assignee=u:<user id>` instead — and a view sorted by `progress` (computed in the app); both are a 422 (`errors[].code` `viewer_relative` and `unsupported_sort`). `updatedSince` orders by `updatedAt` ascending instead of the view's sort. `columns` is presentation only and ignored.

### Owner rules

A saved view always has a person behind it. A view the API creates is **owned by the member who created the key**, who must still be a Builder or Admin of the workspace (else 403 `not_allowed_for_key` — mint a key as a current Admin). Each owner keeps at most 30 views (a 31st is a 422, `errors[].code` `limit_reached`).

- `POST /v1/views` — `{ name, query, visibility?: "org" (default) | "groups", groupIds? }`. **Send only the `query` members you set; the rest take the board's defaults** — any workflow, status `active` + `on_hold`, any priority, label, assignee and due date, no name filter, sorted by `started` newest first, and the board's default columns. `sort: { "key": "name" }` without a `dir` takes that key's natural direction (`asc` for `name`, `key_date` and `due`; `desc` for the rest). The 201 carries the complete query, with an absolute `Location`. `groups` needs at least one group of the workspace (a group id from another workspace is a 422, `errors[].code` `unknown_group`).
- `PATCH /v1/views/{id}` — any subset of the same members. A `query` changes **only the members it sends**; the rest keep the view's stored values, and `null` clears `workflowId`, `assignee` or `due`. Only a view owned by the key's creator can be changed through the API (403 `not_allowed_for_key` otherwise — create a view of your own), and the creator must still be a Builder or Admin of the workspace (403 `not_allowed_for_key` otherwise). Switching to `org` drops the group list.
- In both, every `query` member is checked: a bad value is a 422 naming it (`errors[].field` `query.status.0`), and a member the query does not have (`"workflow"` for `workflowId`) is a 422 `unrecognized_keys` rather than a silent default.
- `DELETE /v1/views/{id}` — removes **any** shared view, whoever owns it (the same power an Admin has in the app); audited under the key's name. 204.

```bash
curl -X POST https://app.processcart.com/api/v1/views \
  -H "Authorization: Bearer pc_live_…" -H "Content-Type: application/json" \
  -d '{ "name": "Active by name", "query": { "workflowId": "3c1d…", "status": ["active"], "sort": { "key": "name" } } }'
```

The status labels a `query.labels` filter takes are listed by `GET /v1/me` (`org.labels`).

## CSV export

`GET /v1/exports/runs.csv?workflowId=…` returns one workflow's runs as a CSV file, newest first:

```bash
curl "https://app.processcart.com/api/v1/exports/runs.csv?workflowId=3c1d…&status=completed&cols=status,started,completed&fields=amount,ship_date" \
  -H "Authorization: Bearer pc_live_…" -o orders.csv
```

- `workflowId` is required — a CSV has one set of field columns. The run filters are `GET /v1/runs`'s (`status`, `priority`, `labels`, `assignee`, `due`, `q` + `qMode`, `updatedSince`, `includeTrashed`).
- `cols` picks the run columns after "Run name": `status`, `priority`, `labels`, `progress`, `key_date`, `due`, `started`, `completed` (default: status, started, completed). `fields` picks field columns by key, in the workflow's own order (default: the first six fields); an unknown key is a 422.
- Every cell is formatted as the app's own export formats it — currency with its symbol, dates in the workspace's time zone, members by name — and cells that could be read as formulas by a spreadsheet are defused.
- `text/csv; charset=utf-8` with a BOM and CRLF line ends (so it opens cleanly in Excel) and a `Content-Disposition: attachment` header.
- The whole file is built before it is sent; a selection above **10,000 rows** is refused with a 422 (`errors[].code` `too_many_rows`) — narrow the filter (by status, by `updatedSince`, by label).
- The usual pre-checks answer Problem Details: 404 for an unknown workflow, 409 `workflow_not_published`, 403 outside the key's allowlist.

For a machine-readable copy of every run, prefer `GET /v1/runs` with `updatedSince` — it carries canonical values rather than display text ([Incremental sync](https://developers.processcart.com/guides/incremental-sync)).
