Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Keep a dated report of a gateway view

Turn a saved gateway view into a report run that keeps one dated answer, then open it, run it again and export it.

TutorialLast updated

On this page

Somebody asks how the checkout gateway did yesterday. You have a saved view that answers it, but a view asks again every time it opens, so by Wednesday it answers a different question. You want Monday’s answer, with the window it covered, kept where you and they can open it.

In this tutorial we’ll turn a saved view into a report, wait for its first run to finish, open the answer, run it again for a fresh window, and export one run as CSV. By the end you’ll have two dated runs of the same question side by side, each with its own window and digest.

What a report run keeps, and who can open it explains why a run keeps its answer; this page only uses that.

Every output block on this page is program output, captured by replaying the recorded answers in sdk/spec/report-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. Those answers were recorded from the control plane’s report handler, service and worker; the analytics answer inside each run comes from a test reader that stands in for the analytics service. The CLI writes a line naming each request to stderr, and the blocks keep it where a terminal shows it.

Before you start

You need Why
the tokentriage CLI and your deployment’s origin for --base-url every step is one cloud report command
a management key in TOKENTRIAGE_MANAGEMENT_KEY reports are read with a person’s or a management credential
an organization ID (org_…) and a workspace ID (t_…) reports belong to one workspace
a saved view you can open (sview_…) the report copies its question
metadata.read on the view’s whole scope a narrower block is answered as not found

You don’t need cost.read. Without it, money cells arrive as unavailable rather than as numbers, and the rest of the report is unchanged. You don’t need a runtime key, and nothing here calls a model provider. If you have no saved view yet, Save a gateway view and share it makes one.

1. Read the view’s revision

A report pins the revision of the view you read, so a view somebody edits while you type is caught instead of reported over a different question. Read the view with tokentriage cloud view get --org … --view … and note the revision it prints:

$ tokentriage cloud view get --org org_K11111111111 --view sview_K11111111111 \
    --base-url http://127.0.0.1:13115
cloud: customer.saved_views.get GET http://127.0.0.1:13115/portal/api/v1/customer/v1/organizations/org_K11111111111/saved-views/sview_K11111111111
sview_K11111111111	Checkout gateway, last 24h	personal	relative	overview	revision 1
cloud: this is the stored QUESTION and not its answer; `cloud view open` answers it under your own scope

Replayed from the recorded case report-view-get: a view saved at revision 1 that asks about one checkout gateway over a rolling 24 hours.

The revision is a decimal string. Copy it as it is printed.

2. Create the report

Write the report document to report.json. It names the view and the revision you read, and lists nobody else yet:

{
  "name": "Gateway daily report",
  "saved_view_id": "sview_…",
  "expected_view_revision": "1",
  "recipient_subject_ids": [],
  "evidence_refs": []
}

Create the report with tokentriage cloud report create --org … --workspace … --file report.json --idempotency-key gateway-daily-report-01. On stdout, the first line is the definition and the second is its first run, which starts queued:

$ tokentriage cloud report create --org org_K11111111111 --workspace t_K11111111111 \
    --file report.json --idempotency-key gateway-daily-report-01 \
    --base-url http://127.0.0.1:13117
cloud: customer.reports.create POST http://127.0.0.1:13117/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/reports (Idempotency-Key gateway-daily-report-01)
rpt_W11111111111	Gateway daily report	view sview_K11111111111@1	latest run rrun_W11111111111
rrun_W11111111111	queued	[2026-09-20T12:00:00Z, 2026-09-21T12:00:00Z)	groups 0	

Replayed from report-create, recorded with the service clock at 2026-09-21T12:00Z, which is where the run’s window ends.

Keep the rpt_… and rrun_… IDs; every later step uses them. If the command times out, send it again with the same --idempotency-key. The server answers with the report it already made instead of a second one.

If the create answers HTTP 412 precondition_failed

Somebody edited the view after you read it. Nothing was created, and the refusal names both revisions:

$ tokentriage cloud report create --org org_K11111111111 --workspace t_K11111111111 \
    --file report.json --idempotency-key gateway-daily-report-03 \
    --base-url http://127.0.0.1:9272
cloud: customer.reports.create POST http://127.0.0.1:9272/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/reports (Idempotency-Key gateway-daily-report-03)
Error: customer.reports.create was refused as a stale revision (HTTP 412); detail: foundation: stale revision: saved view sview_K11111111111 is at revision 2 and the report expected 1; type=precondition_failed; request_id=req_report-create-stale-revision

Replayed from report-create-stale-revision, recorded after the view was edited to revision 2, with a new key: the same key and body would have answered the first report instead of checking the view again.

Read the view again, look at what changed, and create the report with the new revision.

If the create answers HTTP 503 dependency_unavailable

This deployment has no report worker, store or reader running, so a report would be saved but would not run. Nothing was created. Your saved view still opens. Ask whoever runs the deployment to enable reporting, then create the report again with the same key.

$ tokentriage cloud report create --org org_K11111111111 --workspace t_K11111111111 \
    --file report.json --idempotency-key gateway-daily-report-01 \
    --base-url http://127.0.0.1:13196
cloud: customer.reports.create POST http://127.0.0.1:13196/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/reports (Idempotency-Key gateway-daily-report-01)
Error: customer.reports.create was refused (HTTP 503); detail: this control plane composes no report store, analytics reader and report worker, so no report can be saved, run or opened here; the request id identifies this attempt in its logs; type=dependency_unavailable; request_id=req_report-create-unavailable

Replayed from report-create-unavailable, recorded against a control plane that composes no reports. The server sent no Retry-After header with this 503.

3. Wait for the run to settle

A worker picks the run up and answers the view’s question for the window it resolved when the run was created. Poll it with tokentriage cloud report show --org … --report rpt_… --run rrun_… until its state is ready, partial, failed or cancelled:

$ tokentriage cloud report show --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_W11111111111 --base-url http://127.0.0.1:13217
cloud: customer.reports.runs.get GET http://127.0.0.1:13217/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_W11111111111
rrun_W11111111111	queued	[2026-09-20T12:00:00Z, 2026-09-21T12:00:00Z)	groups 0	

Replayed from report-show-queued, recorded before any worker had claimed the run.

$ tokentriage cloud report show --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_W11111111111 --base-url http://127.0.0.1:13219
cloud: customer.reports.runs.get GET http://127.0.0.1:13219/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_W11111111111
rrun_W11111111111	ready	[2026-09-20T12:00:00Z, 2026-09-21T12:00:00Z)	groups 1	sha256:ceccd02ca24b008b84416282a088f37133e7d8646e830c54de3d84a2f01284d0

Replayed from report-show-ready, recorded after one worker pass and opened by the report’s owner, who holds cost.read.

The line gives the run’s state, its half-open UTC window [from, to), the number of groups and the artifact digest. Coverage reasons, truncation and withheld money are stated on stderr, under the line they qualify.

If the run ends failed

A failed run keeps no answer, and the second line names the reason. scope_unavailable means the access of the person who started the run no longer covered its scope when it ran; output_cap_exceeded means the answer passed 1,000 groups or 10 MiB; attempts_exhausted means five transient failures in a row. Fix the cause, then run the report again as in step 5. The failed run stays as it is.

$ tokentriage cloud report show --org org_K11111111111 --report rpt_Y11111111111 \
    --run rrun_Y11111111111 --base-url http://127.0.0.1:13221
cloud: customer.reports.runs.get GET http://127.0.0.1:13221/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_Y11111111111/runs/rrun_Y11111111111
rrun_Y11111111111	failed	[2026-09-20T12:00:00Z, 2026-09-21T12:00:00Z)	groups 0	
failed	output_cap_exceeded	the answer holds more than 1000 rows

Replayed from report-show-failed, recorded for a second report over the same view whose test reader answered 1,001 groups.

If show answers HTTP 404 not_found

The server does not tell you whether the report is missing or hidden from you. Check the IDs and the organization. Then check that you own the report or are a recipient, and that your metadata.read still covers the view’s whole scope.

$ tokentriage cloud report show --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_W11111111111 --base-url http://127.0.0.1:13223
cloud: customer.reports.runs.get GET http://127.0.0.1:13223/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_W11111111111
Error: customer.reports.runs.get was refused as not found (HTTP 404): the lane conceals organizations and resources the caller has no binding for, so this can mean it does not exist OR that you cannot see it; detail: foundation: not found: no report rpt_W11111111111; type=not_found; request_id=req_report-show-not-found

Replayed from report-show-not-found, recorded as a workspace administrator who is neither the owner nor a recipient. An ID that does not exist gets the same answer.

4. Open the answer

The same command with --json prints the whole run document, including the answer, the coverage and the two digests:

$ tokentriage cloud report show --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_W11111111111 --json --base-url http://127.0.0.1:13225
cloud: customer.reports.runs.get GET http://127.0.0.1:13225/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_W11111111111
{
  "schema": "customer/v1",
  "request_id": "req_report-show-json",
  "kind": "opened-report-run",
  "data": {
    "run_id": "rrun_W11111111111",
    "report_id": "rpt_W11111111111",
    "organization_id": "org_K11111111111",
    "workspace_id": "t_K11111111111",
    "initiator": "owner",
    "idempotency_key": "gateway-daily-report-01",
    "state": "ready",
    "failure_code": "",
    "failure_detail": "",
    "question": {
      "schema": 1,
      "shape": "overview",
      "scope": {
        "organization_id": "org_K11111111111",
        "workspace_id": "t_K11111111111",
        "application_ids": [
          "app_K11111111111"
        ],
        "gateway_ids": [
          "gw_K11111111111"
        ],
        "environment_ids": [
          "env_K11111111111",
          "env_M11111111111"
        ]
      },
      "from": "2026-09-20T12:00:00Z",
      "to": "2026-09-21T12:00:00Z",
      "display_tz": "UTC",
      "purposes": [
        "application"
      ],
      "metrics": [
        "requests.v1",
        "provider_spend.v1",
        "cost_per_1000_requests.v1"
      ]
    },
    "view_revision": "1",
    "window": {
      "from": "2026-09-20T12:00:00Z",
      "to": "2026-09-21T12:00:00Z"
    },
    "display_tz": "UTC",
    "authority_epoch": 14,
    "attempt": 1,
    "coverage": [],
    "truncated": false,
    "artifact_digest": "sha256:ceccd02ca24b008b84416282a088f37133e7d8646e830c54de3d84a2f01284d0",
    "group_count": 1,
    "byte_count": 1156,
    "created_at": "2026-09-21T12:00:00Z",
    "started_at": "2026-09-21T12:00:00Z",
    "finished_at": "2026-09-21T12:00:00Z",
    "expires_at": "2026-10-21T12:00:00Z",
    "answer": {
      "read_context": {
        "schema": 1,
        "central_account_id": "acct_K11111111111",
        "workspace_id": "t_K11111111111",
        "scope_digest": "f644d618df3314b70c1de5a5cc3d3da82b31dc31af3917ac8af4ab59279e2800",
        "authority_epoch": "1",
        "catalog_version": 1,
        "filter_version": 1,
        "projection_generation": "4",
        "retention_generation": "1",
        "exclusion_generation": "7",
        "from": "2026-09-20T12:00:00Z",
        "to": "2026-09-21T12:00:00Z",
        "display_tz": "UTC",
        "purposes": [
          "application"
        ],
        "arrival_fence": "918273",
        "watermarks": [
          {
            "producer_class": "gateway_journal",
            "produced_through": "918290",
            "indexed_through": "918273",
            "last_indexed_at": "2026-09-21T11:58:30Z"
          },
          {
            "producer_class": "control_plane_outbox",
            "produced_through": "41902",
            "indexed_through": "41902",
            "last_indexed_at": "2026-09-21T11:59:48Z"
          }
        ],
        "evaluated_at": "2026-09-21T12:00:00Z"
      },
      "minted": true,
      "shape": "overview",
      "groups": [
        {
          "purpose": "application",
          "cells": [
            {
              "metric": "requests.v1",
              "value": {
                "state": "available",
                "number": "1284",
                "unit": "count"
              }
            },
            {
              "metric": "provider_spend.v1",
              "value": {
                "state": "unavailable",
                "reason": "scope_not_authorized"
              }
            },
            {
              "metric": "cost_per_1000_requests.v1",
              "value": {
                "state": "unavailable",
                "reason": "scope_not_authorized"
              }
            }
          ]
        }
      ]
    },
    "presentation_digest": "sha256:ca85fdd9098d6278a96b6be763de0c98a669e50999083d02e20fe3570d64c3d1",
    "money_withheld": true
  }
}

Replayed from report-show-json, recorded for a viewer without cost.read. No role in this release grants metadata.read without cost.read, so the recording applied the service’s own money redaction through a test wrapper.

Three things in that document tell you what you are holding. window is the exact UTC window the run measured, and display_tz only labels it. coverage lists anything the reader could not see; an empty list means the window was complete. And this document was opened without cost.read, so money_withheld is true, both money cells read unavailable with the reason scope_not_authorized and no figure, and presentation_digest differs from artifact_digest: it hashes what you received, not the stored answer. With cost.read, money_withheld is false and the two digests are equal.

Open it again tomorrow and you’ll see the same window and the same artifact_digest.

5. Run it again

“Run again” is a new run with a new window. It does not refresh the first one. Run it with tokentriage cloud report run --org … --report rpt_… --idempotency-key gateway-daily-report-02, then poll the new run as in step 3:

$ tokentriage cloud report run --org org_K11111111111 --report rpt_W11111111111 \
    --idempotency-key gateway-daily-report-02 --base-url http://127.0.0.1:13240
cloud: customer.reports.runs.create POST http://127.0.0.1:13240/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs (Idempotency-Key gateway-daily-report-02)
rrun_X11111111111	queued	[2026-09-21T12:00:00Z, 2026-09-22T12:00:00Z)	groups 0	

Replayed from report-run-again, recorded with the service clock moved forward 24 hours.

$ tokentriage cloud report show --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_X11111111111 --base-url http://127.0.0.1:13250
cloud: customer.reports.runs.get GET http://127.0.0.1:13250/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_X11111111111
rrun_X11111111111	partial	[2026-09-21T12:00:00Z, 2026-09-22T12:00:00Z)	groups 1	sha256:4d600cc9385d1fa5010d6ed038d164baf20e5ed089d8b51f0cb0f07a8fc0e9d1
coverage  ingest_gap:gateway_journal

Replayed from report-show-second-run; the test reader answered that day with a journal gap and part of the traffic unpriced.

The new run has its own rrun_… ID, window and digest. This one settled partial, and stderr names the reason: the journal had a gap that day. cloud report show on the first run still answers what it answered in step 4.

If run answers HTTP 412 precondition_failed

The view has been edited since the report was created, and the report pins the revision it was created at. A report never answers a question it was not created with. Create a new report from the view’s current revision, as in steps 1 and 2.

$ tokentriage cloud report run --org org_K11111111111 --report rpt_W11111111111 \
    --idempotency-key gateway-daily-report-04 --base-url http://127.0.0.1:3366
cloud: customer.reports.runs.create POST http://127.0.0.1:3366/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs (Idempotency-Key gateway-daily-report-04)
Error: customer.reports.runs.create was refused as a stale revision (HTTP 412); detail: foundation: stale revision: saved view sview_K11111111111 is at revision 2 and this report pins 1; type=precondition_failed; request_id=req_report-run-stale-revision

Replayed from report-run-stale-revision, recorded after the view was edited to revision 2. report run takes no --expected-revision, so the CLI prints the server’s detail as it came; the remedy is a new report.

6. Export a run

Download the first run as CSV with tokentriage cloud report export --org … --report rpt_… --run rrun_… --format csv --output report.csv. The file is written verbatim, and stderr states its content type and a sha256 of the bytes that arrived:

$ tokentriage cloud report export --org org_K11111111111 --report rpt_W11111111111 \
    --run rrun_W11111111111 --format csv --output report.csv \
    --base-url http://127.0.0.1:13256
cloud: customer.reports.runs.export GET http://127.0.0.1:13256/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_W11111111111/runs/rrun_W11111111111/export?format=csv
cloud: exported 465 bytes (text/csv; charset=utf-8) to report.csv digest sha256:6872a0494f05cbda406f6f83715098d6e336ed8cede74c12c8a5ea8ec5c937c0
report.csv

Replayed from report-export-csv, exported by the owner, who holds cost.read, so the file’s spend cells carry nano-USD figures.

That digest hashes the file you downloaded. It is not the run’s artifact_digest, which hashes the stored answer. --format json writes the same run document show --json printed.

7. Do the same from a program

Both SDKs ship steps 1 to 4 as one example: read the view’s revision, create the report with an idempotency key, poll until the run settles (at most 60 polls, honouring Retry-After), and print the run’s window, coverage and artifact digest. This is the function, quoted from sdk/python/examples/create_gateway_report.py and sdk/ts/examples/create_gateway_report.ts:

def create_gateway_report(
    client: CustomerClient,
    org: str,
    ws: str,
    view: str,
    *,
    idempotency_key: str,
    retry_after: Callable[[], Optional[float]] = lambda: None,
    sleep: Callable[[float], None] = time.sleep,
) -> Mapping[str, Any]:
    # The revision is a uint64 carried as a STRING; it is copied, never parsed.
    revision = client.saved_views.get(org, view)["data"]["revision"]
    created = client.reports.create(
        org,
        ws,
        {
            "name": "Gateway daily report",
            "saved_view_id": view,
            "expected_view_revision": revision,
            "recipient_subject_ids": [],
            "evidence_refs": [],
        },
        idempotency_key=idempotency_key,
    )["data"]
    report_id, run_id = created["report"]["report_id"], created["run"]["run_id"]
    for _ in range(MAX_POLLS):
        run = client.reports.runs.get(org, report_id, run_id)["data"]
        if run["state"] in SETTLED:
            break
        delay = retry_after()
        sleep(DEFAULT_DELAY if delay is None else delay)
    else:
        raise TimeoutError(f"report run {run_id} is still {run['state']}; reopen it later")
    if run["state"] not in ("ready", "partial"):
        raise RuntimeError(f"report run {run_id} is {run['state']}: {run.get('failure_code')}")
    return {
        "report_id": report_id,
        "run_id": run_id,
        "state": run["state"],
        "coverage": run.get("coverage", []),
        "window": run["window"],
        "artifact_digest": run["artifact_digest"],
    }

Each script reads TOKENTRIAGE_BASE_URL, TOKENTRIAGE_MANAGEMENT_KEY, TOKENTRIAGE_ORG, TOKENTRIAGE_WORKSPACE and TOKENTRIAGE_SAVED_VIEW, and prints one JSON line:

$ python create_gateway_report.py
{"report_id": "rpt_W11111111111", "run_id": "rrun_W11111111111", "state": "ready", "coverage": [], "window": {"from": "2026-09-20T12:00:00Z", "to": "2026-09-21T12:00:00Z"}, "artifact_digest": "sha256:ceccd02ca24b008b84416282a088f37133e7d8646e830c54de3d84a2f01284d0"}

Run with Python 3.14 against a loopback server answering the four requests recorded in sdk-python-create-gateway-report. The poll that found the run queued carried no Retry-After, so the script waited its default two seconds.

$ node create_gateway_report.mjs
{"report_id":"rpt_W11111111111","run_id":"rrun_W11111111111","state":"ready","coverage":[],"window":{"from":"2026-09-20T12:00:00Z","to":"2026-09-21T12:00:00Z"},"artifact_digest":"sha256:ceccd02ca24b008b84416282a088f37133e7d8646e830c54de3d84a2f01284d0"}

Bundled with esbuild and run with Node 24 against the four requests recorded in sdk-ts-create-gateway-report.

A failed or cancelled run raises with its failure code instead of returning, and a run still unsettled after 60 polls raises with its ID so you can reopen it later.

What a report does not do yet

A report runs when you create it, when you run it again, or when a schedule runs it. Deliver a gateway report on a schedule sets one up and delivers each scheduled run to its recipients’ in-app inboxes. A report raises no alerts. Listing a recipient lets them see the report and grants them nothing else, so a recipient without access to the view’s scope still cannot open it. Reporting and alerts lists every field, state, problem code and limit.