Pular para o conteúdo principal

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:

CampoO que ele decide
whenO SQL cujas linhas são a violação
sayA frase que o destinatário lê, preenchida a partir da primeira linha
everyDe quanto em quanto tempo a condição é observada
captureQue imagem é tirada, em Playwright
toQuem 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​

  1. describe_dashboard_controls(viz_id) — os nomes e valores que este painel realmente aceita. Pular isso é chutar nomes de seletores.
  2. Escreva o SQL. Suas linhas são a violação; suas colunas são a frase.
  3. Escreva a captura. Use {column} para que a imagem seja a violação.
  4. verify — antes de confiar nele, e de novo depois de cada edição do painel.
  5. 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 (lastFiredAt nã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.