Alertas que un agente puede escribir
Un panel te dice algo cuando lo abres. Una alerta te avisa cuando no estabas mirando — y esa es justo la parte que un agente no puede producir por sí solo: necesita estado alojado, datos frescos, un reloj de pared y algún sitio donde entregar.
Una alerta son cinco decisiones, así que la API tiene cinco campos:
| Campo | Qué decide |
|---|---|
when | El SQL cuyas filas son la infracción |
say | La frase que lee quien la recibe, rellenada con la primera fila |
every | Cada cuánto se mira la condición |
capture | Qué imagen se toma, en Playwright |
to | Quién la recibe |
El ejemplo completo
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) # cada selector se resuelve — no captura nada, no envía nada
client.alerts.test(alert.id) # captura de verdad Y ENVÍA UN CORREO a finance@acme.com
La misma llamada en TypeScript, donde el SDK es 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); // cada selector se resuelve — no captura nada, no envía nada
await client.alerts.test(alert.id); // captura de verdad Y ENVÍA UN CORREO a finance@acme.com
Por MCP es create_alert con los mismos cinco nombres, y luego verify_alert y
test_alert.
El contrato «truthy»
Cero filas significa no disparada. Una o más filas significa disparada. No hay ningún
comparador que puedas poner del revés ni ningún umbral que mantener sincronizado con la
consulta: la cláusula WHERE es la condición.
La primera fila devuelta son los metadatos del mensaje, y say los convierte en palabras.
Escribe la infracción y su explicación como una única consulta, para que la frase no pueda
afirmar nada que los datos no digan:
SELECT region,
ROUND(margin*100,1) AS margin_pct,
10 AS threshold_pct
FROM margins
WHERE margin < 0.10
ORDER BY margin
Lo que lee quien la recibe (la frase la genera el programa, así que queda en inglés):
Profit margin 8.2% is below 10% in West, and 3 more
Las filas extra son contexto, con un tope de cinco. Un marcador de say que nombra una
columna que la consulta no hace SELECT se rechaza al crear la alerta, nombrando la
columna y listando las disponibles — nunca un hueco en blanco, y nunca el literal {region}
enviado por correo a alguien como si fuera un texto escrito por ti. Los alias cuentan, así
que es ROUND(margin*100,1) AS margin_pct lo que hace que {margin_pct} se resuelva.
La forma antigua — comparator más threshold sobre una única celda numérica — se sigue
aceptando como azúcar sintáctico. Prefiere la forma truthy: «cero filas» es inequívoco de una
manera en que «el número no está por debajo del umbral» no lo es.
La imagen muestra la infracción, no la vista por defecto
{column} también funciona en capture, y ese es todo su sentido. Allí un marcador es un
valor: ?f.region={region} fija el filtro a la región que realmente se disparó, así que
la alerta sobre West es una imagen de West. El enlace del mensaje se construye con la misma
receta, de modo que la imagen y el enlace coinciden.
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>') se dirige a un control por el nombre de configuración que tú le
diste — describe_dashboard_controls(viz_id) por MCP, o GET /v1/viz/{id}/controls, lista
los nombres y los valores que un panel acepta de verdad. Nunca una ruta CSS en crudo: esas se
rechazan, porque se rompen en silencio la próxima vez que el marcado se mueve.
La receta se analiza, nunca se ejecuta. page.evaluate, require/import, las
plantillas literales, la concatenación de cadenas, los comentarios y las rutas CSS vuelven
cada uno como un rechazo que nombra la línea. Lo que se ejecuta es una pequeña receta
declarativa que el carril de captura aplica construyendo la URL, algo determinista de una
manera en que un clic no lo es.
every: una cadena cron o una duración
every="0 7 * * *" # las 07:00 de cada día
every="15m" # cada quince minutos
every="2h" # cada dos horas
every="1d" # una vez al día, a medianoche
Solo se aceptan los pasos que cubren su unidad de forma exacta. 7m se rechaza: cron
reinicia el paso al comienzo de la hora, así que */7 se dispararía a los :00, :07 … :56 y
otra vez cuatro minutos después — no «cada siete minutos». El rechazo lista los pasos que sí
funcionan.
Una expresión cron válida que nunca puede ocurrir — 0 0 30 2 *, que pide un 30 de
febrero — también se rechaza. Nada aguas abajo se quejaría jamás de ella: la alerta se
quedaría active, nunca vencida, nunca evaluada, y todas las superficies la darían por sana
para siempre.
Omite every con when definido y la alerta se evalúa a la cadencia propia del planificador.
to: el canal se lee de la dirección
to=[
"finance@acme.com", # correo electrónico
"https://hooks.slack.com/services/T/B/X", # webhook entrante de Slack
"usr_1a2b3c", # un id de usuario — notificación in-app
]
Las tres formas son disjuntas, así que no hay ningún canal que declarar junto a la dirección. Algo que parece una dirección y no es ninguna de las tres se rechaza en lugar de archivarse como un id de usuario que no existe.
La garantía de caché cero
Una condición siempre se ejecuta saltándose la caché de SQL. Ni lo pides ni puedes desactivarlo. Una alerta que se disparara sobre un resultado cacheado no sería una alerta — sería el informe de algo que fue cierto en algún momento. Sin lectura de caché, sin incremento del contador de aciertos, sin escritura de caché: la consulta va al motor cada vez que se evalúa.
Dispara en la transición, no en cada comprobación
Entrar en la infracción dispara una vez. Permanecer en ella es deliberadamente silencioso. La alerta se rearma cuando la condición se despeja. Una métrica que oscila alrededor de su límite envía un mensaje, no uno por tick.
El canario: ejecuta verify después de cambiar un panel
verify es la única llamada que te dice si tu receta se sigue resolviendo.
result = client.alerts.verify(alert.id)
if not result.verification.resolved:
for step in result.verification.steps:
print(step.index, step.status, step.detail)
No captura nada, no envía nada, no escribe ninguna fila y no mueve ninguna columna. Ejecuta el mismo ejecutor que ejecuta una captura real, en modo solo-resolución, contra el propio código generado del panel — de modo que no puede desviarse de lo que hará la captura. Se informa de cada paso, no solo del primero: renombra tres selectores y te lo dirá de los tres.
Conviértelo en costumbre, porque el fallo que atrapa es invisible de otro modo:
Editas un panel. Renombras un filtro. La receta de la alerta sigue apuntando al nombre antiguo. Nada falla — hasta que alguien abre un correo con la imagen de lo que no era.
Un paso en rojo es el cambio incompatible que acabas de hacer. Ejecuta verify tras cada
edición de un panel que toque un id de página, un nombre de selector o un valor de filtro, y
lo encontrarás cuando todavía puedes arreglarlo.
Cada rechazo nombra el arreglo
Un 400 de estos endpoints está pensado para leerse y actuar en consecuencia, no para
reintentarse (los mensajes de abajo son los que devuelve la API, en inglés):
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'.
El orden en el que trabajar
describe_dashboard_controls(viz_id)— los nombres y los valores que este panel acepta de verdad. Saltárselo es adivinar los nombres de los selectores.- Escribe el SQL. Sus filas son la infracción; sus columnas son la frase.
- Escribe la captura. Usa
{column}para que la imagen sea la infracción. verify— antes de confiar en ella, y otra vez tras cada edición del panel.test— captura de verdad y devuelve la fila de entrega que hay que vigilar. Envía un mensaje a los destinatarios de la alerta: una ejecución de prueba es un envío real, y solo la contabilidad difiere de un disparo real (lastFiredAtno se mueve, porque no se disparó nada). Ejecútalo cuando lo que quieras sea un mensaje real a esas direcciones;verifyes el paso que no llega a nadie.
Permisos
Crear, editar, borrar y probar una alerta requiere el rol creator o admin y acceso
de edición al panel. Leer una — y verify, que no deriva nada que no pudieras leer ya —
requiere acceso de lectura. Un panel que no puedes editar responde 404, no 403: el rechazo
no confirma que el id exista.