---
title: get_refresh_status
description: Read a dashboard's refresh state and its last twenty runs, or hold the call open until a refresh lands. Parameters, return shape, the wait, errors and scope.
updated: 2026-09-22
tools: [get_refresh_status]
---

Reads a dashboard's refresh state: whether it is refreshable at all, its
schedule, its status, when it runs next, the last successful run and last
attempt, the consecutive-failure count, and the most recent 20 runs with their
row counts, durations and errors. A failed run also names its class and subcode,
and each run lists which of its datasets failed and on what. See
[How a failure is named](#how-a-failure-is-named).

This is the read side of scheduled refresh. It answers "is it refreshing on
schedule", "when does it run next", and "why did the last run fail". It does not
trigger a refresh and does not change the schedule.

Behaviour hints for clients: read-only, not open-world.

## Parameters

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `slug` | string | yes | Canonical slug of the dashboard to inspect. |
| `workspace` | string | no | Workspace slug. |
| `wait_seconds` | integer | no | Default 0. How many seconds the call may hold itself open while a refresh is in flight, 0 to 45. See [Waiting for a refresh to land](#waiting-for-a-refresh-to-land). |

## Returns

```text
Refresh status for "revenue-overview":
- Phase: up_to_date
- Waited: 41208 ms for the in-flight refresh to finish (wait_outcome: changed); the phase above is the answer after that wait
- Refreshable: yes
- Connection: Analytics Postgres [7a1e...]
- Schedule: daily at 07:00 America/Los_Angeles
- Status: active
- Next refresh: 2026-07-31T14:00:00Z
- Last successful run: 2026-07-30T14:00:03Z
- Last attempt: 2026-07-30T14:00:03Z
- Consecutive failures: 0

The numbers are live as of 2026-07-30T14:00:03Z. To confirm a refresh you just asked for has landed, poll until `last_successful_run` is later than the value you read before asking.

Recent runs (newest first, up to 20):
1. 2026-07-30T14:00:00Z -> 2026-07-30T14:00:03Z - success - 96 rows - 3120 ms

BEGIN_JSON
{"slug":"revenue-overview","is_refreshable":true,"phase":"up_to_date","waited_ms":41208,"wait_outcome":"changed","run_count":1,"runs":[]}
END_JSON
```

The `- Waited:` line appears only when `wait_seconds` was passed; the JSON block
always carries `waited_ms` and `wait_outcome` (`0` and `not_waiting` on a call
that did not wait), so a programmatic poller reads one shape either way.

## Waiting for a refresh to land

Pass `wait_seconds` and, when a refresh is in flight at the moment of the call
(`phase` is `updating`), the call holds itself open, re-reads the status every
3 seconds, and returns as soon as the phase leaves `updating` or the budget
elapses. The answer is the status AFTER the wait, plus two fields:

| Field | Meaning |
|---|---|
| `waited_ms` | How long the call held itself open beyond an ordinary read, measured on the server. `0` when it did not wait. |
| `wait_outcome` | `changed`: the refresh finished (read `phase` and the sentence under it). `budget_elapsed`: the budget ran out with the refresh still in flight; call again with `wait_seconds`. `not_waiting`: nothing was in flight at entry, or `wait_seconds` was 0. |

The wait holds no database transaction and burns no CPU: between reads it is a
timer.

:::note{title="Why the wait lives in the tool, and why the cap is 45"}
An authoring agent told to "wait, then poll" often cannot: the Claude Code CLI
blocks a foreground `sleep`, and a sleep sent to the background returns at once,
so the agent narrates a wait it never took and ends its turn with the refresh
still running. Putting the wait inside the tool call takes both the CLI hook and
the agent's judgment out of the loop.

The cap is the MCP client's own timeout on a single tool call. Measured
2026-09-03 with Claude Code 2.1.259 against a localhost MCP server whose one tool
sleeps N seconds and answers with a plain JSON body, the same wire shape as this
tool: a 55 s call returned, a 60 s call was reported to the model as
`The operation timed out.` even though the server had answered, and 65, 120, 180
and 330 s calls all lost the client 60.002 s after the request arrived. The
client's own code agrees: its timeout resolution reads a per-server setting, then
an environment variable, then a default of 60,000 ms, and branches only on
transport type, so a localhost server and a remote one take the same path.

So the whole call has to return inside 60 s, and 45 leaves 15 s for the reads
that surround the wait - which a production round trip of this tool uses about
3 s of. A value above 45 is refused rather than clamped, naming the cap. A
refresh that outlasts one budget is waited for by calling again; each call is a
fresh budget.

Two things follow. Streaming progress notifications does **not** raise the
ceiling: the client documents that they do not extend the limit. Raising the
per-server `timeout` in your own MCP client config does, which makes 45 exactly
right on a default configuration and conservative if you have raised yours.
:::

## `phase` is the field to poll on

`Phase` is a one-word verdict, first in the summary and carried in the JSON
block, so a caller looping on this tool does not have to re-derive it from the
run array every iteration.

| `phase` | Meaning |
|---|---|
| `not_refreshable` | No data source. The numbers never change on their own. |
| `waiting` | Refreshable, and no refresh has finished yet. |
| `updating` | A refresh is in flight right now, on any surface. |
| `suspended` | Dashies has stopped scheduling this dashboard after repeated failures on a cause only its owner can fix. A refresh that succeeds turns the schedule back on. |
| `failed` | The most recent finished refresh failed. |
| `up_to_date` | The most recent finished refresh succeeded. It may have succeeded for only some datasets: read `partial_failure` and the sentence under the phase. |

:::note{title="Poll on last_successful_run to confirm a refresh LANDED"}
Every refresh is now observable while it runs, on-demand ones included, so
`updating` is reachable for a refresh you triggered yourself and the run appears
in `runs` immediately with the surface that started it. `queued` is deliberately
absent: a run that has been accepted and one whose extract has begun are not
distinguishable, so both read as `updating`.

`updating` tells you a refresh is HAPPENING, which is not the same as its
numbers having ARRIVED. To confirm one landed, hold the `last_successful_run`
you read BEFORE asking and poll until it moves. That is the only signal correct
on a first publish (null becomes a timestamp) and on a republish that starts a
refresh (one timestamp becomes a later one) alike.

A republish whose receipt says `First data: unchanged` started no refresh,
because the numbers already served were built from exactly the data definition
it published, by a newest refresh that succeeded. That run already reads
`current: true`, the receipt prints the dashboard's `last_successful_run`, and
this publish moves that field no further, so there is nothing to poll for, and
its report prints no note about waiting for data. Call `trigger_refresh` only if
you want fresher rows. Any other receipt is polled as above.
:::

:::note{title="A success can belong to an older version of the dashboard"}
Each run carries `trigger_source`, the `spec_hash` and `manifest_hash` it RAN,
and `current`. `current` is three-valued: `true` means the run executed the
version live now, `false` means it ran an older one, and `null` means the run
was recorded before runs carried a version and cannot say. A run that finished
after you republished may well report `false`, because a run is attributed to
the version it started with rather than the one live when it ended.
:::

## How a failure is named

A dashboard with several datasets refreshes all of them in one run, so a run can
fail for one reason, fail for several, or succeed for some datasets and not
others. Each run line names what it can, and the lines under it name the rest:

```text
1. 2026-09-22T10:00:00Z - failed [cron] (120000 ms) - class: connection_timeout - subcode: statement_timeout - error: all datasets failed
   - orders, returns failed: connection_timeout / statement_timeout
2. 2026-09-21T10:05:00Z - failed [cron] (90000 ms) - error: all datasets failed
   - orders failed: connection_timeout / statement_timeout
   - returns failed: cube_error / object_not_found
3. 2026-09-20T10:10:00Z - success [cron] - 5569 rows (70000 ms) - PARTIAL: 1 of 2 datasets failed
   - returns failed: cube_error / object_not_found
```

- **`class`** is the run's own failure class. When every dataset failed for one
  reason it is that reason's class. When they failed for different reasons the
  run has no single class, and the per-dataset lines are the answer.
- **`subcode`** is the second level under the class, when there is one.
- **The per-dataset lines** group the failed datasets by cause.
- **`PARTIAL`** marks a run that succeeded for some datasets and failed for
  others. The datasets it names were not updated by that run, so their numbers
  are not live. When that run is the newest finished one, the JSON block's
  `partial_failure` names them in every phase, and under `up_to_date` the
  sentence says so too, unless it reports that the run used an older version of
  the dashboard, which is the stronger warning:

```json
"partial_failure": {"run_started_at": "2026-09-20T10:10:00+00:00", "failed_datasets": ["returns"], "dataset_count": 2}
```

`partial_failure` is `null` whenever the newest finished run is not a partial
success. In the JSON block every run also carries `error_class` and `datasets`,
one `{name, status, error_class, subcode}` per dataset, or `null` for a run that
recorded no datasets.

The `Connection:` line takes one of four forms:
`self (Dashies' own no-PII metrics)`, `<label> [<id>]`, `[<id>]`, or `unknown`.

A dashboard with no runs yet reports `No refresh runs recorded yet.` The JSON
block always carries `run_count`, so an empty history is distinguishable from a
failed read.

A dashboard that is not set up to refresh reports:

```text
- Phase: not_refreshable
- Refreshable: no (no connected data source)
This dashboard has no data source, so its numbers never change on their own. Republish it with a source to put it on a schedule.
```

:::note{title="Last attempt and last successful run are different fields"}
When they diverge, the dashboard is failing: the cron is still running on
schedule and the runs are not succeeding. Read `Consecutive failures` next, since
the retry backoff widens with it.
:::

## Errors

The shared [slug rules](/reference/mcp-tools/conventions#paths-and-slugs), plus:

| Condition | Text |
|---|---|
| The read failed | `refresh status lookup failed: <err>` |
| No such dashboard | `No dashboard named "<slug>" in that workspace.` |
| `wait_seconds` is not an integer | `wait_seconds must be an integer` |
| `wait_seconds` is negative or above the cap | `wait_seconds must be between 0 and 45` |

A re-read that fails mid-wait is reported exactly as a failed first read, and a
dashboard that stops existing mid-wait exactly as one that never did: the wait
changes when the read happens, never what a failure means.

:::note{title="A missing dashboard and one you cannot see read the same"}
The lookup is membership-gated, and a workspace dashboard that genuinely does not
exist produces the same sentence as one you cannot see. That is deliberate: a
distinguishable refusal would be an existence oracle keyed by slug.

This callout used to warn that the sentence said `in your account` even for a
workspace target. It no longer does: this tool is one of six that share a single
not-found sentence naming the workspace, enumerated in
[Conventions](/reference/mcp-tools/conventions).
:::

## Scope

Any member of the workspace may read the status of its
dashboards.

## Check it worked

After arming a schedule, confirm `Next refresh` is populated and in the future.
After the first run lands, confirm `Last successful run` matches it and
`Consecutive failures` is 0. A `Next refresh` that never moves is the signal that
the cadence never re-anchored.
