Saltar al contenido principal

Create Alert

POST 

/v1/alerts

Create an alert on a dashboard you can edit — from five fields, or in full.

The five fields. when / say / every / capture / to are the five things a caller actually decides, and one call is enough::

when: SELECT region, ROUND(margin*100,1) AS margin_pct FROM margins WHERE margin < 0.10 ORDER BY margin LIMIT 5 say: Profit margin {margin_pct}% is below 10% in {region} every: 0 7 * * * (or "15m", "2h", "1d") capture: await page.goto('?f.region={region}'); await expect(page.getByTestId('chart-rendered')).toBeVisible(); await page.locator('[data-capture-root]').screenshot(); to: ["finance@acme.com"]

They are NAMING, DEFAULTS and one round trip over the structured fields — not a second model. Each is rewritten onto condition / schedule / captureRecipe / recipients before the row is written, so the alert you read back is the same shape as one created the long way, and capturePlaywright is the normalised capture you wrote.

{column} binds the picture to the breach. A placeholder in capture is a VALUE: ?f.region={region} captures the region that actually fired, not the default view, and the link in the message carries the same filters. A placeholder naming a column when does not SELECT is refused HERE, with the column named and the available ones listed — an agent can still fix it; a recipient cannot.

Every refusal names the fix. An unknown column, an address that is neither an email nor a Slack webhook, a duration that does not tile its unit (7m does not mean "every 7 minutes" in cron), a payload with neither a clock nor a condition, and a cron expression that is VALID and can never occur (0 0 30 2 *) each come back as a 400 saying what to do instead.

The long form is unchanged. condition, schedule, captureRecipe / capturePlaywright, recipients and viewState are still accepted exactly as before — viewState is the only way to pin a page, a date window or a measure without writing a recipe. Passing both spellings of the same thing is a 400 naming the pair, never a precedence rule.

The response carries a backtest: how often this condition would have fired over history. A threshold that can never fire used to look exactly like a healthy service — status: "active", not_fired, rowCount: 0, byte-for-byte what a working alert returns. backtest.crossings is the number that separates them and backtest.scope says over what history it was counted; backtest.note says it in one sentence. crossings: 0 is never a refusal — watching for a line nobody has crossed yet is often the point, so the alert is created either way, and a backtest that could not run (a cold SQL worker) reports warming_up rather than failing the creation. Ask again with POST /v1/alerts/{alert_id}/backtest.

An inline condition may only read the DASHBOARD'S OWN datasets. There is nowhere else for it to name data: the alert hangs off a viz, and that viz's dataset_ids are the scope the SQL worker registers. A FROM naming a table bound to some other dashboard resolves to nothing, and the engine says so — Catalog Error: Table with name X does not exist! Did you mean "Y"?.

status is honest from the moment the row exists. That refusal used to arrive days later, on the alert's first scheduled run, while the create response said status: "active" — an alarm reported as armed that was not. The backtest above runs the real query through the real executor, so the refusal is already in hand here: when it comes back, the alert is created with status: "error" and statusReason carrying the engine's own sentence, exactly what the first evaluation would have written. It is still a creation and never a 400 — a refusal decided by how warm the SQL worker happened to be would not be a gate — and it clears itself on the next evaluation that succeeds. warming_up leaves status alone: nothing was measured, so nothing is known.

Evaluation, capture AND DELIVERY are all live. POST /v1/alerts/\{id\}/evaluate runs the condition, the tick runs it on the schedule, and a fire is delivered — email, Slack and in-app all send, and the outcome is written back onto the row (lastFiredAt moves only once a recipient was reached). This paragraph denied that for weeks while the product was sending their mail, and because these docstrings are codegen input the denial travelled from here into the OpenAPI spec, both SDKs and the docs site; do not reintroduce the denial — a gate asserts its absence (packages/mcp/tests/test_alert_delivery_claims_agree.py).

Request​

Responses​

Successful Response