Alertas que um agente consegue escrever
Um painel te conta algo quando você o abre. Um alerta te conta quando você não estava olhando — e essa é justamente a parte que um agente não consegue produzir sozinho: ele precisa de estado hospedado, dados frescos, um relógio de parede e algum lugar para entregar.
Um alerta são cinco decisões, então a API tem cinco campos:
| Campo | O que ele decide |
|---|---|
when | O SQL cujas linhas são a violação |
say | A frase que o destinatário lê, preenchida a partir da primeira linha |
every | De quanto em quanto tempo a condição é observada |
capture | Que imagem é tirada, em Playwright |
to | Quem o recebe |
O exemplo 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 seletor resolve — não captura nada, não envia nada
client.alerts.test(alert.id) # captura de verdade E ENVIA UM E-MAIL para finance@acme.com
A mesma chamada em TypeScript, onde o SDK é 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 seletor resolve — não captura nada, não envia nada
await client.alerts.test(alert.id); // captura de verdade E ENVIA UM E-MAIL para finance@acme.com
Via MCP, é create_alert com os mesmos cinco nomes, depois verify_alert e test_alert.
O contrato "truthy"
Zero linhas significa não disparado. Uma linha ou mais significa disparado. Não há
comparador para inverter por engano nem limiar para manter em sincronia com a consulta: a
cláusula WHERE é a condição.
A primeira linha retornada são os metadados da mensagem, e say os transforma em palavras.
Escreva a violação e sua explicação como uma única consulta, para que a frase não possa afirmar
nada que os dados não digam:
SELECT region,
ROUND(margin*100,1) AS margin_pct,
10 AS threshold_pct
FROM margins
WHERE margin < 0.10
ORDER BY margin
O que o destinatário lê (a frase é produzida pelo programa, então permanece em inglês):
Profit margin 8.2% is below 10% in West, and 3 more
Linhas extras são contexto, com teto de cinco. Um marcador de say que nomeia uma coluna que a
consulta não faz SELECT é recusado no momento em que você cria o alerta, com a coluna
nomeada e as disponíveis listadas — nunca um espaço em branco, e nunca o literal {region}
enviado por e-mail a alguém como se fosse um texto escrito por você. Os aliases contam, então é
ROUND(margin*100,1) AS margin_pct que faz {margin_pct} resolver.
A forma mais antiga — comparator mais threshold sobre uma única célula numérica — ainda é
aceita como açúcar sintático. Prefira a forma truthy: "zero linhas" é inequívoco de um jeito que
"o número não está abaixo do limiar" não é.
A imagem mostra a violação, não a visão padrão
{column} também funciona em capture, e é esse o ponto. Um marcador ali é um valor:
?f.region={region} fixa o filtro na região que de fato disparou, então o alerta sobre West é
uma imagem de West. O link da mensagem é construído a partir da mesma receita, então a imagem e
o link concordam.
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>') endereça um controle pelo nome de configuração que você deu a ele —
describe_dashboard_controls(viz_id) via MCP, ou GET /v1/viz/{id}/controls, lista os nomes e
os valores que um painel realmente aceita. Nunca um caminho CSS cru: esses são recusados, porque
quebram em silêncio na próxima vez que a marcação se mexer.
A receita é analisada, nunca executada. page.evaluate, require/import, template
literals, concatenação de strings, comentários e caminhos CSS voltam cada um como uma recusa que
nomeia a linha. O que roda é uma pequena receita declarativa que a trilha de captura aplica
montando a URL, o que é determinístico de um jeito que um clique não é.
every: uma string cron ou uma duração
every="0 7 * * *" # 07:00 todos os dias
every="15m" # a cada quinze minutos
every="2h" # a cada duas horas
every="1d" # uma vez por dia à meia-noite
Só são aceitos passos que preenchem sua unidade de forma exata. 7m é recusado: o cron
reinicia o passo na virada da hora, então */7 dispararia em :00, :07 … :56 e de novo quatro
minutos depois — não "a cada sete minutos". A recusa lista os passos que funcionam.
Uma expressão cron válida que nunca pode ocorrer — 0 0 30 2 *, pedindo 30 de fevereiro —
também é recusada. Nada rio abaixo jamais reclamaria dela: o alerta ficaria active, nunca
vencido, nunca avaliado, e toda superfície o reportaria como saudável para sempre.
Omita every com when definido e o alerta é avaliado na cadência do próprio agendador.
to: o canal é lido do endereço
to=[
"finance@acme.com", # e-mail
"https://hooks.slack.com/services/T/B/X", # webhook de entrada do Slack
"usr_1a2b3c", # um id de usuário — notificação in-app
]
As três formas são disjuntas, então não há canal a declarar ao lado do endereço. Algo que parece um endereço e não é nenhum dos três é recusado em vez de ser arquivado como um id de usuário que não existe.
A garantia de cache zero
Uma condição é sempre executada com o cache de SQL contornado. Você não pede por isso e não pode desligar. Um alerta que disparasse sobre um resultado em cache não seria um alerta — seria o relato de algo que foi verdade em algum momento. Sem leitura de cache, sem incremento na contagem de acertos, sem escrita de cache: a consulta vai ao motor toda vez que é avaliada.
Dispare na transição, não a cada verificação
Entrar na violação dispara uma vez. Permanecer nela é deliberadamente silencioso. O alerta se rearma quando a condição se resolve. Uma métrica que oscila em torno do seu limite envia uma mensagem, não uma por tique.
O canário: rode verify depois de mudar um painel
verify é a única chamada que te diz que a sua receita ainda resolve.
result = client.alerts.verify(alert.id)
if not result.verification.resolved:
for step in result.verification.steps:
print(step.index, step.status, step.detail)
Ele não captura nada, não envia nada, não escreve nenhuma linha e não move nenhuma coluna. Ele roda o mesmo executor que uma captura real roda, em modo só-resolução, contra o próprio código gerado do painel — então não pode divergir do que a captura fará. Cada passo é reportado, não apenas o primeiro: renomeie três seletores e você é avisado sobre três.
Faça disso um hábito, porque a falha que ele pega é invisível de outra forma:
Você edita um painel. Você renomeia um filtro. A receita do alerta ainda aponta para o nome antigo. Nada falha — até um destinatário abrir um e-mail com a imagem da coisa errada dentro.
Um passo vermelho é a mudança incompatível que você acabou de fazer. Rode verify depois de
cada edição de painel que toque um id de página, um nome de seletor ou um valor de filtro, e você
a encontra enquanto ainda dá para consertar.
Toda recusa nomeia a correção
Um 400 desses endpoints existe para ser lido e agido, não repetido (as mensagens abaixo são as
que a API retorna, em 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'.
A ordem em que trabalhar
describe_dashboard_controls(viz_id)— os nomes e valores que este painel realmente aceita. Pular isso é chutar nomes de seletores.- Escreva o SQL. Suas linhas são a violação; suas colunas são a frase.
- Escreva a captura. Use
{column}para que a imagem seja a violação. verify— antes de confiar nele, e de novo depois de cada edição do painel.test— captura de verdade e retorna a linha de entrega para acompanhar. Ele envia uma mensagem aos destinatários do alerta: uma execução de teste é um envio real, e apenas a contabilidade difere de um disparo verdadeiro (lastFiredAtnão se move, porque nada disparou). Execute-o quando uma mensagem real para esses endereços for o que você quer;verifyé o passo que não alcança ninguém.
Permissões
Criar, editar, excluir e testar um alerta exige o papel de creator ou admin e acesso de
edição ao painel. Ler um — e verify, que não deriva nada que você já não pudesse ler — exige
acesso de visualização. Um painel que você não pode editar responde 404, não 403: a recusa
não confirma que o id existe.