---
title: "Create a metric alarm"
method: POST
path: "/orgs/{org_id}/alarms"
tags: ["alarms"]
---

# Create a metric alarm

`POST /orgs/{org_id}/alarms`

Create a metric alarm with a threshold over evaluation periods.

Set `denominator_metric_name` for a **rate** rather than a count: the datapoint becomes `metric_name / denominator_metric_name` and `threshold` reads as a fraction (`0.05` = 5%). Prefer this for anything that scales with traffic — a fixed error count pages at 3am once the org grows. Pair it with `min_sample_count` so a single failure in a quiet minute isn't a 100% error rate.

Both series must be binned on the same clock, since they are divided period by period. For outcome rates that means `app.run.terminal_count` — runs that *finished* in the period — and not `app.run.count`, which counts runs that *started* and includes ones still running. Mismatched pairs are rejected.

Ratio alarms also require `stat` `sum` or `count`. The stat is applied to each series and the two results divided, so only stats that add up across the minutes in a period come out as a rate — `avg` would divide two means and `min`/`max` would take their extremes from different minutes. Other stats are rejected rather than silently producing a number that isn't a rate.

A sustained incident only ever notifies once, so repeat messages mean the alarm is flapping. Three levers fix that, and which one depends on the metric: `clear_threshold` (fire high, clear low) for continuous series like rates and latencies; `datapoints_to_alarm` for spiky ones that cross the line briefly; and `missing_data: "ignore"` for sparse ones, where a minute with no traffic is absence of evidence rather than a recovery.

Use `GET /orgs/{org_id}/metrics/catalog` to see which metric names and labels this org is actually emitting. Two metrics won't be listed until you alarm on them: `org.credits.usage_pct` (percent of the billing period's prepaid budget consumed) and `org.credits.remaining` (credits left in it) are only sampled for orgs that have an enabled alarm on one, since the underlying read is the billing gate's and too heavy to run speculatively. Both are levels sampled once a minute, so use `max`, `avg` or `min` — `sum` would add one reading per minute in the period and is rejected. `org.credits.remaining` goes negative under overage.

Percent and headroom are worth having separately: 95% of a large budget can still be thousands of credits, while 50% of a small one is one bad afternoon. For burn *volume* rather than budget position, alarm on `org.credits.spend` with `sum` — that one carries `project_id` and `workforce_id` dims, so it can scope to a single project or workforce the way a budget can't.

For latency, `stat` accepts any percentile from `p0.1` to `p99.9`, in tenths. Equivalent spellings are fine (`p95.0` is accepted and comes back as `p95`), but anything finer than a tenth is rejected rather than rounded, so an alarm never silently watches a different point than the one you asked for. These are exact, computed over the individual run durations rather than interpolated from bins, which is also why they are limited to `app.run.duration_ms`, cannot be a ratio, and cannot carry `label`, `project_env_id` or `dims.deployment_id` — a run row has no such column, so those filters would silently widen the alarm to everything. Prefer a high percentile over `avg` for latency: an average hides the tail that users actually notice, and hides it harder as traffic grows.

Two different project fields, gated separately. Top-level `project_id` is ownership — who may see and edit the alarm afterwards — and setting it requires `projects.alarms.manage` on that project, while leaving it out makes an org-level alarm and requires `alarms.manage`. `dims.project_id` only narrows which metric bins are read, but it still exposes that project's numbers through the alarm's notifications, so it requires `projects.alarms.read` on the project it names.

## Path parameters

- `org_id` string, required

## Request body

- CreateAlarmReq — Body for **POST** `/orgs/{org_id}/alarms`.
  - `channel` CreateChannelReq, required — Channel reference embedded in another resource's create body (alarms). Either reuse by `id`, or supply `type` + `config` inline.
    - `config` object — Omitted or null when linking by `id` only.
    - `id` string, nullable — Existing `NotificationChannels.id` for this org. When set, `type` / `config` / `name` are ignored.
    - `name` string, nullable
    - `type` 'email' | 'slack'
  - `clear_threshold` number, double, nullable — Hysteresis: once in `alarm`, a period only stops counting as breaching once it clears *this* value rather than `threshold`. Must sit on the non-breaching side of `threshold` — below it for `gt`/`gte`, above for `lt`/`lte`. Fire at `0.05` and clear at `0.03`, and a metric hovering on 5% produces one transition instead of one per swing. Without it the only defences against flapping are a longer `period_seconds` or a higher `datapoints_to_alarm`, both of which also delay the *first* detection.
  - `comparison` 'gt' | 'gte' | 'lt' | 'lte', required
  - `datapoints_to_alarm` integer — Datapoints that must breach out of the last `evaluation_periods`.
  - `denominator_metric_name` string, nullable — Turns the alarm into a **ratio** condition: each period's datapoint becomes `metric_name / denominator_metric_name`, read with the same dims, stat and period. `threshold` is then a rate — `0.05` for a 5% error rate, not `5`. Pair it with `min_sample_count`; see that field.
  - `dims` AlarmDims — Metric dimension filters. Omitted = aggregate across that dimension. Not `Copy` since `label` is owned.
    - `deployment_id` string, nullable
    - `label` string, nullable — `Metrics.label` — the span name behind a trace-derived metric. Scopes the alarm to one tool/step instead of the sum over every span the filter matched.
    - `project_env_id` string, nullable
    - `project_id` string, nullable
    - `workforce_id` string, nullable — Agent / workflow app (`OrgsApps.id`).
  - `evaluation_periods` integer
  - `metric_name` string, required — Metric to watch, e.g. `app.run.errors`, `deployment.health`, `org.credits.spend`. The numerator when `denominator_metric_name` is set.
  - `min_sample_count` integer — Minimum denominator for a ratio period to count. Below it (or at zero) the period has no datapoint and follows `missing_data`. Exists because ratios are meaningless on tiny samples: 1 error out of 1 run is 100%. Set it to the smallest traffic level you'd actually act on.
  - `missing_data` 'not_breaching' | 'breaching' | 'insufficient' | 'ignore'
  - `name` string, required
  - `period_seconds` integer — Length of one evaluation period in seconds (multiple of 60, >= 60).
  - `project_id` string, nullable — Scope the alarm to a project. Omit for an org-level alarm.
  - `stat` string, required — Aggregation applied to each evaluation period. Percentiles run from p0.1 to p99.9 in tenths; equivalent spellings such as p95.0 are accepted and returned canonically as p95, while a finer value such as p95.05 is rejected rather than rounded. They are exact rather than interpolated from bins, so they are only available on app.run.duration_ms and cannot be used with denominator_metric_name.
  - `threshold` number, double, required

## Response `201`

Alarm and channel link created

- CreateAlarmOut
  - `channel_action` 'linked' | 'reused' | 'created', required — How the request was wired to `NotificationChannels`.
  - `channel_id` integer, required
  - `id` integer, required

## Other responses

- `400` — Bad request
- `403` — Forbidden

---

[API](https://skmtc.net/timbal-ai/apis/timbal-platform-api.md) · [All operations](https://skmtc.net/timbal-ai/apis/timbal-platform-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/timbal-ai/timbal-platform-api/versions/dd7e398ce0d3/schema)
