# Save a gateway view and share it

Keep a question you asked of a gateway's traffic, share it with your team, and understand why two people see different answers.

Source: https://tokentriage.com/docs/build/save-gateway-views

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

You spent ten minutes narrowing a window, a set of applications and five metrics until
the number on screen answered the question somebody asked you in a meeting. Tomorrow
you want that question back, and so does the colleague who asked it.

A saved view is that question, kept under a name. It stores no query language and no
permission: opening one asks the stored question again, on behalf of whoever opened it.
Two people on the same view can therefore see two different answers, and both are
correct.

You need an organization id (`org_…`), a workspace id (`t_…`), and `metadata.read` on
the scope the question asks about. Saving a view for your team additionally needs
`metadata.read` for every explicitly selected application and gateway. An unfiltered
workspace question requires workspace access; a mixed question is refused if any
selected application is outside the viewer’s authority. Money metrics inside the question still need
`cost.read` at the moment somebody opens it.
[Read your gateway's traffic and spend](/docs/build/gateway-insights) covers asking the
question in the first place, and this page assumes you can already ask it.

Every command block on this page is program output, captured by replaying the recorded
lane answers in `sdk/spec/insight-read-fixtures.json` through a loopback server, so
`--base-url` names `127.0.0.1` and a port that changes on every run. Yours names your
deployment.

## What a view stores, and what it cannot store

A stored view holds the question document, a name, an audience, how it is dated, and a
display zone. It holds no rows, no answer and no access.

| Field | What it holds |
|---|---|
| `name` | the words you would use to ask for it out loud |
| `audience` | `personal` (only you see it) or `team` (everyone in the workspace sees the question) |
| `query` | an `insight-query/v1` document: scope, window, purposes, metrics |
| `time_intent` | `relative` with a span, or `fixed` with the window it was saved at |
| `display_tz` | the IANA zone every reader sees the times in |
| `revision` | a forward-only counter, carried as a decimal string |

Five members are turned away by name if they appear anywhere in the question document:
`sql`, `raw_sql`, `grant`, `grants` and `role`. A view is a question, so there is no
member of one that could carry a statement or a permission, and both the server and the
CLI say which member was found.

<Callout type="key">

Sharing a view shares the question, not the answer. Everyone who opens it is answered
with their own access, so it gives nobody anything they could not already read. That
sentence is what the portal renders under the audience control, at the moment somebody
picks "Everyone in this workspace".

</Callout>

## List and open the views you can see

`view list` answers with the views *you* can see: your own, plus the team views your
current access still reaches. It does not say how many exist in the workspace, because
that read cannot support such a claim.

```console
$ tokentriage cloud view list --org org_K11111111111 \
    --workspace t_K11111111111 --base-url http://127.0.0.1:2503
sview_K11111111111	Checkout spend, last 24h	team	fixed	overview	revision 1
sview_M11111111111	A question this build lost	team	fixed	overview	revision 1
```

`view get` prints the stored row and says, on stderr, that a row is not an answer:

```text
cloud: this is the stored QUESTION and not its answer; `cloud view open` answers it under your own scope
```

`view open` returns the question and the answer in one document, so a chart and the
caption above it cannot come from two different moments of your access:

```console
$ tokentriage cloud view open --org org_K11111111111 \
    --view sview_K11111111111 --base-url http://127.0.0.1:2507
sview_K11111111111	Checkout spend, last 24h	team	fixed	overview	revision 1
application	requests.v1	1284 count
application	provider_spend.v1	unavailable (unpriced_population)
application	cost_per_1000_requests.v1	unavailable (unpriced_population)
application	tokens.v1	84210773 tokens (FLOOR: ingest_gap:gateway_journal)
application	response_latency.v1	412 milliseconds
```

Two of those cells carry no figure, and one is a floor.
[Read your gateway's traffic and spend](/docs/build/gateway-insights#read-a-cell-that-has-no-number-in-it)
covers how to read each state; opening a view changes none of it.

## Save the question that is on screen

The portal is where a question on a screen becomes a view, because the portal is the
surface that has one. Open a gateway, set the window, the activity purposes and the
metrics you want, and save from there. The panel writes the question the screen is
asking; it offers no second editor for the query document, so a saved view cannot
disagree with the screen it was saved from.

Four fields decide what you get:

- **What this view answers.** The name, up to 200 characters.
- **Who can see this question.** "Only me", or "Everyone in this workspace".
- **How this view is dated.** "A rolling window", re-dated on every open, or "The exact
  window on screen", frozen as it is now. Rolling spans are a choice of last hour, last
  24 hours, last 7 days or last 30 days rather than a free number, because a span the
  producer turns away is a failure nobody can act on.
- **Show times in.** An IANA zone name, applied for everyone who opens the view, so a
  shared view does not read as a different day to different people.

The saved-view page of one gateway lives at
`/account/orgs/{org}/applications/{app}/gateways/{gw}/views/{sv}`, and carries the
workspace in the `?workspace=` search parameter like every other portal address. That
URL is the thing a colleague pastes into chat. It lists, opens, edits and deletes; it
does not create, because creating needs the question from the screen that asks it.

## Save the same question from a file

The CLI draws no chart, so there is nothing on it to "save the current view" from. It
takes the question as an `insight-query/v1` document, which is the same document
`cloud insight overview --json` prints. Keep it in a file and store it under a name:

```console
$ tokentriage cloud view save --org org_K11111111111 \
    --workspace t_K11111111111 --name "Checkout spend, rolling 24h" \
    --query-file question.json --time-intent relative \
    --relative-window 24h --base-url http://127.0.0.1:2509
sview_W11111111111	Checkout spend, rolling 24h	personal	relative	overview	revision 1
```

The audience defaults to `personal` and the new row starts at revision 1. The body that
went out carries the question, the intent and the window in nanoseconds. It carries no
owner, because there is no owner member to put anybody's name in: the creator's scope is
stamped from their own resolved authority.

<Callout type="warning">

Saving is not idempotent and sends no idempotency key. A repeat with a new name mints a
second view, and a repeat with the same name comes back as a `conflict`. If a save times
out, run `view list` and delete the stray rather than sending it again.

</Callout>

Both packaged SDKs carry the same six operations under `saved_views` (Python) and
`savedViews` (TypeScript): `list`, `get`, `create`, `update`, `delete` and `open`.

## Edit at the revision you read

An edit replaces the whole view and names the revision the editor saw. Over a document
with a scope inside it, "did not mention the scope" and "cleared the scope" would be the
same bytes, so every member travels on every edit:

```console
$ tokentriage cloud view edit --org org_K11111111111 \
    --view sview_W11111111111 --name "Checkout spend, shared with the team" \
    --audience team --query-file question.json --time-intent relative \
    --relative-window 24h --expected-revision 1 \
    --base-url http://127.0.0.1:2511
sview_W11111111111	Checkout spend, shared with the team	team	relative	overview	revision 2
```

Send the same bytes again and the row does not move. The second editor in a race gets a
412 that names the revision the row is at now:

```text
customer.saved_views.update was refused as a stale revision (HTTP 412): re-read the resource and retry with its current --expected-revision; detail: foundation: stale revision: saved view sview_W11111111111 is at revision 2 and the edit expected 1; type=precondition_failed; request_id=req_saved_view_update_stale_revision
```

The remedy is to read the row again, re-apply your change and send that. Retrying these
bytes with a bumped revision would overwrite an edit nobody would have seen losing. The
portal says the same thing in words and offers one button, "Read this view again"; it
offers no retry.

In the portal, a question that has drifted from the view it was opened from shows an
"Unsaved changes" chip and a sentence naming what "Update" would replace. A rolling view
does not sit permanently in that state: drift is compared on the stored span, not on the
window an open happened to be re-dated to.

## What goes wrong, and which person fixes it

| What you see | What happened | What to do |
|---|---|---|
| `HTTP 422`, `validation_failed` naming a member | the question document carried `sql`, `raw_sql`, `grant`, `grants` or `role` | remove the member; nothing was saved |
| `HTTP 422` on `open` while `get` still works | the stored question names something this build no longer publishes | edit the question |
| `HTTP 412`, `precondition_failed` | somebody edited the view after you read it | re-read, re-apply, send again |
| `HTTP 409`, `conflict` | a ceiling: 50 views per owner in a workspace, or 500 per workspace | delete a view you no longer open |
| `HTTP 403` on delete | you can see this team view but do not own it and do not run the workspace | ask the owner or a workspace administrator |
| `HTTP 404` on delete | the view is not one you can see at all | check the id and the workspace |

The 422-on-open pair is the one that surprises people. The row reads perfectly and you
are allowed to read it; what the build cannot do is ask the question again:

```text
customer.saved_views.open was refused (HTTP 422); detail: foundation: unsupported combination: saved view sview_M11111111111 asks a question this build cannot re-ask: insightcontract: metric version unknown; type=validation_failed; request_id=req_saved_view_open_stale_question
```

It is not a 404 and not a 403, so nobody needs to be asked for anything. The portal
prints the server's own sentence and adds one line: the remedy is to edit the stored
question.

<Details summary="Why 403 and 404 differ on delete">

Somebody who can see a team view but may not remove it is told they may not; somebody
who cannot see it at all is told there is no such view. Answering the other way round
would turn a delete into an oracle for which questions colleagues keep.

</Details>

## When two people see different numbers

A team view is re-resolved per reader on every open, so a colleague whose access reaches
fewer applications than yours gets a narrower answer to the identical question. The
portal shows the rows it did get and a banner above them saying which part of the
question this reader was not answered for, in one of two shapes:

- the stored question named applications, gateways or environments the reader is not
  answered for, which the panel can state as a fact because it holds both lists;
- the stored question named none on that dimension, meaning "every descendant I may
  read", and the answer came back with an explicit list. The panel says the resolved
  count may be fewer than somebody else sees, and claims nothing more.

Blanking the panel instead would tell that reader their traffic was empty, which is a
claim about their application rather than about their access.

## What a saved view does not do

It grants nothing, and deleting one removes a question and no measurement. It stores no
chart: the portal decides how an answer is drawn, and the stored document names only
metrics, a window, purposes and a scope. It carries no schedule and mails nobody. And an
`open` never widens a reader's scope to the author's, which is the whole reason the same
view can answer two people differently.

<NextSteps items={[
  { title: "Read your gateway's traffic and spend", href: "/docs/build/gateway-insights", description: "Ask the question a view stores, and read a cell that has no number in it." },
  { title: "Gateway insight reads", href: "/docs/reference/gateway-insights", description: "The saved-view fields, the operations and every refusal code, field by field." },
  { title: "Report and read app feedback", href: "/docs/build/report-app-feedback", description: "The other question about a gateway's traffic: what its users thought of the answers." },
  { title: "Who can do what in your company", href: "/docs/build/organizations-and-access", description: "How metadata.read and cost.read reach a person, which decides what an opened view can show them." }
]} />
