# Deliver a gateway report on a schedule

Run one of your reports every morning at a local time you choose, and have each dated run land in a colleague's in-app inbox.

Source: https://tokentriage.com/docs/build/schedule-gateway-report

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

Your team lead opens the checkout gateway report every morning before stand-up. Today you run it
again by hand for them each day, and on the day you forget, they read yesterday's numbers without
knowing it. You want the report to run on its own at nine o'clock their time, and to be waiting
for them when they sit down.

In this tutorial we'll schedule a report you already own, read when it will run next, check what
ran, open the delivered run as the recipient, then pause the schedule and delete it. By the end
you'll have a dated run in a colleague's inbox that they opened with their own access, and a
history that says when it ran and over which window.

Delivery is to the in-app inbox only. A schedule sends no email and no Slack message: a recipient
sees what arrived by listing their inbox in the product, the CLI or an SDK.

[What a report run keeps, and who can open it](/docs/concepts/report-snapshots-and-access)
explains why each run keeps its own answer, and
[Keep a dated report of a gateway view](/docs/build/create-gateway-report) makes the report this
page schedules.

Every output block on this page is program output, captured by replaying the recorded answers in
`sdk/spec/report-schedule-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 schedule handler, dispatcher, inbox and report worker; the
analytics answer inside each run comes from a test reader that stands in for the analytics
service. In the recording, `it-lead` owns the report and the schedule, and `it-analyst` is the
colleague. 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` | the owner's steps run as you |
| an organization ID (`org_…`) and a workspace ID (`t_…`) | schedules belong to one workspace |
| a report you own (`rpt_…`) made from a view with a relative window | only the report's owner can schedule it, and only a relative window repeats |
| your colleague listed as a recipient of that report, and their subject ID | a schedule delivers only to the report's owner or its recipients |
| your colleague's own management key, for step 5 | an inbox is read with its owner's credential |
| `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.
You don't need a runtime key, and nothing here calls a model provider.

A report's recipients are fixed when it is created. If yours lists nobody, create a new report
that names your colleague in `recipient_subject_ids`, as in step 2 of
[Keep a dated report of a gateway view](/docs/build/create-gateway-report).
`tokentriage cloud member list` shows each member's subject ID if you hold `members.manage`;
otherwise ask your colleague for theirs.

## 1. Read the report's revision

A schedule pins the revision of the report you read, so a report that changed while you were
writing the schedule is caught instead of scheduled. The report's own `revision` is in its JSON
document, so read it with `tokentriage cloud report get --org … --report rpt_… --json`:

```console
$ tokentriage cloud report get --org org_K11111111111 --report rpt_S11111111111 --json \
    --base-url http://127.0.0.1:6976
cloud: customer.reports.get GET http://127.0.0.1:6976/portal/api/v1/customer/v1/organizations/org_K11111111111/reports/rpt_S11111111111
{
  "schema": "customer/v1",
  "request_id": "req_schedule-report-get",
  "kind": "report",
  "data": {
    "report_id": "rpt_S11111111111",
    "organization_id": "org_K11111111111",
    "workspace_id": "t_K11111111111",
    "owner": "it-lead",
    "name": "Gateway daily report",
    "saved_view_id": "sview_K11111111111",
    "view_revision": "1",
    "recipient_subject_ids": [
      "it-analyst"
    ],
    "evidence_refs": [],
    "revision": "1",
    "latest_run_id": "rrun_S11111111111",
    "created_at": "2026-09-21T12:00:00Z",
    "updated_at": "2026-09-21T12:00:00Z"
  }
}
```

Replayed from the recorded case `schedule-report-get`: the report `Gateway daily report`,
owned by `it-lead` and made from the checkout gateway's rolling 24-hour view, with
`it-analyst` as its one recipient. Its first run was made by hand when the report was created.

Copy `data.revision` as it is printed. It is a decimal string, not a number. `recipient_subject_ids`
in the same document lists the people you can name on the schedule.

## 2. Create the schedule

Write the schedule document to `schedule.json`. It names the report and the revision you read,
runs daily at 09:00 Los Angeles time, and delivers to your colleague:

```json
{
  "report_id": "rpt_…",
  "expected_report_revision": "1",
  "cadence": {
    "mode": "wall_clock",
    "frequency": "daily",
    "local_time": "09:00",
    "timezone": "America/Los_Angeles"
  },
  "recipient_subject_ids": ["<colleague's subject ID>"],
  "paused": false
}
```

Create it with `tokentriage cloud report schedule create --org … --workspace … --file
schedule.json --idempotency-key gateway-daily-schedule-01`:

```console
$ tokentriage cloud report schedule create --org org_K11111111111 \
    --workspace t_K11111111111 --file schedule.json \
    --idempotency-key gateway-daily-schedule-01 --base-url http://127.0.0.1:6978
cloud: customer.report_schedules.create POST http://127.0.0.1:6978/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/report-schedules (Idempotency-Key gateway-daily-schedule-01)
rsch_S11111111111	report rpt_S11111111111	armed	revision 1	next run 2026-09-21T16:00:00Z
next	2026-09-21T16:00:00Z	2026-09-21T09:00:00-07:00
next	2026-09-22T16:00:00Z	2026-09-22T09:00:00-07:00
next	2026-09-23T16:00:00Z	2026-09-23T09:00:00-07:00
```

Replayed from `schedule-create`, recorded with the service clock at 2026-09-21T12:00Z, which
is 05:00 in Los Angeles, so the first occurrence is 09:00 that morning. The recording runs the
dispatcher, the report worker, the reader and the inbox, so the state is `armed`.

The first line is the schedule: its `rsch_…` ID, the report, its execution state, its revision
and the next run in UTC. The `next` lines are the next three occurrences, each as a UTC instant
and as the local time in your timezone. Keep the `rsch_…` ID; every later step uses it.

Read the execution state before you rely on the schedule. A saved schedule is not a promise that
it will run:

| `execution_state` | What it means |
|---|---|
| `armed` | the dispatcher, the report worker, the reader and the inbox are all running, so the next occurrence will run |
| `paused` | nothing runs until the owner resumes it; stderr names the reason |
| `unavailable` | the schedule is saved, but this deployment lacks one of those four parts; stderr names which |

If the command times out, send it again with the same `--idempotency-key`. The server answers with
the schedule it already made instead of a second one.

<Details summary="If the create answers HTTP 422 validation_failed with fixed_view_not_schedulable">

The report was made from a view with a fixed window, such as 1 to 7 September. A fixed view asks
about one stretch of history, so running it every morning would deliver the same window again and
again, and nobody could tell which question the schedule repeats. Nothing was saved.

```console
$ tokentriage cloud report schedule create --org org_K11111111111 \
    --workspace t_K11111111111 --file fixed-schedule.json \
    --idempotency-key gateway-fixed-schedule-01 --base-url http://127.0.0.1:6980
cloud: customer.report_schedules.create POST http://127.0.0.1:6980/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/report-schedules (Idempotency-Key gateway-fixed-schedule-01)
Error: customer.report_schedules.create was refused (HTTP 422); detail: foundation: unsupported combination: cadence: fixed_view_not_schedulable (view): a fixed-window view cannot be scheduled; create a relative view or run a report manually; type=validation_failed; request_id=req_schedule-create-fixed-view
```

Replayed from `schedule-create-fixed-view`, recorded for a second report, made from a view
over 1 to 7 September and named in `fixed-schedule.json`, with a new key. The problem type is
`validation_failed`, and the detail carries the code `fixed_view_not_schedulable`.

Save a view with a relative window, such as the last 24 hours, create a report from it, and
schedule that report. To see the fixed window again, run the existing report by hand with
`tokentriage cloud report run`.

</Details>

<Details summary="If the create answers HTTP 503 dependency_unavailable">

This deployment has no schedule store, dispatcher or report worker running, so a schedule could
not be saved or read. Nothing was saved, and your report still opens and runs by hand. Ask whoever
runs the deployment to enable scheduled reports, then create the schedule again with the same key.

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

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

A 503 means the schedule lane is missing entirely. A schedule that was saved but reads
`unavailable` is a different case: the lane is there, and one of the parts that runs it is not.

</Details>

## 3. Check when it will run

A daylight-saving change moves a schedule's UTC times, so read them before you wait for one. `tokentriage cloud report schedule get --org … --schedule rsch_…` prints the same
line and the next three occurrences, computed again from the current time:

```console
$ tokentriage cloud report schedule get --org org_K11111111111 \
    --schedule rsch_S11111111111 --base-url http://127.0.0.1:7025
cloud: customer.report_schedules.get GET http://127.0.0.1:7025/portal/api/v1/customer/v1/organizations/org_K11111111111/report-schedules/rsch_S11111111111
rsch_S11111111111	report rpt_S11111111111	armed	revision 1	next run 2026-09-21T16:00:00Z
next	2026-09-21T16:00:00Z	2026-09-21T09:00:00-07:00
next	2026-09-22T16:00:00Z	2026-09-22T09:00:00-07:00
next	2026-09-23T16:00:00Z	2026-09-23T09:00:00-07:00
```

Replayed from `schedule-get-next-occurrences`, recorded straight after the create, before
the first occurrence.

Each `next` line pairs the UTC instant with the local time in `America/Los_Angeles`. Across a
daylight-saving change the UTC instant moves and the local time stays at 09:00, because a
`wall_clock` cadence follows the local clock.

A third column appears only on an occurrence a clock change touches. `dst_gap` means the local time
did not exist that day, so the occurrence runs later by the length of the gap: a 02:30 schedule
runs at 03:30 on the morning the clocks go forward an hour. `dst_fold` means the local time
happened twice, and the schedule runs once, at the earlier of the two.

<Details summary="Other cadences: hourly, weekly, and a fixed interval">

A cadence has one of two modes, and a schedule keeps its mode until an update names a different
one. There is no cron expression.

| `mode` | `frequency` | Other fields | Runs |
|---|---|---|---|
| `wall_clock` | `daily` | `local_time` `HH:MM`, IANA `timezone` | once per local calendar day at that time |
| `wall_clock` | `weekly` | `local_time`, `timezone`, `weekday` (`monday` to `sunday`) | once per local week |
| `interval` | `hourly`, `daily` or `weekly` | optional `anchor` (RFC 3339), optional `timezone` | every 1, 24 or 168 hours of elapsed time from the anchor |

A weekly schedule on Monday mornings:

```json
{"mode": "wall_clock", "frequency": "weekly", "local_time": "08:30",
 "timezone": "Europe/Berlin", "weekday": "monday"}
```

An `interval` cadence counts elapsed time from its `anchor`, which defaults to the moment you
create the schedule. Its `timezone` only labels the local side of each `next` line, so a daily
interval that runs at 09:00 in winter runs at 10:00 local time in summer. Hourly exists only as
an interval: `wall_clock` with `hourly` is answered 422 with `hourly_requires_interval`.

</Details>

## 4. See what ran

After the first occurrence's time has passed, list the schedule's history with
`tokentriage cloud report schedule occurrences --org … --schedule rsch_…`. This is what ran, newest
first, and not a forecast:

```console
$ tokentriage cloud report schedule occurrences --org org_K11111111111 \
    --schedule rsch_S11111111111 --base-url http://127.0.0.1:7027
cloud: customer.report_schedules.occurrences GET http://127.0.0.1:7027/portal/api/v1/customer/v1/organizations/org_K11111111111/report-schedules/rsch_S11111111111/occurrences
rocc_S11111111111	2026-09-21T16:00:00Z	delivered	[2026-09-20T16:00:00Z, 2026-09-21T16:00:00Z)	run rrun_T11111111111
```

Replayed from `schedule-occurrences`, recorded after the 09:00 occurrence on 21 September. In
the recording the dispatcher and the report worker were stepped one pass at a time instead of
running on their own: the dispatcher claimed the occurrence at 16:00:30Z and queued its run,
the worker settled the run, and the dispatcher delivered it at 16:01:30Z. The window still
ends at 16:00Z.

Each line is one occurrence: its `rocc_…` ID, the instant it was scheduled for, its state, the
window its run measured and the `rrun_…` it made. The window ends at the scheduled instant, not at
the moment a worker got to it, and it spans the view's relative window in elapsed time. A
"last 24 hours" view gives `[09:00 yesterday, 09:00 today)` in UTC terms, and stays 24 hours long
on the night the clocks change. A retry keeps the same window.

`delivered` means the run settled and every recipient who can still open it has an item in their
inbox. `failed` carries a code on the next line. `authority_lost` means you, the owner, no longer
see the view's whole scope, so the schedule pauses with that reason and runs nothing more until you
resume it.

If the schedule could not run on time, for example while the deployment was down, it runs once for
the latest time it owed within the last 24 hours, and a `skipped` line counts the older times it
passed over. If even the latest owed time is more than 24 hours old, the occurrence is `missed`: it
has no window and no run, its `skipped` line gives the count and range, and the schedule moves on
to its next future time.

Only one scheduled report runs at a time in a workspace. If another schedule in the workspace is
mid-run at 09:00, yours waits until it finishes. The delivery arrives later, and its window still
ends at 09:00.

## 5. Open the delivery as the recipient

Your colleague reads their own inbox with their own management key. Being named a recipient lets
nobody open anything on their behalf, including you. They run
`tokentriage cloud report inbox list --org … --workspace …`:

```console
$ tokentriage cloud report inbox list --org org_K11111111111 --workspace t_K11111111111 \
    --base-url http://127.0.0.1:7029
cloud: customer.report_inbox.list GET http://127.0.0.1:7029/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/report-inbox
rdlv_S11111111111	report_delivered	2026-09-21T16:01:30Z	report rpt_S11111111111	run rrun_T11111111111	unread
```

Replayed from `inbox-list`, recorded as `it-analyst` with the clock at 16:20Z, which is 09:20
in Los Angeles.

An item carries identifiers only: the `rdlv_…` delivery, the event type, when it arrived, the
report and the run, and whether it has been read. No numbers travel in the inbox. They open the
item with `tokentriage cloud report inbox open --org … --delivery rdlv_…`:

```console
$ tokentriage cloud report inbox open --org org_K11111111111 --delivery rdlv_S11111111111 \
    --base-url http://127.0.0.1:7031
cloud: customer.report_inbox.open POST http://127.0.0.1:7031/portal/api/v1/customer/v1/organizations/org_K11111111111/report-inbox/rdlv_S11111111111/open
rdlv_S11111111111	report_delivered	2026-09-21T16:01:30Z	report rpt_S11111111111	run rrun_T11111111111	read 2026-09-21T16:20:00Z
rrun_T11111111111	ready	[2026-09-20T16:00:00Z, 2026-09-21T16:00:00Z)	groups 1	sha256:4781e1181553fcb188615c3a47a582e013e3b2e3aac7f4aa4e6cd120accf4871
```

Replayed from `inbox-open`, recorded as `it-analyst` straight after the list. `it-analyst`
holds `cost.read`, so no money cell is withheld and stderr says nothing more.

The first line is the item, now marked read. The rest is the run, as `cloud report show` prints it
for your colleague: their own access is checked again when they open it, and money cells are
withheld if they lack `cost.read`. Opening it again shows the same run and keeps the first read
time.

<Details summary="If inbox open answers HTTP 404 not_found">

The status and type are the same whether the item is missing or no longer yours to open. The
usual cause is that your colleague's access changed after the delivery: they were removed from the
report, or their `metadata.read` no longer covers its whole scope. Nothing was recorded as read.

```console
$ tokentriage cloud report inbox open --org org_K11111111111 --delivery rdlv_T11111111111 \
    --base-url http://127.0.0.1:7033
cloud: customer.report_inbox.open POST http://127.0.0.1:7033/portal/api/v1/customer/v1/organizations/org_K11111111111/report-inbox/rdlv_T11111111111/open
Error: customer.report_inbox.open 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 run rrun_U11111111111; type=not_found; request_id=req_inbox-open-not-found
```

Replayed from `inbox-open-not-found`, recorded as `it-analyst` for the next morning's
delivery, after their analyst role in the workspace was revoked. The detail names the run
they can no longer open. An item that does not exist, or that belongs to somebody else, gets
the same status and type with a detail that names the delivery instead.

A recipient who has already lost access when an occurrence runs gets no item for it at all, and
the other recipients are unaffected. Restore their access, and the next occurrence delivers to them
again.

</Details>

## 6. Pause and resume it

Pausing stops future runs and cancels one that is queued or running. An update replaces the whole
editable document, so write all three members to `pause.json`:

```json
{
  "cadence": {
    "mode": "wall_clock",
    "frequency": "daily",
    "local_time": "09:00",
    "timezone": "America/Los_Angeles"
  },
  "recipient_subject_ids": ["<colleague's subject ID>"],
  "paused": true
}
```

Send it with the revision `schedule get` printed:
`tokentriage cloud report schedule update --org … --schedule rsch_… --expected-revision 1 --file
pause.json`.

```console
$ tokentriage cloud report schedule update --org org_K11111111111 \
    --schedule rsch_S11111111111 --expected-revision 1 --file pause.json \
    --base-url http://127.0.0.1:7036
cloud: customer.report_schedules.update PUT http://127.0.0.1:7036/portal/api/v1/customer/v1/organizations/org_K11111111111/report-schedules/rsch_S11111111111
rsch_S11111111111	report rpt_S11111111111	paused	revision 2	next run -
paused  manual
next	2026-09-23T16:00:00Z	2026-09-23T09:00:00-07:00
next	2026-09-24T16:00:00Z	2026-09-24T09:00:00-07:00
next	2026-09-25T16:00:00Z	2026-09-25T09:00:00-07:00
```

Replayed from `schedule-update-pause`, recorded the day after the create, when both mornings'
occurrences had run. Running an occurrence does not change the schedule's revision, so the
revision step 3 printed was still current. `next run -` means nothing will run; the `next`
lines are the times the schedule would run if you resumed it now.

The state reads `paused`, stderr says `manual`, and the revision moved up by one. To resume, send
the same document with `"paused": false` and the new revision. A resumed schedule starts at its
next time after the moment you resume it and does not run the occurrences it skipped while paused.
A cadence edit does the same.

If the update answers HTTP 412 `precondition_failed`, somebody changed the schedule after you read
it. Read it again with `schedule get`, apply your change to what it now holds, and send the new
revision.

## 7. Delete it

`tokentriage cloud report schedule delete --org … --schedule rsch_…` retires the schedule and
cancels any run it has in flight:

```console
$ tokentriage cloud report schedule delete --org org_K11111111111 \
    --schedule rsch_S11111111111 --base-url http://127.0.0.1:7038
cloud: customer.report_schedules.delete DELETE http://127.0.0.1:7038/portal/api/v1/customer/v1/organizations/org_K11111111111/report-schedules/rsch_S11111111111
rsch_S11111111111	deleted 2026-09-22T16:01:30Z	occurrences cancelled 0
```

Replayed from `schedule-delete`, recorded straight after the pause, with nothing in flight.

The line names the schedule, when it was deleted, and how many occurrences were cancelled. The
schedule no longer appears in any read. Items already delivered stay in each recipient's inbox,
and the report itself is unchanged: you can still run it by hand.

## 8. Do the same from a program

Both SDKs ship steps 2 and 5 as one example: create the schedule as the report's owner, print its
execution state and next occurrences, then list the recipient's inbox with the recipient's own
client and open the first item if there is one. This is the function, quoted from
`sdk/python/examples/schedule_gateway_report.py` and `sdk/ts/examples/schedule_gateway_report.ts`:

<CodeTabs>
  <Fragment slot="python">
  ```python
  def schedule_gateway_report(
      owner: CustomerClient,
      recipient: CustomerClient,
      org: str,
      ws: str,
      body: Mapping[str, Any],
      *,
      idempotency_key: str,
      out: Callable[[str], None] = print,
  ) -> Mapping[str, Any]:
      schedule = owner.report_schedules.create(
          org, ws, dict(body), idempotency_key=idempotency_key
      )["data"]
      # `armed` only when every dependency is mounted; `unavailable` names what is missing.
      out(f"schedule {schedule['schedule_id']} is {schedule['execution_state']}")
      for occurrence in schedule.get("next_occurrences", []):
          note = f" ({occurrence['note']})" if occurrence.get("note") else ""
          out(f"  next {occurrence['scheduled_utc']} = {occurrence['local']}{note}")

      # The recipient's OWN credential: selecting them as a recipient does not let the owner
      # open anything on their behalf.
      items = recipient.report_inbox.list(org, ws)["data"].get("items", [])
      if not items:
          out(NO_DELIVERY)
          return {"schedule_id": schedule["schedule_id"], "opened": None}
      opened = recipient.report_inbox.open(org, items[0]["delivery_id"])["data"]
      run = opened.get("run") or {}
      out(f"opened {opened['item']['delivery_id']}: run {run.get('run_id')} is {run.get('state')}")
      out(f"  coverage {json.dumps(run.get('coverage', []))}")
      return {"schedule_id": schedule["schedule_id"], "opened": opened}
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```typescript
  export async function scheduleGatewayReport(
    owner: CustomerClient,
    recipient: CustomerClient,
    org: string,
    ws: string,
    body: ReportScheduleCreateInput,
    options: ScheduleReportOptions,
  ): Promise<{ schedule_id: string; opened: OpenedInboxItemV1 | null }> {
    const out = options.out ?? ((line: string) => console.log(line));
    const { data: schedule } = await owner.reportSchedules.create(org, ws, body, {
      idempotencyKey: options.idempotencyKey,
    });
    const scheduleId = schedule?.schedule_id ?? "";
    // `armed` only when every dependency is mounted; `unavailable` names what is missing.
    out(`schedule ${scheduleId} is ${schedule?.execution_state ?? ""}`);
    for (const occurrence of schedule?.next_occurrences ?? []) {
      const note = occurrence.note ? ` (${occurrence.note})` : "";
      out(`  next ${occurrence.scheduled_utc ?? ""} = ${occurrence.local ?? ""}${note}`);
    }

    // The recipient's OWN credential: selecting them as a recipient does not let the owner
    // open anything on their behalf.
    const { data: inbox } = await recipient.reportInbox.list(org, ws);
    const first = inbox?.items?.[0];
    if (first?.delivery_id === undefined) {
      out(NO_DELIVERY);
      return { schedule_id: scheduleId, opened: null };
    }
    const { data: opened } = await recipient.reportInbox.open(org, first.delivery_id);
    const run = opened?.run;
    out(`opened ${opened?.item?.delivery_id ?? ""}: run ${run?.run_id ?? ""} is ${run?.state ?? ""}`);
    out(`  coverage ${JSON.stringify(run?.coverage ?? [])}`);
    return { schedule_id: scheduleId, opened: opened ?? null };
  }
  ```
  </Fragment>
</CodeTabs>

Each script reads `schedule.json` (or the path you pass), `TOKENTRIAGE_BASE_URL`,
`TOKENTRIAGE_MANAGEMENT_KEY` for the owner, `TOKENTRIAGE_RECIPIENT_KEY` for the recipient,
`TOKENTRIAGE_ORG` and `TOKENTRIAGE_WORKSPACE`. `TOKENTRIAGE_IDEMPOTENCY_KEY` is optional and
defaults to `gateway-report-schedule-01`. The script prints one line per step:

```console
$ python schedule_gateway_report.py
schedule rsch_S11111111111 is armed
  next 2026-09-21T16:00:00Z = 2026-09-21T09:00:00-07:00
  next 2026-09-22T16:00:00Z = 2026-09-22T09:00:00-07:00
  next 2026-09-23T16:00:00Z = 2026-09-23T09:00:00-07:00
No delivered report yet; inspect occurrence history
```

Run with Python 3.14 against a loopback server answering the two requests recorded in
`sdk-python-schedule-gateway-report`, straight after the report was made: the create as
`it-lead` with the default key, then the inbox list as `it-analyst` with their own key.

```console
$ node schedule_gateway_report.mjs
schedule rsch_S11111111111 is armed
  next 2026-09-21T16:00:00Z = 2026-09-21T09:00:00-07:00
  next 2026-09-22T16:00:00Z = 2026-09-22T09:00:00-07:00
  next 2026-09-23T16:00:00Z = 2026-09-23T09:00:00-07:00
No delivered report yet; inspect occurrence history
```

Bundled with esbuild and run with Node 24 against the two requests recorded in
`sdk-ts-schedule-gateway-report`.

Run straight after creating a schedule, the inbox is normally empty, and the script prints
`No delivered report yet; inspect occurrence history` instead of opening anything. Nothing in it
waits for a delivery. Run it again after the first occurrence, or read the history as in step 4.

## What a schedule does not do

- It delivers to the in-app inbox and nowhere else: no email, no Slack message.
- It grants nothing. A recipient opens each run under their own current access, and a recipient
  who has lost access gets no item.
- It repeats a relative window only. A fixed-window view cannot be scheduled.
- It has no cron expressions and no calendar-day windows: "yesterday" is the 24 hours before the
  scheduled time, not midnight to midnight.
- It runs one scheduled report at a time per workspace.
- It names at most 20 recipients; a 21st is answered 422 `validation_failed`. A workspace is sized
  for 20 schedules, a documented capacity that is not enforced by a refusal in this release.

[Reporting and alerts](/docs/reference/reporting-and-alerts) lists every schedule field, state,
failure code, problem code and limit.

<NextSteps items={[
  { title: "Reporting and alerts", href: "/docs/reference/reporting-and-alerts", description: "Every schedule and inbox operation, occurrence state, failure code and limit in one table each." },
  { title: "What a report run keeps, and who can open it", href: "/docs/concepts/report-snapshots-and-access", description: "Why each delivered run keeps its own answer, and why a recipient may see fewer cells than you." },
  { title: "Keep a dated report of a gateway view", href: "/docs/build/create-gateway-report", description: "Make the report a schedule runs, and name its recipients." }
]} />
