Skip to content

Reference / Refresh

get_refresh_status

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.

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.

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

ParameterTypeRequiredNotes
slugstringyesCanonical slug of the dashboard to inspect.
workspacestringnoWorkspace slug.
wait_secondsintegernoDefault 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.

Returns

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:

FieldMeaning
waited_msHow long the call held itself open beyond an ordinary read, measured on the server. 0 when it did not wait.
wait_outcomechanged: 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.

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.

phaseMeaning
not_refreshableNo data source. The numbers never change on their own.
waitingRefreshable, and no refresh has finished yet.
updatingA refresh is in flight right now, on any surface.
suspendedDashies 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.
failedThe most recent finished refresh failed.
up_to_dateThe most recent finished refresh succeeded. It may have succeeded for only some datasets: read partial_failure and the sentence under the phase.

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.

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:

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:
"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:

- 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.

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, plus:

ConditionText
The read failedrefresh status lookup failed: <err>
No such dashboardNo dashboard named "<slug>" in that workspace.
wait_seconds is not an integerwait_seconds must be an integer
wait_seconds is negative or above the capwait_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.

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.

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.