defineAlert.
What it measures is up to the scope: span telemetry (every tool run, play batch and agent message emits spans), your play and tool runs end to end, a raw SQL query against the orchestration store or your data warehouse, or the records of a model.
Define an alert
alerts/error-spike.ts
schedule takes a 5-field cron expression or an @every interval (@every 5m, @every 1h30m), evaluated in UTC. An alert evaluates at most once a minute — every tick scans your spans, so sub-minute intervals are rejected. In the CDK enabled defaults to true, so a deployed alert is armed; set it to false to deploy one without arming it. folder files the alert under an "alert"-kind folder.
Scope: what it watches
The scope’skind names the data source. Each source pairs with its own metric menu, so a metric can never be asked of a source that can’t compute it.
Spans scope
kind: "spans" watches span telemetry directly. Use any subset of these filters; omitted fields don’t narrow. They mirror the Spans view.
parentAgent and agent are different filters. parentAgent matches spans
of runs an agent started — the agent-trigger case. agent matches an agent
node running inside the watched spans.Runs scope
kind: "runs" watches your play and tool runs end to end, rather than the individual node executions a spans scope sees. A run is one record travelling through a workflow, so count counts records processed and duration measures a record’s whole journey, not one node’s.
Ad-hoc runs, the ones an agent spawns outside a play or a tool, are never
counted, whatever the filters. They live on the workspace’s default workflow,
which this source excludes, so an agent’s own activity can’t drown out the
workflows you are watching. Use a spans scope with
parentAgent to alert on
an agent instead.Records scope
kind: "records" watches the same play and tool work as a runs scope, counted once per record instead of once per attempt. Each record is held in its latest state, so a record that failed and was later re-run into success stops counting as failed, and idle or skipped runs never appear at all. Reach for it when you care about how your records are doing rather than how your executions are doing.
Retries are the difference between the two sources. Three failed attempts at
one record are three failed runs but one failed record, and if a fourth
attempt succeeds, the record counts as a success while the three failed runs
remain. Alert on runs to catch a flaky workflow; alert on records to catch
work that never got done. Ad-hoc runs are excluded here too.
SQL scopes
Two sources run raw SQL and use its result as the value. The query computes the value itself, so the threshold carries only the comparison:kind: "orchestrationQuery"runs read-only ClickHouse SQL over your orchestration data: thespans,runs,batches, andrecordstables, automatically scoped to your workspace.kind: "storageQuery"runs read-only SQL over your data warehouse, referencing models as<dataset>.<model>exactly as the model SQL editor does.
error event. A query that returns no rows or a NULL counts as an empty window instead: the tick is recorded as healthy with value: null and nothing fires. That is what an aggregate over no rows gives you, and what a rate query returns for 0 / 0 in an idle window, so an empty window can never quietly read as a real 0 and breach an lte threshold.
See Querying orchestration data for the columns of each orchestration table, the ClickHouse idioms that differ from PostgreSQL, and the limits a query runs under; see Querying models for the warehouse side.
A warehouse query never waits for a model to refresh. An alert runs on a cron,
and blocking a tick on a sync would leave it measuring a moment that has
passed by the time the sync lands.
Model scope
kind: "model" watches the records of one model, optionally narrowed by a filter, the same filter a segment is built from. Unlike every other source, a model is measured as it stands rather than over the evaluation window:
Threshold: when it breaches
operator is gte (breach at or above value) or lte (breach at or below). Apart from the SQL scopes, where the query is the metric, metric says what is measured. It is required, since it decides what the value means, and a metric that supports aggregations requires one too.
Spans, runs and records share a metric menu; the scope decides what is being counted, one node execution, one record’s whole journey, or one record:
A model has its own four, about the records themselves and about how fresh they are:
recordsShare requires the scope’s filter: a share of everything is
always 100, so the pair is rejected at save time rather than firing on a
constant.Actions: what fires on breach
Each action becomes its own run, exactly like a play’shealthAlertActions. An action is a connector action, an agent, or a tool, and its config is the input it runs with:
config is typed already — every agent takes the same { prompt, output? }, so a misspelled key is an editor error in the literal above with nothing to import.
A connector action or a tool has an input of its own, and a bare object literal leaves it unchecked. Wrap it in alertConnectorAction / alertToolAction to have TypeScript check config against the real thing:
{{ … }} template string — so a numeric input can be bound to {{event.value}}. Where the two differ is the source of the schema: a connector action’s comes from cargo-ai cdk types, so an integration you haven’t synced keeps the loose object, while a tool’s comes from its own defineWorkflow input and needs no sync — but a toolRef(uuid) names a tool you didn’t author here, so that one stays loose. Both are helpers rather than the type of actions because TypeScript can’t infer a per-element type through an array literal, the same reason agentConnectorTrigger is one.
config is interpolated against the firing under two roots — alert is what you configured, event is what this firing measured — so an action can say what happened:
The evaluation window
An alert does not re-scan a fixed lookback on every tick. Each evaluation covers the time since the previous one, so windows are contiguous and never overlap and every span is judged exactly once:- Window start — where the last evaluation ended. The very first evaluation starts from the moment the alert was last saved.
- Window end — slightly behind now, by an allowance for span indexing lag. Spans that land late are picked up by the next tick instead of being missed.
@every 5m means each evaluation judges roughly the last five minutes.
The window applies to the spans, runs and records sources. A SQL scope windows itself (or doesn’t), and a model scope is a statement about the records as they stand right now; for those two, the cron is only how often the question gets asked.
Actions fire at most once per window. Before firing, an alert atomically
claims its window; if a retry or an overlapping tick already claimed it,
nothing is recorded and nothing fires. Actions spawn runs that spend credits
and can take real action, so a duplicate is worse than a rare miss — and a
sustained breach is detected again on the next tick anyway.
From the CLI
create also takes --actions (a JSON array), --description and --folder.
On update, passing none to --description or --folder clears the field,
while omitting the flag leaves it untouched.
Unlike
defineAlert, an alert created through the API or the CLI starts
disabled — arm it with alert update --uuid <uuid> --enabled true once
its scope and threshold look right.preview computes the value
now and reports whether it would breach, without firing anything:
--window-minutes (default 60) is the lookback for spans, runs and records
scopes; a query scope windows itself and a model scope is measured as it stands,
so the flag doesn’t affect either.
Using the UI
See Using the UI to build an alert visually, with a live preview of the spans it matches, and Events for reading an alert’s history. An alert built in the UI can be brought back into code:cargo-ai cdk pull writes it as a defineAlert under alerts/, with its scope, threshold and actions referencing the other resources it was pulled alongside.
