Alerts
Email and webhook notifications when matching events land on an execution timeline. Multi-channel, retried, HMAC-signed.
An alert rule subscribes a project to a subset of event types and fans matching events out to email or a webhook. Rules are independent of the policy engine — they fire on events, not on actions, so you can alert on anything that lands on a timeline (denials, errors, approvals waiting, completed runs, etc.).
Anatomy of a rule
{
"id": "alert_…",
"project_id": "proj_…",
"name": "On-call: prod denials",
"enabled": true,
"event_types": ["action_blocked", "error"],
"channel": {
"type": "email",
"target": "[email protected]"
}
}A rule has:
name— free-form, surfaced in the dashboard.enabled— toggle without deletion.event_types— non-empty list. The rule fires when the event'stypematches any entry. See Data model for the canonical list.channel— one ofemailorwebhook(see below).
A project can have many rules, and one event can match multiple rules — each fires independently.
Channels
{ "type": "email", "target": "[email protected]" }Sent via Resend from Klent <[email protected]>. The body is plain text with the event
type, the execution ID, the matched policy reason (when applicable), and a link back to
the execution timeline in the dashboard.
For multiple recipients, create one rule per address — keeps the audit trail (which inbox got which event) clean.
Webhook
{
"type": "webhook",
"target": "https://hooks.acme.com/klent",
"secret": "whsec_…optional…"
}The body is the same JSON event row that lives in the timeline:
{
"id": "evt_…",
"execution_id": "exec_…",
"project_id": "proj_…",
"type": "action_blocked",
"payload": {
"tool": "transfer_funds",
"matched_policy_id": "pol_…",
"reason": "Matched policy \"Block high-value transfers\""
},
"occurred_at": "2026-05-09T15:42:00.123Z"
}Sent as POST with Content-Type: application/json.
HMAC signature
When the rule has a secret, every delivery includes:
x-klent-signature: sha256=<hex>where <hex> is HMAC-SHA256(secret, raw-body). Verify on your side before trusting the
payload. Example (Node):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(req: Request, secret: string, rawBody: string): boolean {
const header = req.headers.get('x-klent-signature') ?? '';
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
// Use timing-safe equality.
return (
header.length === expected.length &&
timingSafeEqual(Buffer.from(header), Buffer.from(expected))
);
}Without a secret, deliveries are unsigned. Don't run unsigned webhooks against anything
you wouldn't expose publicly.
Delivery semantics
- Async. Event insert never blocks on alert dispatch. The request that produced the event returns the moment it lands; rules are matched and dispatched out of band.
- At-least-once. Each delivery is tracked in
webhook_deliveries. On a non-2xx response (or a network error), Klent retries with exponential backoff for up to a few attempts. If your endpoint is idempotent (use the eventid), duplicates are harmless. - Per-rule audit. The dashboard surfaces
Alerts → Deliverieswith status (pending,succeeded,failed,cancelled), attempt count, the upstream response code (or network error), the last try timestamp, and the rule that produced it.
Managing rules
Dashboard
Alerts → New alert opens a sheet where you can pick event types, choose the channel,
and (for webhook) optionally set a secret. Each rule has a … menu with Delete;
toggling enabled is one click.
API
# Create a rule
curl -X POST https://api.klent.dev/v1/alerts \
-H "Authorization: Bearer $KLENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "On-call: prod denials",
"event_types": ["action_blocked", "error"],
"channel": { "type": "email", "target": "[email protected]" }
}'
# List rules
curl https://api.klent.dev/v1/alerts \
-H "Authorization: Bearer $KLENT_API_KEY"Update / delete are dashboard-only today (the same row IDs persist; deletes leave the
historical webhook_deliveries rows intact for audit).
What to alert on
A practical default that covers most teams:
| Event type | Why |
|---|---|
action_blocked | A policy denied something. Worth knowing about — every time. |
pending_approval | Something is waiting on a human. Stale pending actions need attention. |
error | A tool call threw. Debug signal. |
Skip action_executed and decision for default rules — they fire on every successful
agent step and will saturate any inbox in minutes. If you do want them, scope by an
upstream filter (e.g. only execute the webhook when agent_id === "billing-agent") —
that's not in the alert rule today; for now use a webhook handler that filters.
Limits
- A project can have an arbitrary number of rules; we don't cap it.
- A single event can match every rule in the project — each delivery is independent.
- Webhook bodies are the same shape and size as the underlying event row; we don't pad them. Keep your event payloads lean (don't dump full LLM completions into them — see Data model: what not to log).