docs/alert-webhook-template-variables.md
Rama main · réplica de lectura del repositorio original.
# Alert webhook template variables
**Generic** and **incident.io** webhook bodies are Handlebars templates. These
variables are available inside them, so a receiver can route, filter or dedupe
a HyperDX alert without parsing the human-readable message body.
| Variable | Type | Notes |
| --- | --- | --- |
| `{{title}}` | string | Alert title. |
| `{{body}}` | string | Rendered message body (markdown). |
| `{{link}}` | string | Deep link back into HyperDX. |
| `{{state}}` | string | Raw internal alert state. |
| `{{eventId}}` | string | Unique id for this firing. |
| `{{startTime}}` / `{{endTime}}` | number | Evaluation window, Unix ms. |
| `{{startTimeISO}}` / `{{endTimeISO}}` | string | The same window, ISO-8601. |
| `{{alertId}}` | string | Stable id of the alert itself — the key to dedupe on. |
| `{{status}}` | string | `firing`, `resolved`, `no_data`, `pending` or `error`. |
| `{{alertType}}` | string | `search`, `dashboard_chart` or `inline_query`. |
| `{{comparator}}` | string | `>=`, `>`, `<`, `<=`, `=`, `!=`, `between`, `outside`. |
| `{{threshold}}` | number | The configured threshold. For `between`/`outside`, the lower bound. |
| `{{thresholdMax}}` | number | Upper bound of a `between`/`outside` condition. Always unset for every other comparator, even if the alert was once a range, so guard it (see below). |
| `{{value}}` | number | The value that triggered or resolved the alert. |
| `{{groupKey}}` | string | The breaching group, for a grouped alert. |
| `{{var}}` | object | The breaching group's values by group-by column or SQL alias. Use one value at a time, e.g. `{{var.customer_id}}`, or `{{var.[service.name]}}` for a name with dots. Empty when not grouped. |
| `{{sourceQuery}}` | string | The condition behind the alert — search expression or SQL (see below). |
| `{{teamId}}` | string | Team the alert belongs to. |
| `{{note}}` | string | The alert's freeform note — commonly a runbook link. |
| `{{tags}}` | list of strings | The alert's tags. `{{tags}}` renders them comma-separated (`team-payments,prod`); iterate with `{{#each tags}}` to emit a JSON array (see below). Empty when the alert has no tags. |
Strings are JSON-escaped, so they are safe to drop into a quoted slot.
Numbers (`startTime`, `endTime`, `threshold`, `thresholdMax`, `value`) are
emitted raw for unquoted slots. Every enriched variable is optional and renders
as an empty string when the alert doesn't carry it — an alert with no group has
an empty `{{groupKey}}`, for instance.
`{{sourceQuery}}` reads whichever fields the alert's condition lives in: a
saved-search alert reports the search's, a dashboard-tile alert its tile's, and
an inline alert its own. For a raw SQL chart that is the whole SQL template.
Otherwise the condition is spread over several inputs and they are joined with
`AND`, each bracketed when there is more than one — for example
`(ServiceName: "checkout") AND (SeverityText: "error")`. An alert with no
condition at all reports an empty string.
It reports only what the alert query actually applies, so it will not always
match everything the chart shows. A builder chart contributes its chart-level
`where` and the `aggCondition` of the series the alert reads — the last one,
which is the series that produces the value. A chart's pinned `filters` and
`having` are **not** included, because a tile or inline alert does not apply
them. A saved search contributes its `where` and its pinned `filters`, which it
does apply.
Treat the result as opaque text rather than something to parse or re-run:
nothing in the variable set says which dialect you got, and the parts of a
builder condition can each be Lucene or SQL, so the joined string is not
necessarily valid in either. It is truncated at 2000 characters. PromQL charts
do not support alerts, so no webhook carries a PromQL expression.
Tags let one shared webhook route alerts by owner: tag each alert with its
team and read the tag in the receiver. To send them as a JSON array rather
than one comma-separated string:
```
{
"tags": [{{#each tags}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]
}
```
An empty string is not valid JSON in an unquoted numeric slot, so guard any
number that may be absent. Compare against `undefined` rather than using
`{{#if}}`, which treats a legitimate bound of `0` as absent:
```
{
"threshold": {{threshold}}{{#unless (eq thresholdMax undefined)}},
"threshold_max": {{thresholdMax}}{{/unless}}
}
```
Keep a newline or space between `{{/unless}}` and a closing `}` — Handlebars
reads `}}}` as a triple-stache and fails to compile the template.
The template is Handlebars, not JSON: it only has to be JSON once rendered.
Write string arguments inside `{{ … }}` with plain or single quotes —
`{{#if (eq state "ALERT")}}` or `{{#if (eq state 'ALERT')}}` — never JSON-escaped
as `\"ALERT\"`, which Handlebars cannot parse. The webhook form shows where a
body fails to compile. The API rejects a body that doesn't compile, or that
calls a helper other than `eq` and the Handlebars built-ins, when a webhook is
created, updated, or tested.
The **Send test** button on the webhook form fills every variable with a sample
value, so a template that uses them can be checked before an alert fires. The
sample is a `between` alert, so `{{thresholdMax}}` is populated there — a test
send will not catch a template that breaks when an optional number is absent,
which is what the guard above is for.
## incident.io
incident.io accepts only `firing` or `resolved` in `status`, so the default
body maps HyperDX's state onto those two and carries `{{status}}` in
`metadata.hyperdx_status`, alongside the alert id, condition, evaluation
window and the alert's tags (comma-separated, in `metadata.tags`). Only a firing and a resolve are delivered today, so that key reads
`firing` or `resolved` as well. `metadata` is free-form, but incident.io only
surfaces the keys its alert source is configured to parse.
`deduplication_key` is `{{eventId}}`, which is stable per alert, group and
channel across a firing and its resolve, so incident.io closes the alert it
opened rather than opening a second one. It is derived from those three, so
changing the alert's group-by or deleting and recreating the webhook starts a
new key and leaves anything already open unresolved. Dedupe on `{{alertId}}`
instead if every group of a grouped alert should collapse into one incident.
## Example
Routing by severity and deduping on the alert rather than the firing:
```
{
"alert_id": "{{alertId}}",
"dedup_key": "{{alertId}}-{{groupKey}}",
"status": "{{status}}",
"summary": "{{title}}",
"urgency": "{{#if (eq alertType 'dashboard_chart')}}low{{else}}high{{/if}}",
"value": {{value}},
"threshold": {{threshold}},
"window": { "start": "{{startTimeISO}}", "end": "{{endTimeISO}}" },
"runbook": "{{note}}",
"link": "{{link}}"
}
```