# Avvisi che un agente può scrivere

Una dashboard ti dice qualcosa quando la apri. **Un avviso te lo dice quando non stavi guardando** — ed è proprio la parte che un harness non può produrre da solo: gli servono stato ospitato, dati freschi, un orologio a muro e un posto dove consegnare.

Un avviso sono cinque decisioni, quindi l'API ha cinque campi:

| Campo     | Cosa decide                                                   |
| --------- | ------------------------------------------------------------- |
| `when`    | Il SQL le cui **righe sono la violazione**                    |
| `say`     | La frase che legge il destinatario, riempita dalla prima riga |
| `every`   | Ogni quanto viene guardata la condizione                      |
| `capture` | Quale immagine viene scattata, in Playwright                  |
| `to`      | Chi lo riceve                                                 |

## L'esempio svolto[​](#lesempio-svolto "Link diretto a L'esempio svolto")

```
from vibedasher import Vibedasher



client = Vibedasher()



alert = client.alerts.create(

    viz_id=1430,

    name="Margin watch",

    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 * * *",

    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"],

)



client.alerts.verify(alert.id)   # ogni selettore si risolve — non cattura nulla, non invia nulla

client.alerts.test(alert.id)     # cattura davvero E INVIA UNA MAIL a finance@acme.com
```

La stessa chiamata in TypeScript, dove l'SDK è in camelCase:

```
const alert = await client.alerts.create({

  vizId: 1430,

  name: 'Margin watch',

  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 * * *',

  capture: "await page.goto('?f.region={region}');\nawait expect(page.getByTestId('chart-rendered')).toBeVisible();\nawait page.locator('[data-capture-root]').screenshot();",

  to: ['finance@acme.com'],

});



await client.alerts.verify(alert.id); // ogni selettore si risolve — non cattura nulla, non invia nulla

await client.alerts.test(alert.id);   // cattura davvero E INVIA UNA MAIL a finance@acme.com
```

Via MCP è `create_alert` con gli stessi cinque nomi, poi `verify_alert` e `test_alert`.

## Il contratto «truthy»[​](#il-contratto-truthy "Link diretto a Il contratto «truthy»")

**Zero righe significa non scattato. Una riga o più significa scattato.** Non c'è nessun comparatore da invertire per sbaglio né alcuna soglia da tenere sincronizzata con la query: la clausola `WHERE` *è* la condizione.

La prima riga restituita sono i metadati del messaggio, e `say` li trasforma in parole. Scrivi la violazione e la sua spiegazione come un'unica query, così che la frase non possa affermare nulla che i dati non dicano:

```
SELECT region,

       ROUND(margin*100,1) AS margin_pct,

       10                  AS threshold_pct

FROM margins

WHERE margin < 0.10

ORDER BY margin
```

Ciò che legge il destinatario (la frase la produce il programma, quindi resta in inglese):

```
Profit margin 8.2% is below 10% in West, and 3 more
```

Le righe in più sono contesto, con un tetto di cinque. Un segnaposto di `say` che nomina una colonna che la query non fa `SELECT` viene **rifiutato quando crei l'avviso**, con la colonna nominata e l'elenco di quelle disponibili — mai uno spazio vuoto, e mai il letterale `{region}` spedito per email a qualcuno come se fosse un testo scritto da te. Gli alias contano: è `ROUND(margin*100,1) AS margin_pct` a far risolvere `{margin_pct}`.

La forma più vecchia — `comparator` più `threshold` su una singola cella numerica — è ancora accettata come zucchero sintattico. Preferisci la forma truthy: «zero righe» è inequivocabile in un modo in cui «il numero non è sotto la soglia» non lo è.

## L'immagine mostra la violazione, non la vista predefinita[​](#limmagine-mostra-la-violazione-non-la-vista-predefinita "Link diretto a L'immagine mostra la violazione, non la vista predefinita")

`{column}` funziona anche in `capture`, ed è tutto il punto. Lì un segnaposto è un **valore**: `?f.region={region}` fissa il filtro sulla regione che è davvero scattata, quindi l'avviso su West è un'immagine di West. Il link nel messaggio è costruito con la stessa ricetta, così immagine e link concordano.

```
capture: await page.goto('?page=overview&f.region={region}');

         await expect(page.getByTestId('chart-rendered')).toBeVisible();

         await page.locator('[data-capture-root]').screenshot();
```

`getByTestId('<name>')` indirizza un controllo tramite **il nome di configurazione che gli hai dato** — `describe_dashboard_controls(viz_id)` via MCP, oppure `GET /v1/viz/{id}/controls`, elenca i nomi e i valori che una dashboard accetta davvero. Mai un percorso CSS grezzo: quelli vengono rifiutati, perché si rompono in silenzio la prossima volta che il markup si sposta.

La ricetta viene **analizzata, mai eseguita**. `page.evaluate`, `require`/`import`, i template literal, la concatenazione di stringhe, i commenti e i percorsi CSS tornano ciascuno come un rifiuto che nomina la riga. Ciò che viene eseguito è una piccola ricetta dichiarativa che la corsia di cattura applica costruendo l'URL, deterministica in un modo in cui un clic non lo è.

## `every`: una stringa cron o una durata[​](#every-una-stringa-cron-o-una-durata "Link diretto a every-una-stringa-cron-o-una-durata")

```
every="0 7 * * *"   # le 07:00 di ogni giorno

every="15m"         # ogni quindici minuti

every="2h"          # ogni due ore

every="1d"          # una volta al giorno a mezzanotte
```

Sono accettati solo i passi che riempiono la loro unità in modo esatto. `7m` viene rifiutato: cron fa ripartire il passo allo scoccare dell'ora, quindi `*/7` scatterebbe a :00, :07 … :56 e poi di nuovo quattro minuti dopo — non «ogni sette minuti». Il rifiuto elenca i passi che funzionano.

Anche un'espressione cron valida che non può **mai verificarsi** — `0 0 30 2 *`, che chiede il 30 febbraio — viene rifiutata. Nulla a valle se ne lamenterebbe mai: l'avviso resterebbe `active`, mai dovuto, mai valutato, e ogni superficie lo segnalerebbe come sano per sempre.

Ometti `every` con `when` impostato e l'avviso viene valutato alla cadenza propria dello scheduler.

## `to`: il canale si legge dall'indirizzo[​](#to-il-canale-si-legge-dallindirizzo "Link diretto a to-il-canale-si-legge-dallindirizzo")

```
to=[

    "finance@acme.com",                       # email

    "https://hooks.slack.com/services/T/B/X", # webhook in ingresso di Slack

    "usr_1a2b3c",                             # un id utente — notifica in-app

]
```

Le tre forme sono disgiunte, quindi non c'è nessun canale da dichiarare accanto all'indirizzo. Qualcosa che sembra un indirizzo e non è né l'uno né l'altro viene rifiutato invece di essere archiviato come un id utente che non esiste.

## La garanzia cache-zero[​](#la-garanzia-cache-zero "Link diretto a La garanzia cache-zero")

**Una condizione viene sempre eseguita bypassando la cache SQL.** Non lo chiedi tu e non puoi disattivarlo. Un avviso che scattasse su un risultato in cache non sarebbe un avviso — sarebbe il resoconto di qualcosa che è stato vero a un certo punto. Nessuna lettura di cache, nessun incremento del contatore di hit, nessuna scrittura di cache: la query va al motore a ogni valutazione.

## Scattare sulla transizione, non a ogni controllo[​](#scattare-sulla-transizione-non-a-ogni-controllo "Link diretto a Scattare sulla transizione, non a ogni controllo")

Entrare nella violazione fa scattare una volta. Restarci è deliberatamente silenzioso. L'avviso si riarma quando la condizione rientra. Una metrica che oscilla attorno al suo limite invia un messaggio, non uno per tick.

## Il canarino: esegui `verify` dopo aver modificato una dashboard[​](#il-canarino-esegui-verify-dopo-aver-modificato-una-dashboard "Link diretto a il-canarino-esegui-verify-dopo-aver-modificato-una-dashboard")

`verify` è l'unica chiamata che ti dice che la tua ricetta si risolve ancora.

```
result = client.alerts.verify(alert.id)

if not result.verification.resolved:

    for step in result.verification.steps:

        print(step.index, step.status, step.detail)
```

**Non cattura nulla, non invia nulla, non scrive alcuna riga e non muove alcuna colonna.** Esegue lo stesso executor che esegue una cattura reale, in modalità solo-risoluzione, contro il codice generato della dashboard stessa — quindi non può divergere da ciò che farà la cattura. Ogni passo viene riportato, non solo il primo: rinomina tre selettori e te ne verranno segnalati tre.

Fanne un'abitudine, perché il guasto che intercetta è altrimenti invisibile:

> **Modifichi una dashboard. Rinomini un filtro. La ricetta dell'avviso punta ancora al vecchio nome. Nulla fallisce — finché un destinatario non apre un'email con dentro l'immagine della cosa sbagliata.**

Un passo rosso **è** la modifica incompatibile che hai appena fatto. Esegui `verify` dopo ogni modifica alla dashboard che tocca un id di pagina, un nome di selettore o un valore di filtro, e lo trovi finché puoi ancora ripararlo.

## Ogni rifiuto nomina la correzione[​](#ogni-rifiuto-nomina-la-correzione "Link diretto a Ogni rifiuto nomina la correzione")

Un `400` da questi endpoint è pensato per essere letto e agito, non ritentato (i messaggi qui sotto sono quelli che restituisce l'API, in inglese):

```
say= references {margin_pct}, but the when= query does not return that column.

It returns: region. Fix: SELECT it (an alias counts, e.g.

'ROUND(margin*100,1) AS margin_pct'), or reference one of the columns above.
```

```
every='7m' does not divide the hour evenly, so it would not actually run every

7 minutes — cron restarts the step at :00, making the last gap of each hour

shorter. Fix: use one of 5m, 6m, 10m, 12m, 15m, 20m, 30m, or pass the cron

expression you mean.
```

```
Cron '0 0 30 2 *' is a valid expression that never occurs — no date matches its

day-of-month, month and day-of-week together (e.g. '30 2' asks for February

30th). An alert on it would stay 'active' and silent forever. Fix: name a date

that exists, or use a duration like every='1d'.
```

## L'ordine in cui lavorare[​](#lordine-in-cui-lavorare "Link diretto a L'ordine in cui lavorare")

1. **`describe_dashboard_controls(viz_id)`** — i nomi e i valori che questa dashboard accetta davvero. Saltarlo significa indovinare i nomi dei selettori.
2. **Scrivi il SQL.** Le sue righe sono la violazione; le sue colonne sono la frase.
3. **Scrivi la cattura.** Usa `{column}` così che l'immagine sia la violazione.
4. **`verify`** — prima di fidarti, e di nuovo dopo ogni modifica alla dashboard.
5. **`test`** — cattura davvero e restituisce la riga di consegna da tenere d'occhio. **Invia un messaggio ai destinatari dell'avviso**: un'esecuzione di test è un invio reale, e solo la contabilità differisce da uno scatto vero (`lastFiredAt` non si muove, perché non è scattato nulla). Eseguilo quando un messaggio reale a quegli indirizzi è ciò che vuoi; `verify` è il passaggio che non raggiunge nessuno.

## Permessi[​](#permessi "Link diretto a Permessi")

Creare, modificare, eliminare e testare un avviso richiede il ruolo **creator** o **admin** *e* l'accesso in modifica alla dashboard. Leggerne uno — e `verify`, che non deriva nulla che tu non potessi già leggere — richiede l'accesso in visualizzazione. Una dashboard che non puoi modificare risponde `404`, non `403`: il rifiuto non conferma che l'id esista.
