Alerts & channels
An alert is a (threshold, channel) pair. The SDK fires the channel when an agent crosses the threshold. Four notification channels ship in the box: email, Slack, PagerDuty, and webhook. A separate kill switch can halt the agent outright, covered at the end.
How alerts work #
A threshold is a fraction in (0.0, 1.0], so 0.70 fires when usage reaches 70% of the budget. After every LLM call the engine recomputes the usage ratio and fires any threshold that was just crossed.
A threshold can watch any budget dimension, not just cost: cost, tokens_total, or duration. Each alert binds to one dimension so the backend evaluates the right axis. See Limits & budgets for where each dimension comes from.
Because a channel binds to a single dimension, one watching cost will not fire on token usage; to alert on both, register the channel twice. To keep a busy agent from flooding you, each (budget, threshold, channel) combination fires at most once every five minutes within a budget period.
The AgentKavach cloud aggregates your spend across every agent, process, and host, then delivers each alert channel itself — email, Slack, PagerDuty, and webhooks. You provide the target for each channel; there is no extra setup and nothing to deliver from your own side.
ChannelType #
| Enum | String value | Behavior |
|---|---|---|
ChannelType.EMAIL | "email" | Transactional email, sent by the AgentKavach backend. |
ChannelType.SLACK | "slack" | POST to a Slack incoming webhook. Block Kit message with a text fallback. |
ChannelType.PAGERDUTY | "pagerduty" | PagerDuty Events API v2 trigger. |
ChannelType.WEBHOOK | "webhook" | POST JSON to a URL. Optional HMAC-SHA256 signing. |
from agentkavach import AgentKavach, ChannelTypeConfiguring channels #
from agentkavach import AgentKavach, Budget, ChannelType
guard = AgentKavach(
provider="openai",
api_key="ak_prod_...", # AgentKavach key
llm_key="sk-...", # OpenAI key
agent_name="research-bot",
budget=Budget.daily(50),
channels=[
AgentKavach.channel(ChannelType.EMAIL, threshold=0.50, to="team@acme.com"),
AgentKavach.channel(ChannelType.SLACK, threshold=0.80, webhook_url="https://hooks.slack.com/..."),
AgentKavach.channel(ChannelType.PAGERDUTY, threshold=0.95, routing_key="R0..."),
],
)Build a list of ChannelConfig objects with AgentKavach.channel() and pass them to the channels= argument.
Misconfigurations fail at startup
Every channel is validated the moment you construct it. A missing recipient, a bad webhook URL, or a threshold outside (0.0, 1.0] raises a ValueError right away, with a message naming the problem. This is standard Python for an argument that has the right type but an unusable value, and it means you catch the mistake on the first run rather than the night an alert silently fails to fire.
Email #
AgentKavach.channel(ChannelType.EMAIL, threshold=0.80, to="oncall@acme.com")You provide only the recipient address. The AgentKavach backend sends the message through Resend using its own key, so there is nothing else to configure and no email credential of your own to manage.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
channel_type | ChannelType | str | Yes | — | ChannelType.EMAIL or "email". |
threshold | float | Yes | — | Trigger ratio in (0.0, 1.0]. |
to | str | Yes | — | Recipient email address. |
Slack #
The channel posts a Block Kit message to your Slack URL. Each message includes a plain-text text fallback so simpler Slack apps still render something useful.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
channel_type | ChannelType | str | Yes | — | ChannelType.SLACK or "slack". |
threshold | float | Yes | — | Trigger ratio in (0.0, 1.0]. |
webhook_url | str | Yes | — | Your Slack URL (from Slack's Incoming Webhooks feature). |
template | dict | No | — | Optional override with a 'text' key and optional 'blocks'. Supports template variables (see below). |
Get a Slack URL #
- Open api.slack.com/apps and click Create New App, then From scratch.
- Pick the workspace, then enable Incoming Webhooks in the left sidebar.
- Click Add New Webhook to Workspace, choose a channel, and copy the Slack URL it gives you.
- Store it in
.envasAGENTKAVACH_SLACK_WEBHOOK_URLand pass it viawebhook_url=os.environ["AGENTKAVACH_SLACK_WEBHOOK_URL"].
Treat the Slack URL as a secret
Anyone with this Slack URL can post to your channel. Keep it in an environment variable or a secret manager, and never commit it.
Custom template
AgentKavach.channel(
ChannelType.SLACK,
threshold=0.70,
webhook_url=os.environ["AGENTKAVACH_SLACK_WEBHOOK_URL"],
template={
"text": ":rotating_light: {agent_name} at {pct}% of {period} {budget_type} limit",
},
)PagerDuty #
AgentKavach.channel(
ChannelType.PAGERDUTY,
threshold=0.95,
routing_key=os.environ["AGENTKAVACH_PAGERDUTY_ROUTING_KEY"],
)The channel triggers an incident through the PagerDuty Events API v2. Severity maps from the threshold: warning below 90%, error from 90% to 99%, critical at 100%.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
channel_type | ChannelType | str | Yes | — | ChannelType.PAGERDUTY or "pagerduty". |
threshold | float | Yes | — | Trigger ratio in (0.0, 1.0]. |
routing_key | str | Yes | — | Events API v2 integration key (32-char hex). |
Create the routing key #
- In PagerDuty, open Services, Service Directory, then New Service.
- Pick an escalation policy, then add an Events API v2 integration.
- Copy the integration key from the integration row.
- Store it as
AGENTKAVACH_PAGERDUTY_ROUTING_KEYand pass it viarouting_key=os.environ[...].
Integration key is the routing key
PagerDuty calls the same 32-character hex string Integration Key in the UI and routing_key in the API. AgentKavach uses routing_key to match the Events API v2 spec.
Webhook #
AgentKavach.channel(
ChannelType.WEBHOOK,
threshold=0.80,
url="https://api.acme.com/budget-alerts",
secret=os.environ["AGENTKAVACH_WEBHOOK_SECRET"],
)The channel POSTs a JSON payload to your URL on every threshold breach. Use HTTPS in production (plain HTTP is allowed only for local development). Provide a secret and, when you set a secret, signs each request so your receiver can verify it. See Verifying requests below.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
channel_type | ChannelType | str | Yes | — | ChannelType.WEBHOOK or "webhook". |
threshold | float | Yes | — | Trigger ratio in (0.0, 1.0]. |
url | str | Yes | — | Endpoint to POST the JSON payload to. |
secret | str | No | "" | HMAC-SHA256 secret. When set, the signature is sent in X-AgentKavach-Signature. |
The payload
A single JSON object, sent with Content-Type: application/json. The default body is below; override the shape with a template using any of the template variables.
{
"event": "threshold.breach",
"level": "WARNING",
"agent": "research-bot",
"budget_type": "cost",
"pct": "80",
"current": "$40.0600",
"limit": "$50.0000",
"remaining": "$9.9400",
"threshold_pct": 0.8
}threshold_pct is the configured trigger ratio; pct is the actual usage percentage at the time of the breach. The body carries no timestamp — use the X-AgentKavach-Timestamp header (below) for request freshness.
Signature headers
When a secret is set, every request carries two headers:
X-AgentKavach-Signature: sha256=<hex>— HMAC-SHA256 of the raw request body, keyed with your secret.X-AgentKavach-Timestamp: <unix-seconds>— when we sent the request. Reject anything older than a few minutes to block replays.
Verifying requests
Compute the HMAC over the raw bytes you received — do not re-serialize the parsed JSON first, or the signature won't match (we sign compact, key-sorted JSON). Always compare in constant time, and reject stale timestamps. A complete receiver:
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["AGENTKAVACH_WEBHOOK_SECRET"]
MAX_SKEW_SECONDS = 300 # reject requests older than 5 minutes
@app.post("/budget-alerts")
def budget_alerts():
body = request.get_data() # RAW bytes — sign over THESE, not the parsed JSON
signature = request.headers.get("X-AgentKavach-Signature", "")
timestamp = request.headers.get("X-AgentKavach-Timestamp", "")
# 1. Replay protection: timestamp must be present and recent.
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > MAX_SKEW_SECONDS:
abort(401, "stale or missing timestamp")
# 2. Verify the HMAC-SHA256 signature in constant time.
expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", signature):
abort(401, "bad signature")
# 3. Trusted — act on the alert.
alert = request.get_json()
print(f"{alert['agent']} hit {alert['pct']}% of its "
f"{alert['budget_type']} budget ({alert['current']} of {alert['limit']})")
return "", 200No secret? No signature.
The signature and timestamp headers are only sent when you configure a secret. For any endpoint that takes an action, set one and verify every request so only AgentKavach can trigger it.
Delivery #
The AgentKavach cloud both evaluates and delivers every alert. Evaluation runs centrally because only the cloud has your full spend across every process and host — a single SDK instance sees only its own traffic. Delivery then happens from the cloud too: it makes the HTTP call to each channel's target — email through AgentKavach's mail provider, Slack to your incoming-webhook URL, PagerDuty to your routing key, and webhooks to your URL.
You provide the target for each channel and the cloud handles the rest. Every alert is evaluated, recorded for your dashboard history, and delivered exactly once.
channels:
ops_slack:
type: slack
webhook_url: https://hooks.slack.com/services/T000/B000/xxxx
budget_hook:
type: webhook
url: https://hooks.example.com/budget-alerts
secret: ${ALERT_WEBHOOK_SECRET}Webhooks sign their requests when you set a secret — verify the X-AgentKavach-Signature header on your receiver; see Verifying requests above.
Template variables #
By default each channel formats its own message. When you want different wording, pass a template to the channel (as shown in the Slack example above) and reference any of the variables below with Python str.format() syntax, for example {agent_name} hit {pct}% of its {period} budget. Unknown names are left untouched, so a typo never crashes an alert.
| Variable | Example |
|---|---|
{agent_name} | research-bot |
{pct} | 82 |
{spent} / {spent_fmt} | 4.13 / $4.13 |
{budget} / {budget_fmt} | 5.00 / $5.00 |
{remaining} / {remaining_fmt} | 0.87 / $0.87 |
{period} | daily |
{budget_type} | cost |
{severity} | warning / error / critical |
{resets_at} | 2026-05-30 00:00 UTC |
{dashboard_url} | https://agentkavach.com/dashboard/agents/... |
Credentials #
AgentKavach never reads channel credentials from the environment. Give each channel its credential explicitly when you build the client, or define it in your YAML config. Reading from os.environ in your own code is encouraged, for example webhook_url=os.environ["AGENTKAVACH_SLACK_WEBHOOK_URL"]; the SDK simply will not do it for you.
Never commit secrets
Webhook URLs, routing keys, and signing secrets all grant the ability to post into your systems. Store them in .env or a secret manager. Never in source.
Kill switch #
The kill switch is not a notification channel. The four channels above tell someone that spend is climbing; the kill switch acts on it by stopping the agent. You arm it by giving the client an on_kill callback and a kill threshold, usually 1.0. When usage reaches that point the SDK invokes your callback and then refuses to run again: every later guard.create() on that instance raises BudgetExceededError immediately.
import sys
from agentkavach import AgentKavach, Budget, ChannelType
def emergency_stop(reason):
# reason tells you WHY: "cost" for a budget kill, "tier_agent_limit"
# etc. for a backend stop — see SDK reference › on_kill kill reasons.
print(f"agent killed: {reason}")
sys.exit(1)
guard = AgentKavach(
provider="openai",
api_key="ak_prod_...",
llm_key="sk-...",
agent_name="expensive-agent",
budget=Budget.daily(10),
on_kill=emergency_stop,
channels=[
AgentKavach.channel(ChannelType.KILL, threshold=1.0),
],
)The reason parameter is optional — a zero-argument callback (def emergency_stop():) keeps working unchanged. Declare the parameter to receive the kill reason; the full vocabulary is in the SDK reference.
A killed instance does not resume
Once the kill fires, that AgentKavach instance stays killed for its lifetime. Construct a fresh instance to start again, for example at the next budget period.
Dashboard Kill button
The Kill button on the Agents page records a kill on the backend. The SDK picks it up on its next config sync, and on the agent's next guard.create() it invokes the agent's on_kill callback and then raises a stop exception. It runs the same callback as a budget kill, but the callback's reason parameter and the exception type (IngestRejectedError for backend stops, a BudgetExceededError subclass) tell the two apart.
The button stops the agent on its next call
The kill takes effect on the agent's next LLM call, not the instant you click. To make it terminate the process immediately, define an on_kill callback that exits, for example sys.exit(1). Without one, the agent stops making LLM calls but any surrounding non-LLM work continues until it finishes on its own.