Best practices
Which endpoint should I call?
The four ways to get numbers out of boards are not interchangeable. Picking the wrong one is the most common cause of avoidable 429s.
Prefer a tab run over N widget runs
Rendering six widgets one at a time costs six requests against a 20/min budget. Run tab costs one against a 50/min budget, and runs the widgets concurrently, so it is faster as well as cheaper.
A tab run also degrades gracefully. Each result is tagged ok or error, and a widget that fails does not fail the request — you still get every other widget.
Always branch on status before reading rows. Treating the array as uniformly successful will break the first time one query times out.
filter_values_used echoes back what was actually applied after merging your filter_values over each filter’s default — useful for labelling a rendered page, and for confirming a default was what you assumed.
Caching
Widget runs and tab runs are cached for 60 seconds, keyed by widget, filter values, and row limit. Editing a widget invalidates its cache immediately.
- Polling the same widget with the same filters more often than once a minute returns identical rows and still consumes request budget. Match your poll interval to the cache, not to your refresh animation.
- Changing any filter value is a different cache key, so a dashboard where users move filters around will miss the cache often. Budget for that.
- Preview is not cached — every call executes, so it is the wrong endpoint to poll on a timer.
Staying inside the query budget
Every run gets 15 seconds and a row cap. Exceeding the time budget returns 408 with code TIMEOUT; exceeding the row cap returns 200 with truncated: true.
The dialect-level advice for keeping a query fast — filter on effective_datetime early, aggregate in the database rather than returning raw rows, name your columns instead of SELECT * — is the same whether you write the query here or in the dashboard editor, and lives on SQL best practices.
Two things are specific to calling this over the API:
There is no cursor for stepping through a large result — row_limit is a cap, not a page size.
truncated: true means your answer is wrong, not just short. The cap cut the result off, so any total, average, or max you compute from those rows is derived from a partial set. Raising row_limit is rarely the right fix — it just moves the cliff. Aggregate further or narrow the range so the full answer fits.
row_limit and a LIMIT in your SQL are not the same control. row_limit is the ceiling; a LIMIT inside the query can narrow the result below it but never widen it past it, and on preview and widget runs an in-SQL LIMIT is itself clamped to 1000. If you want the top 20, write ORDER BY ... LIMIT 20 — don’t set row_limit: 20 and hope the ordering holds.
Handling rate limits
Both limits return 429 with Retry-After in seconds; sleep for that long and back off exponentially on repeated 429s, with jitter if several workers share a key.
The per-organization limits are shared with people using the dashboard, so an integration that consumes the entire preview budget will make boards feel broken for your colleagues.
Notification rules
For anything on a fixed schedule, a notification rule beats polling: it costs no request budget and survives your service being down.
- The cron is a five-field expression evaluated in Asia/Kolkata; the minimum interval is one hour.
next_run_atin the response is UTC. - At least one of
email_recipientsorslack_webhook_urlis required. Both may be set. slack_webhook_urlis write-only — no endpoint ever returns it. Useslack_configuredto tell whether one is set, andslack_channel_nameto label it.- Pause a rule with
{"enabled": false}rather than recreating it later; disabling clearsnext_run_at.
End-to-end
Discover the schema, develop a query, save it, and run it.