Skip to content

Alert Triage (local)

The alert-triage example is the capstone (L5) of the getting-started golden example. An alert comes in and a phased Op takes it from there. It classifies the alert and gathers context, then proposes a remediation and stops for a person before applying it (a clearly-stubbed step). chant synthesizes the Kubernetes manifests the app runs on, and the triage itself is an ordinary *.op.ts whose steps shell out to this project’s own code.

This tutorial runs the whole thing locally with no cloud, no cluster, and no runtime beyond the one built into chant.

  • Node.js and npm.
Terminal window
cd examples/alert-triage
npm install
npm run dev

npm run dev starts the webhook receiver and sends one demo alert, which starts chant run triage. The run reaches the gate in the Approve phase and stops there:

Op "triage" is gated on "approve-remediation" after 1.4s
Approve the proposed remediation in .chant/triage/current.json
approve : chant approve triage approve-remediation

The run exits 3, not 1. Nothing failed and nothing is being held open. The run read the gate ledger, found nothing standing, and stopped after writing the pending fact. app/start-triage.ts reads exit 3 as “waiting on a human”, which is what lets the webhook answer 202 with a status instead of a 500.

The proposal is on disk between the two runs, so what gets applied is what somebody actually read rather than a fresh classification that moved under them:

Terminal window
cat .chant/triage/current.json
chant approve triage approve-remediation --actor you
chant run triage

chant approve writes the resolution to the gate ledger on the chant/lifecycle branch. The second run reads both facts and walks through the gate, applying the remediation and notifying. Ordering is what makes this safe: a resolution counts only if it is newer than the pending fact it answers, so last week’s approval cannot clear this morning’s proposal.

Every remediation passes the gate, which is a change from the workflow this example replaces. That one paid for its gate with a twelve-hour open wait, so it spent that only on remediations the classifier called risky and let the routine ones through unattended. Nothing is held open now, and the second run is just another run, so there is no cost left to route everything through the gate. risky still does work: it is what the proposal and the notify line say about the change somebody is being asked to clear.

Both start the same Op:

Terminal window
npm run alert # external alert via the webhook (POST /alert)
npm run drift -- --demo # a drift event (the WatchOp/lifecycle counterpart)

The webhook is the receiver the WebApp manifest deploys. The drift source runs chant lifecycle plan --json and triages each drifted resource, so out-of-band cluster changes go through the same triage as external alerts. --demo injects a sample drift so you can see the pipeline without making a real change.

proposeRemediation is a deterministic stub by default — no key, runs offline. Set ANTHROPIC_API_KEY (and npm i @anthropic-ai/sdk) to have it call Claude; override the model with ANTHROPIC_MODEL (default claude-sonnet-4-6). The first run shows chant, not an LLM. The agent may only escalate risk, never de-escalate: a high or critical alert stays risky even if the model calls it safe.

The app’s Kubernetes surface is typed chant in src/:

Terminal window
npm run build # → k8s.yaml (plain Kubernetes)
npm run lint
kubectl apply -f k8s.yaml # e.g. to a local k3d cluster
PieceWhat it is
src/chant manifests — webhook (WebApp) and runner (WorkerPool)
activities/triage.tsthe triage steps (the agent — stub + Claude opt-in)
activities/run-triage.tsthe CLI the Op shells: propose before the gate, apply after it
ops/triage.op.tsthe Op: Propose → Approve (gate) → Remediate
app/the event sources (webhook, drift) and the demo alert

Unit tests cover the triage steps and the event mappers (npm test). See the example README for the full layout, and Gate-as-fact for what the ledger is doing underneath.