Assertion language reference
The assertion language lets a Website (HTTP) or API monitor check more than “did the server answer 2xx”: the status, headers, body text, JSON/XML/HTML/YAML content, timings, redirects, DNS answers and TLS details. Each rule is one line; all rules must pass for the check to pass, and a failing check names every rule that failed. This page is the complete reference. For building rules in the editor, see Assertions and validation rules; for ready-made examples, see the recipes below.
Where assertions apply
Section titled “Where assertions apply”| Monitor type | Status |
|---|---|
Website / HTTP (http) |
Supported. |
API (api) |
Supported (same subjects as HTTP). |
SNMP (snmp) |
Rules can be saved against the SNMP subjects (value, value.type); evaluation is being rolled out. |
| Database, Counter | Use their own condition fields in the monitor editor; the same engine evaluates them. |
| Ping, Port | The language defines their subjects (below); rules cannot be saved on them yet. |
Syntax
Section titled “Syntax”[label:] subject [not] predicate [value] [nocase] # comment- One line = one rule. All rules are ANDed; there is no
and,oror parentheses. - Label (optional):
homepage-ok: status eq 200- the label is what a failure names. notnegates:body not contains "Error".!=is the same asnot eq.nocasemakes a text comparison case-insensitive:body contains "welcome" nocase. Text comparisons are case-sensitive by default.- A subject alone means
exists:body.jsonsaves asbody.json exists. #starts a comment. Identifiers are case-sensitive.
Literals
Section titled “Literals”| Literal | Example | Notes |
|---|---|---|
| Number | 200, 1.5 |
Comparing to a number parses the subject as a number; a value that does not parse fails the rule with a clear message. |
| String | "OK" |
Always double-quoted. "true" and "null" are strings. |
| Boolean | true, false |
Strict: matches a real JSON boolean only; equality only (eq, ne). "true" (quoted) matches the text. |
| Null | null |
eq null tests for a JSON null; exists tests presence. |
| Size | 1kb = 1024, 10kb, 1mb = 1048576 |
Converted to plain numbers when saved. |
| Duration | 500ms, 2s = 2000, 1m, 1h |
Milliseconds; converted when saved. |
| Range (in lists) | [200..299], [..299], [500..] |
Ends are inclusive. |
| Document | json("{\"ok\":true}"), xml("..."), yaml("...") |
Structural comparison with eq (exact) or contains (subset); checked when saved. |
The literal’s type picks the comparison: eq 200, eq "200", eq true and eq "true" are four different rules.
Subjects - HTTP and API monitors
Section titled “Subjects - HTTP and API monitors”A subject reads a part of the check’s result. Unqualified subjects refer to the final response (after redirects).
| Subject | Type | What it reads |
|---|---|---|
status |
number | HTTP status of the final response. |
body |
text | The response body as text. |
body.size |
number (bytes) | Body size. |
body.hash |
text | A hash of the body - compare with previous.body.hash to detect changes. |
body.regex("pattern") |
text | Regex over the body; named groups (?<name>...) can be read with bind("name"). |
body.json, body.xml, body.yaml, body.html |
text | The body, only if it really is that format - otherwise not present. body.json exists = “is this JSON?”. |
body.json.path("$.a.b") |
runtime-typed | JSONPath (RFC 9535) into a JSON body. |
body.yaml.path("$.a") |
runtime-typed | JSONPath into a YAML body. |
body.xml.path("//a") |
runtime-typed | XPath into an XML body. |
body.html.path("nav a") |
collection | CSS selector into an HTML body; each element’s text by default. |
body.html.a, body.html.script, body.html.link |
collection | Links, scripts, link tags; elements have url (default, as written), absolute (resolved URL), text, rel. |
body.<format>.regex("...") |
text | Regex over the body, only when it is that format. |
header("Name") |
text / set | A response header (all values of a multi-valued header). |
setCookie |
collection | The Set-Cookie values. |
hstsHeader |
text | The Strict-Transport-Security header. |
url |
url | The final URL; url.host, url.port, url.path, url.query, url.scheme. |
url.original |
url | The requested URL (before redirects), with the same parts. |
time |
number (ms) | Total response time: connect + TLS + head + data (DNS excluded). |
time.connect, time.tls, time.head, time.data, time.dns |
number (ms) | The phases. time.dns is not part of time. |
dns.ips |
collection | The addresses the host resolved to. |
dns.server |
text | The DNS server that answered. |
conn.ip, conn.ip.family |
text | The address connected to, and its family. |
conn.failed, conn.failedIps |
number / collection | Addresses that failed before one succeeded. |
tls.protocol, tls.cipher |
text | The negotiated TLS version and cipher. |
cert.days, cert.days.left |
number | Certificate validity: total days and days left. |
cert.issuer, cert.subject, cert.serial |
text | Certificate fields. |
cert.san |
collection | Subject Alternative Names. |
requests |
collection | Every request the check made, in order, including the final one. Elements have status, url, header("..."), setCookie. |
redirects |
collection | Only the followed hops (requests minus the final one). |
redirectCount |
number | Number of redirects followed (0 = answered directly). |
bind("name"), bind[0] |
text | A value captured by a named regex group earlier in the list. |
dns, conn, tls and cert are groups - a rule must name a field inside them (dns.ips, not dns).
Subjects - other check types
Section titled “Subjects - other check types”| Type | Subjects |
|---|---|
| Port (TCP) | banner (+ banner.size, banner.hash, banner.regex(...)), time (connect + TLS + read) with time.connect, time.tls, time.dns, ip, dns.ips, dns.server, conn.failed, and with TLS on: tls.protocol, tls.cipher, cert.* |
| Ping | loss (packet loss %), replyCount, sentCount, time (average RTT), time.min, time.max, ttl, ip, dns.ips, dns.server, time.dns; reply collection with time, ttl, ip per reply |
| Database | scalar (the single value), rows (collection), rowCount, time.query |
| Counter | value |
| SNMP | value, value.type |
Predicates
Section titled “Predicates”| Predicate | Example | Meaning |
|---|---|---|
eq / ==, ne / != |
status eq 200 |
Equal / not equal. |
lt / <, le / <=, gt / >, ge / >= |
time lt 2s |
Numeric comparison. |
contains |
body contains "Add to cart" |
Text contains the value; over a collection, an element equals it; with a document literal, structural containment. |
startsWith, endsWith |
url.path startsWith "/api" |
Text begins / ends with. |
matches |
body matches "Order #[0-9]{6}" |
Regular expression, searched anywhere (dot matches newlines; 4 s timeout; pattern up to 1 KB). |
containsAny, containsAll |
body containsAny ["In stock", "Pre-order"] |
Any / all of the listed strings. |
in |
status in [200, 301, 302], status in [200..299] |
The value is in the list or range; over a collection, every element is (subset). |
exists |
body.json exists |
The subject is present (for a format namespace: the body is that format). |
isNumber |
bind("id") isNumber |
The value parses as a number. |
unique |
dns.ips unique |
A collection has no duplicates. (x.unique, with a dot, is the de-duplicating reducer.) |
nocase matters only for textual comparisons (contains, startsWith, endsWith, matches, containsAny,
containsAll, and eq/in with a string); elsewhere it is ignored with a warning.
Shortcuts (sugar)
Section titled “Shortcuts (sugar)”Shortcuts are expanded when a rule is saved; the editor shows what you typed.
| Shortcut | Means |
|---|---|
status isOk |
status in [200..299] |
status isRedirect |
status in [300..399] |
status isClientError |
status in [400..499] |
status isServerError |
status in [500..599] |
body isJson, body isXml, body isHtml |
The body is that format (body.json exists …). |
header("Content-Type") isJson (also isXml, isHtml, isForm) |
The header names that media type family (including +json/+xml suffixes, ignoring parameters, case-insensitive). |
x isEmpty |
Empty (for a format namespace: not that format). |
x isNull |
x eq null |
x absent |
x not exists |
x between 10 and 20 |
x in [10..20] |
bodySize, respTime |
body.size, time |
Collections and reducers
Section titled “Collections and reducers”Collections (requests, redirects, dns.ips, cert.san, setCookie, body.html.a, query results, reply,
rows) compose four ways:
| Form | Result |
|---|---|
requests[0] |
One element (0-based index). |
requests.status |
A projection - the status of every element (still a collection). |
requests.where(current.url contains "cart") |
A filter - the elements matching the condition (still a collection). Name the element with current.; no nesting, one condition. |
requests.count, .min, .max, .sum, .avg, .first, .last, .unique |
A reducer (always last). |
Rules for collections:
- Text predicates are existential:
requests.url matches "/login"passes if any element matches.containsover a collection is exact membership:dns.ips contains "203.0.113.10"does not match203.0.113.100. notmeans none match.- A plain comparison of a whole collection to one value is refused because it is ambiguous. Say which you mean:
.first eq 5(the first),in [5](every element), or.count(how many). - “For all”:
x.where(condition).count eq x.count. “There exists”:x.where(condition) exists. - On an empty collection
.countand.sumare 0;.min,.max,.avg,.first,.lastare null (soexistsis false).
Comparing with the previous check (plan feature)
Section titled “Comparing with the previous check (plan feature)”| Form | Meaning |
|---|---|
previous.x |
The value of x in the previous check - body.hash ne previous.body.hash fails when the page changes. |
delta(x) |
The absolute change of x since the previous check. |
delta(x, s) (also ms, m, h) |
The rate of change per unit of time. |
delta2(x) |
The change of the change (second derivative). |
These need a previous value: the first check (two for delta2) is skipped as “warming” and counts as a pass. A
current value that cannot be read still fails at once. The previous-value family is a plan feature; rules using it
are refused on plans without it.
Rules can also compare two subjects: url.host eq url.original.host, dns.ips eq previous.dns.ips. Comparing a
subject with itself is refused.
Assertion mode
Section titled “Assertion mode”| Monitor setting | API field | Meaning |
|---|---|---|
| Assertion rules | settings.asserts (rows) or settings.assertsSource (text) |
Up to 20 rules; your plan may allow fewer. |
| Use assertions as the verdict | settings.assertMode (default false) |
When on, the rules alone decide whether the response is up; the classic keyword and status fields are refused. Connection, DNS, TLS, timeout and redirect-loop failures still fail the check. When off, rules are checked in addition to the normal verdict. |
Send rules as text with assertsSource - one rule per line, shortcuts allowed - and they are parsed, validated
and stored:
{ "settings": { "assertMode": true, "assertsSource": "status isOk\nbody.json.path(\"$.status\") eq \"ok\"\ntime lt 2s" } }A rule that does not compile is refused with the line named. assertsText in responses shows the stored rules as
text. A failing check records the error type AssertError (codename AssertFailed) and lists each failed rule.
Limits
Section titled “Limits”- Rules per monitor: at most 20; your plan’s own cap may be lower (
403 package_limitwhen exceeded). - Label up to 128 characters; text values up to 2,048; document literals up to 16,384; lists up to 64 items; regex up to 1 KB.
- When the body is larger than the monitor’s maximum response size, a rule that passes by finding something is trusted, while one that depends on something being absent is reported as inconclusive.
Recipes
Section titled “Recipes”| Rule | Catches |
|---|---|
status isOk |
Any non-2xx final response. |
status in [200, 301, 302] |
A URL that may redirect but must never error. |
body contains "Add to cart" |
A 200 page missing its key content. |
body.size gt 1kb |
A blank or truncated page that still returns 200. |
header("Content-Type") isJson |
An API that stops returning JSON. |
body.json.path("$.status") eq "ok" |
A health endpoint reporting a problem. |
body.json.path("$.items").count gt 0 |
An API returning an empty list. |
time lt 2s |
A successful but slow response. |
redirectCount le 2 |
A redirect chain that grew. |
url.host eq url.original.host |
A redirect to a different domain. |
cert.days.left gt 14 |
A certificate close to expiry (on an HTTPS check). |
dns.ips eq previous.dns.ips |
DNS answers that changed (plan feature). |
body.hash eq previous.body.hash |
A page whose content changed (plan feature). |

