Concepts

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's type matches any entry. See Data model for the canonical list.
  • channel — one of email or webhook (see below).

A project can have many rules, and one event can match multiple rules — each fires independently.

Channels

Email

{ "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 event id), duplicates are harmless.
  • Per-rule audit. The dashboard surfaces Alerts → Deliveries with 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 typeWhy
action_blockedA policy denied something. Worth knowing about — every time.
pending_approvalSomething is waiting on a human. Stale pending actions need attention.
errorA 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).