Ir para o conteúdo principal

Assertion Mode: Complete Response Validation Reference

  • assertion mode
  • response validation
  • API monitoring
  • website monitoring
  • SNMP monitoring
  • port monitoring
  • HostTracker

Most uptime monitors judge a check with a handful of fixed fields: a status code that counts as "down," a list of statuses to ignore, maybe a keyword that has to appear somewhere in the body. That works until it does not. An API that legitimately answers 404 for a missing resource. A page that returns 200 with an empty shell because the database timed out. A redirect chain that quietly starts routing through someone else's domain. None of that fits into a status-code box and a keyword field.

HostTracker's assertion mode replaces those fixed fields with a small rule language you write yourself, rule by rule, inside the monitor editor. Each rule says exactly what has to be true about the response - the status, a piece of the body, a header, how long it took, where a redirect ended up - and a check fails only when one of your rules fails. It is the same language whether you are checking a website, a REST API, a database query, a TCP port, a Ping target, an SNMP counter or a generic Counter check: one set of subjects, one set of predicates, learned once and reused everywhere the mode appears. This guide covers the whole language - every subject, operator, reducer, sugar shortcut and syntax form the engine actually implements - not just a sampler of the popular ones.

What assertion mode replaces

The classic Response Validation fields are still there when assertion mode is off: an expected status range, a list of statuses to ignore, a keyword that must (or must not) appear. Assertion mode is a switch that swaps that fixed set of fields for a list of rules you build yourself. Turning it off again before you save puts the classic fields straight back - nothing about a monitor is locked into one mode permanently.

The point of one language instead of a pile of per-type gadgets is that it composes. A "must be JSON and contain a field" check, a "must still say Add to cart" check and a "must not have gotten slower" check are not three separate form features - they are three rules in the same syntax, sitting in the same list, each passing or failing on its own.

Turning it on

Assertion mode lives inside the Response Validation section of the monitor editor, for HTTP (website) and API monitors. Flip the switch, and the classic fields are replaced by a numbered rule list with an "Add rule" button and a live counter showing how many rules you have used against your plan's limit.

Assertion mode switched on, with the first rule built and validated

Each rule is validated as you build it - a rule that is not yet finished carries a distinct "unfinished" state; a rule the editor can actually evaluate is marked complete. Every rule also has a small </> button that opens its plain-text source form, so you can read or copy the exact expression instead of only the box-by-box view.

The mental model: subject, predicate, value

Every rule has the same shape: a subject (what you are looking at - the status code, the body, a header, the response time), a predicate (the test - equals, contains, exists, is less than) and, for most predicates, a value to compare against. status eq 200 is a subject and a predicate and a value; body.json exists is a subject and a predicate with no value at all, because "exists" needs nothing else.

Building a rule in the editor starts with picking what to check, then the condition, then the value:

Building a second rule: the field picker lists every subject with its type and an example

Rules in the list are combined with AND - a check fails when any single rule fails, and passes only when every rule passes. There is no OR yet; if you want to accept one of several statuses or one of several strings, express that inside a single rule instead of writing alternatives (status in [200, 301, 302], body containsAny ["In stock", "Pre-order"]). A rule that fails names itself in the alert, so give each one a short label if you can - the alert then says which specific expectation broke, not just that something did.

What you can check

The picker groups subjects into the response's own document (the body, the headers) and its transport (status, timing, redirects, TLS). A few of the subjects that matter most day to day - the full list, organized the same way the engine organizes it internally, is the reference that follows this section:

SubjectWhat it isExample
statusThe HTTP status code of the final response, after redirects are followedstatus isOk
bodyThe response body as plain textbody contains "Add to cart"
body.json / body.xml / body.html / body.yamlThe body, but only when it genuinely parses as that format - otherwise the subject is simply not presentbody.json exists
body.json.path("$.a")A JSONPath query into a JSON body (the same query language also works over YAML, since YAML parses into the same object model)body.json.path("$.status") eq "ok"
body.size / body.hashThe response size in bytes, and a fingerprint of the body's contentbody.size gt 1kb
header("Name")One response header, looked up by nameheader("Content-Type") isJson
time and time.connect / time.tls / time.dns / time.headTotal response time, and its network, TLS-handshake, DNS and server think-time phasestime lt 2s
redirects / requestsThe hops a check followed, and the full chain (including the final response)redirects.count eq 0
url and its parts (.host, .scheme, .path...)The final URL the check landed onurl.scheme eq "https"
cert.days.leftDays remaining before the TLS certificate expirescert.days.left gt 14

Numbers accept unit suffixes so you never have to convert by hand: 2s means 2000 milliseconds, 1kb means 1024 bytes. Text comparisons are case-sensitive unless you add nocase to the rule.

The complete reference

What follows is every subject, operator, reducer, sugar shortcut, filter and syntax form the assertion engine registers for HTTP and API checks, taken straight from the engine's own registry (the same data that feeds the rule editor's pickers and its docs). Entries that are not yet generally available carry an explicit note in parentheses - reserved fields the engine has not started populating, candidates still rolling out to agents, and the stateful family that needs a specific plan. Anything without such a note is live today, on every plan that has assertion mode at all.

The reference is organized the way the engine itself groups things: response fields first, then the ways to reach into a body, then timing, network and TLS, then the operators and reducers and shortcuts that work on all of them, then the handful of check kinds beyond HTTP that speak the identical language.

Response

The subjects that describe the response itself: its status, its raw body, its headers, the URL it actually answered from, and the chain of requests that got it there.

NameWhat it isType/valuesExample
bodyThe response body as decoded text.stringbody contains "Welcome"
headerOne response header of the final response, by name.stringheader("Content-Type") isJson
redirectsThe redirect hops that were followed - the request chain without its final element.collectionredirects.count eq 0
statusThe HTTP status code of the final response, after redirects are followed.numberstatus isOk
body.sizeThe size of the downloaded body, in bytes.numberbody.size lt 500000
urlThe URL that actually answered, after all redirects. A url: compare it as text, or read its parts (url.host, .port, .path, .query, .scheme).urlurl.host eq "www.example.com"
requestsEvery request this check made, in order - the redirect hops and then the final request.collectionrequests.count le 2
url.hostThe host part of the URL.stringurl.host eq "example.com"
url.pathThe path part of the URL.stringurl.path startsWith "/api"
url.portThe port part of the URL.numberurl.port eq 443
url.queryThe query string of the URL.stringurl.query contains "token"
url.schemeThe scheme of the URL (http or https).stringurl.scheme eq "https"
body.hashA digest of the response body - compare it with previous.body.hash to detect content changes.stringbody.hash eq previous.body.hash
httpVersion (reserved - not yet populated)The negotiated HTTP version.stringhttpVersion eq "HTTP/2"
setCookieThe Set-Cookie values of the final response, one item per cookie - match one with contains or matches, count them with .count.collectionsetCookie exists
steps (not available yet)The steps of a multi-step check. Not available yet.collectionsteps.count le 1
url.originalThe URL the check was configured with, before any redirect. A url like url itself: it has the same parts (url.original.host, .port, .path, .query, .scheme).urlurl.original.scheme eq "http"
url.original.hostThe host part of the URL.stringurl.original.host eq "example.com"
url.original.pathThe path part of the URL.stringurl.original.path startsWith "/start"
url.original.portThe port part of the URL.numberurl.original.port eq 80
url.original.queryThe query string of the URL.stringurl.original.query contains "utm_"
url.original.schemeThe scheme of the URL (http or https).stringurl.original.scheme eq "http"

Two aliases worth knowing: redirectCount is shorthand for redirects.count - redirectCount eq 0 reads slightly more naturally than the equivalent long form when you mean "the check answered directly, no redirects." And setCookie at the top level is the final response's cookie list; the same name reappears per-hop inside requests/redirects (see Hop subjects below), holding just that one request's cookies.

Extracting values from the body

These subjects only resolve when the body genuinely is the format they name - a JSON namespace on an HTML page is simply not present, not a typed error. That makes exists the standard way to probe format: body.json exists asks "did this really parse as JSON?" The four format namespaces each carry a matching .path(...) query function (JSONPath for json/yaml, XPath for xml, CSS selectors for html) and a matching .regex(...), plus a format-agnostic body.regex(...) that reads the raw text regardless of format. body.html.a, body.html.script and body.html.link are ready-made collections of every link, script and <link> element on the page - see Element scopes below for the fields available on each one.

NameWhat it isType/valuesExample
body.jsonThe response body, but only when it really is JSON.stringbody.json exists
body.json.pathQuery the JSON body with a JSONPath expression.jsonbody.json.path("$.status") eq "ok"
body.htmlThe response body, but only when it really is HTML.stringbody.html exists
body.html.pathSelect page elements with a CSS selector.collectionbody.html.path("h1").first contains "Welcome"
body.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists
body.xmlThe response body, but only when it really is XML.stringbody.xml exists
body.xml.pathQuery the XML body with an XPath expression.stringbody.xml.path("//item/@id") exists
body.html.aEvery link in the page.collectionbody.html.a.count gt 0
body.html.linkEvery <link> the page declares. A bare mention means the link targets.collectionbody.html.link.count gt 0
body.html.scriptEvery script the page loads. A bare mention means the script sources.collectionbody.html.script.count gt 0
body.yamlThe response body, but only when it really is YAML.stringbody.yaml exists
body.yaml.pathQuery the YAML body with a JSONPath expression.jsonbody.yaml.path("$.services.web.image") contains ":1."
body.html.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists
body.json.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists
body.xml.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists
body.yaml.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists

A regex family detail worth calling out: named capture groups ((?<id>...)) inside a .regex(...) rule write into a check-scoped binding namespace that a later rule can read back with bind("id") or bind[0] by position - see Captured values below.

Captured values

One subject lives in its own registry group because it does not read the response directly - it reads what an earlier rule captured from it.

NameWhat it isType/valuesExample
bindA value captured by an earlier regex rule, by capture name or by position.stringbind("id") gt 1000

Pair it with a regex rule earlier in the list: a rule like body.regex("order-(?<id>\d+)") exists captures the order id into the name id, and a later rule can test it with bind("id") gt 1000 or, for a positional (unnamed) capture, bind[0].

Timing

One total plus a breakdown by phase, so a "got slower" alert can also say which part got slower.

NameWhat it isType/valuesExample
timeTotal response time: connect + TLS + waiting + download. DNS resolution is not included.numbertime lt 2000
ttfb (reserved - not yet populated)Time to first byte.numberttfb lt 200
time.connectTime taken to establish the TCP connection.numbertime.connect lt 500
time.dataTime taken to download the response body.numbertime.data lt 1000
time.dnsTime taken to resolve the domain name. Not included in the bare time subject.numbertime.dns lt 200
time.headTime waited for the response headers.numbertime.head lt 500
time.tlsTime taken for the TLS handshake.numbertime.tls lt 300

The bare time subject is connect + TLS + waiting-for-headers + downloading the body; DNS resolution is deliberately excluded from it and lives only in the sibling time.dns, because DNS is frequently cached and would otherwise make the same endpoint look inconsistently fast or slow between checks.

Network: DNS and connection

dns and conn are pure namespaces in the registry - a group with no value of its own, so a rule can never stop on the bare name (the editor and the engine both refuse a chain that finishes on dns or conn alone, pointing you at the real fields below).

NameWhat it isType/valuesExample
connThe network connection the check made - the address that served the response and any connect failures.string-
conn.failedHow many of the resolved addresses failed to connect.numberconn.failed eq 0
conn.failedIps (candidate - rolling out to agents)The resolved addresses that failed to connect.collectionconn.failedIps.count eq 0
conn.ipThe IP address that actually served the response.stringconn.ip eq "203.0.113.10"
conn.ip.family (candidate - rolling out to agents)Whether the response was served over IPv4 or IPv6.stringconn.ip.family eq "IPv4"
dnsDNS resolution of the monitored name - which addresses it resolved to and which server answered.string-
dns.ipsAll IP addresses the domain name resolved to during this check.collectiondns.ips contains "1.2.3.4"
dns.serverThe DNS server that answered the resolution.stringdns.server eq "8.8.8.8"

TLS and certificate

Like dns and conn, tls and cert are pure namespaces - the certificate/handshake facts live one level down.

NameWhat it isType/valuesExample
cert.days.leftDays remaining until the site certificate expires.numbercert.days.left gt 14
certThe certificate the server presented - days to expiry, issuer and identity fields.string-
cert.daysDays since the certificate was issued.numbercert.days gt 0
cert.issuerThe certificate authority that issued the site certificate.stringcert.issuer contains "Let's Encrypt"
cert.sanThe domain names the certificate is valid for.collectioncert.san contains "www.example.com"
cert.serialThe certificate's serial number.stringcert.serial exists
cert.subject (reserved - not yet populated)The certificate's subject. Not captured yet - coming with a future agent version.stringcert.subject contains "example.com"
tlsThe TLS handshake of the secured connection - negotiated protocol version and cipher.string-
tls.cipherThe negotiated cipher suite.stringtls.cipher contains "AES"
tls.protocolThe negotiated TLS version.stringtls.protocol eq "Tls13"

cert.subject is listed here for completeness but is not populated yet - see the Honesty section below for what "reserved" means in practice. The everyday certificate rule is cert.days.left gt 14, covered in the worked-rules section.

Hop subjects: fields inside requests and redirects

requests and redirects are collections, and each item in them is a full request/response record with its own reduced field set - not every top-level subject makes sense per-hop (a hop has no certificate of its own, for instance), and one field exists ONLY per-hop: hstsHeader, the Strict-Transport-Security header of that specific request in the chain.

NameWhat it isType/valuesExample
statusThe status code of this request in the chain.numberstatus eq 200
urlThe URL this request was made to.urlurl startsWith "https://"
headerA response header of this request. A hop keeps a reduced, single-value header record - list predicates (containsAny, containsAll, unique) apply only on the final response's header.stringheader("Content-Type") contains "json"
hstsHeaderThe Strict-Transport-Security header of this request.stringrequests[0].hstsHeader exists
setCookieThe Set-Cookie header values of this request.collectionsetCookie exists
url.originalThe URL the check was configured with, before any redirect. A url like url itself: it has the same parts (url.original.host, .port, .path, .query, .scheme).urlurl.original.scheme eq "http"
url.hostThe host part of the URL.stringurl.host eq "example.com"
url.portThe port part of the URL.numberurl.port eq 443
url.pathThe path part of the URL.stringurl.path startsWith "/api"
url.queryThe query string of the URL.stringurl.query contains "token"
url.schemeThe scheme of the URL (http or https).stringurl.scheme eq "https"
url.original.hostThe host part of the URL.stringurl.original.host eq "example.com"
url.original.portThe port part of the URL.numberurl.original.port eq 80
url.original.pathThe path part of the URL.stringurl.original.path startsWith "/start"
url.original.queryThe query string of the URL.stringurl.original.query contains "utm_"
url.original.schemeThe scheme of the URL (http or https).stringurl.original.scheme eq "http"

The per-hop header(...) deliberately keeps a reduced, single-value record - list-shaped predicates like containsAny or unique only make sense on the final response's full header map, so they apply there and not per-hop. Remember the two collections mean different things: requests is the whole chain including the final response that actually answered (so it is never empty, even for a direct 200), while redirects is only the hops that got followed on the way there (empty when nothing redirected).

Element scopes: fields inside one collection item

An element scope is the field set available on ONE item of a collection - after indexing into it (requests[0]) or while filtering it with where(current. ...) (see the next section). Six collections carry their own element scope: the four HTML element collections share one shape, and requests/redirects share the hop-subject shape documented just above.

Element scopeWhat one item looks like
body.html.pathOne element matched by a body.html.path("selector") CSS query.
body.html.aOne <a> link element from body.html.a.
body.html.scriptOne <script> element from body.html.script.
body.html.linkOne <link> element from body.html.link.
requestsOne request in the full chain (see the hop-subjects table above).
redirectsOne followed redirect hop (the same fields as requests, see above).

The shared shape for body.html.path(...), body.html.a, body.html.script and body.html.link - one row per link, script tag or <link> element:

NameWhat it isType/valuesExample
urlThe link target exactly as the markup writes it.stringbody.html.a.url contains "/checkout"
textThe visible text of the element.stringbody.html.a.text contains "Sign in"
absoluteThe link target resolved against the page URL into an absolute address - a list of urls, with the url parts available per element (.host, .path, .port, .query, .scheme).urlbody.html.a.absolute.scheme in ["https"]
relThe element's rel attribute.stringbody.html.a.where(current.rel eq "nofollow").count eq 0
absolute.hostThe host part of the URL.stringbody.html.a.absolute.host.unique.count le 3
absolute.portThe port part of the URL.numberbody.html.a.absolute.port in [443]
absolute.pathThe path part of the URL.stringbody.html.a.absolute.path startsWith "/"
absolute.queryThe query string of the URL.stringbody.html.a.absolute.query not contains "session"
absolute.schemeThe scheme of the URL (http or https).stringbody.html.a.absolute.scheme in ["https"]

Two defaults worth knowing before you write a rule against these collections: a bare mention of body.html.a (or .script/.link) means its .url projection - so body.html.a.count gt 0 is really counting hrefs, and if you reopen a saved rule it will read back with .url spelled out explicitly, because a stored rule always says exactly what it evaluates rather than leaning on an implicit default. And .url is the raw href attribute exactly as the markup wrote it - a relative link stays relative; .absolute is the same link resolved against the page's own URL, which is what you want when checking the scheme or host of outbound links (body.html.a.absolute.scheme in ["https"]).

Operators

The predicate half of every rule - what test to run against the subject. All 15 are shared across every check kind; which ones make sense for a given subject depends on its type (a status code takes lt/gt, a body takes contains, a collection takes exists/unique/in).

NameWhat it isApplies toExample
eqEqual to. Against a json("…") / xml("…") / yaml("…") literal it is an EXACT structural match: same keys (any order), arrays in order, same values.any typebody.json eq json("{\"ok\": true}")
ltLess than.numberstime lt 2000
leLess than or equal to.numberscert.days.left le 30
gtGreater than.numbersbody.size gt 0
geGreater than or equal to.numbersstatus ge 500
containsContains the text - over a list, passes when the value IS one of the items (exact match; use matches to search inside items). Against a json("…") / xml("…") / yaml("…") literal it is STRUCTURAL containment: everything the literal states must be present and match; the response may have more.text or listbody.json contains json("{\"status\": \"ok\"}")
startsWithStarts with the text - over a list, when ANY item does.texturl.path startsWith "/api"
endsWithEnds with the text - over a list, when ANY item does.texturl.path endsWith ".json"
matchesMatches the regular expression anywhere in the text - over a list, when ANY item does.textbody matches "order-\\d+"
containsAnyContains at least one of the listed values (over a list: some item EQUALS one of them).listbody containsAny ["In stock", "Pre-order"]
containsAllContains every one of the listed values (over a list: each value equals some item - witnesses may differ).listbody.html.a.text containsAll ["Home", "Contact"]
inIs one of the listed values or ranges - over a list, every item is.value(s) or rangestatus in [200, 301, 302]
existsThe subject is present.any subjectheader("ETag") exists
isNumberThe value reads as a number.text or anybind("id") isNumber
uniqueThe list has no duplicates.listdns.ips unique

Two operators deserve a second look because they are easy to reach for by habit and mean something narrower than they sound: contains over a list is EXACT membership (the value must equal one whole item, not appear as a substring inside one) - reach for matches when you want substring behavior across a collection's elements. And eq against a json(...)/xml(...)/yaml(...) literal is a full structural match (same keys, arrays in the same order, same values), while contains against the same kind of literal is structural containment - everything the literal states must be present, but the real response may carry more.

Reducers

A reducer collapses a collection to one value, and can only appear at the very end of a chain. The examples below use requests.status - the one naturally numeric collection HTTP subjects form - purely as an illustration; the same eight reducers work identically over any numeric collection in any check kind (a Ping check's round-trip times, a filtered set of database rows, and so on).

NameWhat it collapsesApplies toExample
countHow many items the list has.any collectionrequests.count le 2
minThe smallest number in the list.numeric collectionrequests.status.min ge 200
maxThe largest number in the list.numeric collectionrequests.status.max lt 500
sumThe sum of the numbers in the list.numeric collectionrequests.status.sum gt 0
avgThe average of the numbers in the list.numeric collectionrequests.status.avg lt 300
firstThe first item of the list.any collectionredirects.status.first eq 301
lastThe last item of the list.any collectionrequests.status.last eq status
uniqueThe list with duplicates removed.any collectiondns.ips.unique.count le 3

unique is deliberately overloaded and the spelling is what tells the two meanings apart: written as an operator with no dot (x unique) it is a PREDICATE - "this collection has no duplicates," and it fails by naming the repeat. Written as a reducer with a dot (x.unique) it is a TRANSFORM - the deduplicated collection itself, which still needs something after it (x.unique.count eq 1). A dangling x.unique with nothing following it is a syntax error precisely because the engine cannot guess which of the two you meant.

Sugar

Sugar predicates are shorthand that the engine expands into ordinary core predicates before the rule is ever sent to an agent - a sugar predicate costs nothing extra to add (no agent bundle, no version floor) because by the time it reaches the wire it already looks like a ordinary rule. Prefer sugar whenever it exists: it reads better, and it is exactly equivalent to writing the long form out by hand.

NameWhat it meansApplies toExample
isOkThe status is a success code (200-299).status-
isRedirectThe status is a redirect code (300-399).statusstatus isRedirect
isClientErrorThe status is a client error (400-499).statusstatus isClientError
isServerErrorThe status is a server error (500-599).statusstatus isServerError
isJsonThe content really is JSON.document or media-type textheader("Content-Type") isJson
isXmlThe content really is XML - the body parses as XML, or a header names an XML media type.document or media-type textbody isXml
isHtmlThe content really is HTML - the body parses as HTML, or a header names an HTML media type.document or media-type textbody isHtml
isFormA media type names a form encoding: application/x-www-form-urlencoded or multipart/form-data. Unlike isJson/isXml/isHtml there is no matching body namespace, so this only applies to a media-type value such as a Content-Type header.media-type text onlyheader("Content-Type") isForm
isEmptyNothing there: an empty text, a zero size or an empty list.text, size or listbody isEmpty
isNullThe value is null.any valuebody.json.path("$.deletedAt") isNull
absentThe subject is not present at all.any subjectheader("X-Debug") absent
betweenA number within an inclusive range.numbers (2-operand)body.size between 50 and 500kb

isJson/isXml/isHtml are context-sensitive on purpose, and which meaning applies is decided by what you write them on, not by guessing from the name: written on the document itself (body isJson) they mean "does this parse as that format," the same thing as body.json exists. Written on a text value that names a media type, such as a header (header("Content-Type") isJson), they mean "does this name that format's MIME family" - see MIME families below for exactly which media types qualify. isForm only has the second meaning, since there is no body.form namespace to parse into - writing body isForm is a static error rather than a rule that could never be true.

The where(...) filter

Keep only the elements that match, then carry on from there. It is the one filtering construct in the language, and it has exactly one shape: where(current.<field> <condition> <value>).

Filtering is different from projecting: requests.url contains "cart" asks whether such a hop exists anywhere in the chain, while requests.where(current.url contains "cart").status asks what that specific hop returned. Filtering does not collapse the list, so finish with .first, an in [...] check, or .count. It goes one level only in this version of the language, and takes exactly one condition inside.

PartWhat it namesNotes
current.<field>A field of each element being filtered, written after current.This is the half people get wrong: current.url is the individual hop's URL, not the final response's. Only the element's own fields can follow current..
the conditionAny ordinary predicate, including not and nocaseAny condition that works at the top level of a rule works here too.
the valueWhat each element's field is compared againstUsually a plain value. A subject written here is read from the OUTER response, not the element - where(current.url eq url.original) compares each hop against the URL the check was configured with, which is why the element side has to say current. out loud.

Example: requests.where(current.url contains "cart").status.first eq 301 - keep only the hops whose URL contains "cart", then look at the status of the first one that matched.

Two more limits, both enforced by the parser with their own message: where cannot be nested inside another where, and its argument is exactly one condition - no and/or inside it. It only applies to a collection whose elements carry named fields (requests, body.html.a), not to a plain projected list like dns.ips, where there is nothing for current. to name.

Syntax reference

Everything else the grammar defines: how to name a rule, how to write ranges and lists, what a bare subject means, and the small punctuation forms that show up across many rules.

FormWhat it doesKindExample
whereKeep only the elements that match, then carry on from there.collection filterrequests.where(current.url contains "cart").status.first eq 301
notInverts the condition that follows.flagbody not contains "error"
nocaseCompare text without regard to upper and lower case.flagbody contains "welcome" nocase
currentInside where(…), names the one element being filtered.element reference (inside where)requests.where(current.status eq 301)
rangeA range of numbers, both ends INCLUDED.number rangestatus in [200..299]
listA list of values to compare against.value liststatus in [200, 301, 302]
docLiteralA typed document value for structural comparison: the quoted text is parsed as JSON, XML or YAML when the rule is saved.json/xml/yaml textbody.json contains json("{\"user\": {\"active\": true}}")
implicitExistsA rule that names only a subject checks that it is present: writing just body.json saves as body.json exists.bare subject linebody.json
indexPicks ONE item of a list by position, counting from 0.0-based integerrequests[0].status
labelAn optional name for the rule, written before a colon.name before a colon"json ok": body.json exists
commentEverything after # is a note to yourself; it does not affect the check.trailing # textstatus isOk # the happy path

A few worth a plain-English pass: label gives a rule a name that shows up in the alert instead of the rendered expression - write it before a colon, as in "json ok": body.json exists. implicitExists means a line that names only a subject and no predicate at all is shorthand for exists - writing just body.json saves as body.json exists, and reopening the rule shows the spelled-out form. range and list are the two shapes a value can take after in: a range ([200..299], both ends included) or an explicit list ([200, 301, 302]) - a value can also be open-ended on either side ([400..]). docLiteral is the json(...)/xml(...)/yaml(...) wrapper used for structural comparison against the body (covered under Operators above, and again in the worked rules below). index picks exactly one item out of a collection by position, 0-based, the same way bind[0] reads a positional capture. current only has meaning inside a where(...) filter, covered in its own section above.

Wrappers: previous, delta and delta2

Three forms that look backward instead of only at the current response - each one is gated to plans with stateful assertions, and each is explained in full in "Comparing against the previous check" below.

NameWhat it isType/valuesExample
delta (requires a plan with stateful assertions)How much a numeric value changed since the previous check - optionally as a rate per ms/s/m/h.numberdelta(body.size, h) lt 1mb
delta2 (requires a plan with stateful assertions)The change of the change - is a value accelerating?numberdelta2(time) gt 0
previous (requires a plan with stateful assertions)The value this subject had on the previous check.anystatus eq previous.status

Flags: not and nocase

Two modifiers that attach to a rule rather than standing as subjects or predicates of their own.

FlagWhat it doesKindExample
notInverts the condition that follows.flag (attaches to a rule)body not contains "error"
nocaseCompare text without regard to upper and lower case.flag (attaches to a rule)body contains "welcome" nocase

not composes with the predicate that follows rather than negating the whole rule as a unit - x not != y reads as x eq y, not as a double negative. When a subject might be entirely absent, prefer the sugar absent over not exists: the two mean exactly the same thing, but absent says so directly. nocase only changes anything on a genuinely textual comparison; the editor (and this reference) still let you write it anywhere, but it is silently meaningless on a numeric test like status eq 200 nocase.

MIME families

The lookup tables behind isJson, isXml, isHtml and isForm when they are applied to a media-type value such as a header. Each family matches the obvious top-level media type plus, for JSON and XML, any type ending in the matching structured-syntax suffix - which is what makes header("Content-Type") isJson correctly pass on application/problem+json or application/vnd.api+json and not just the plain application/json.

FamilyMatchesPaired sugarExample
jsonapplication/json, text/json, or any media type ending in +json (application/problem+json, application/vnd.api+json, application/ld+json, ...).sugar: isJsonheader("Content-Type") isJson
xmlapplication/xml, text/xml, or any media type ending in +xml (image/svg+xml, application/atom+xml, ...).sugar: isXmlheader("Content-Type") isXml
htmltext/html or application/xhtml+xml.sugar: isHtmlheader("Content-Type") isHtml
formapplication/x-www-form-urlencoded or multipart/form-data.sugar: isFormheader("Content-Type") isForm

Matching ignores everything from the first ; onward (so application/json; charset=utf-8 still matches) and is case-insensitive.

Subjects for database, counter, SNMP, port and ping checks

Every operator, reducer, sugar shortcut and syntax form documented above is identical for these five check kinds - the engine registers the same 15 operators, 8 reducers, 12 sugar predicates and 11 syntax forms for all six kinds, without exception. What differs per kind is the subject list: each kind exposes a small set of subjects unique to what it actually measures, plus a subset of the network/timing/TLS subjects already documented above (the meaning adapts slightly to the kind - a Database check's time is connect-plus-query rather than connect-plus-TLS-plus-download, for instance, since there is no HTTP response to download).

Database checks

Shares time, time.connect and time.dns with the subjects documented above (re-scoped to a database connection: connect-plus-query, DNS resolution of the database host). Unique to database checks:

NameWhat it isType/valuesExample
scalarThe single value the query returned (Scalar mode).any-
rowsThe rows the query affected or returned (NonQuery mode).collection-
time.queryTime the query itself took.number-

A worked example is in the next section: use rows.count to assert on how many rows a query affected or returned, and scalar to check a single returned value directly.

Counter checks

Shares time with the subjects documented above - as a candidate-status field for Counter checks specifically (round-trip time of the counter request; see the Honesty section for what "candidate" means). Unique to counter checks:

NameWhat it isType/valuesExample
valueThe measured counter value.number-

SNMP checks

Shares time and time.dns with the subjects documented above (host-name resolution plus the SNMP request itself). Unique to SNMP checks:

NameWhat it isType/valuesExample
time.requestTime the SNMP request took.number-
valueThe value the OID returned.any-
value.type (candidate - rolling out to agents)The SNMP data type name of the returned value.string-

value.type is candidate-status - see the Honesty section. Note on availability: the assertion-mode Response Validation switch itself has not reached the SNMP monitor editor yet - it shows a "Coming soon" panel with these two subjects named as a preview of what is coming, rather than a working rule list. The subjects and the evaluator underneath are real and already registered; only the SNMP-specific editor UI is still pending.

Port checks

The largest shared list of any non-HTTP kind - a TCP port check carries essentially the same connection, DNS, TLS and certificate subjects an HTTP check does, since a port check can optionally negotiate TLS too: bind, cert and its six sub-fields, conn and its four sub-fields, dns and its two sub-fields, time/time.connect/time.dns/time.tls, and tls/tls.cipher/tls.protocol - all documented in the sections above, with the same meanings. Unique to port checks:

NameWhat it isType/valuesExample
bannerThe data read from the socket after connect.string-
banner.hash (candidate - rolling out to agents)A digest of the banner - compare with previous.banner.hash to detect service changes.string-
banner.regexMatch the text with a regular expression and take what it captured.stringbody.regex("order-(?<id>\\d+)") exists
banner.size (candidate - rolling out to agents)The size of the banner, in bytes.number-

banner.hash and banner.size are candidate-status - see the Honesty section. banner.regex is live today and works exactly like body.regex(...) on an HTTP check: named captures feed the same bind(...) namespace.

Ping checks

Shares the connection and DNS subjects with the sections above: conn, conn.failed, conn.failedIps, conn.ip, conn.ip.family, dns, dns.ips, dns.server, time (here, the average round-trip time of the successful replies) and time.dns. Unique to ping checks:

NameWhat it isType/valuesExample
lossThe percentage of echo requests that got no reply.numberloss eq 0
replyThe successful echo replies.collection-
sentCount (candidate - rolling out to agents)How many echo requests were sent.number-
time.maxThe slowest reply's round-trip time.number-
time.minThe fastest reply's round-trip time.number-
ttlThe TTL of the first successful reply - compare with previous.ttl to spot route changes.number-

sentCount is candidate-status - see the Honesty section. ttl is a good one to know about even outside a strict pass/fail rule: pairing it with previous.ttl (see Wrappers above) flags a route change between checks, since a packet's time-to-live at arrival reflects how many hops it crossed.

Rules people actually want to write

The page is up and actually says the right thing

A status code alone cannot tell you a page rendered correctly - a 200-with-empty-shell response is the classic silent failure a status-only check misses.

status isOk
body contains "Add to cart"

An API answers with JSON and the field you expect

Check that the endpoint is still speaking JSON, then reach into it. The first line catches an HTML error page served where JSON should be, a common outage shape behind a proxy; the second checks the API's own reported health, not just its transport status.

body isJson
body.json.path("$.status") eq "ok"

The response got slower than it should

Break it down when you need to know what to fix rather than just that something is slow - time.connect, time.tls, time.dns and time.head separate network, handshake, resolver and application time.

time lt 2s

A redirect sent visitors off your own domain

Compares where the check ended up against the URL you configured - it fails the moment a redirect chain, hijacked, misconfigured, or just changed by a deploy, lands somewhere you did not intend.

url.host eq url.original.host

The page changed since the last check

Useful on content that should be stable between checks - pricing, terms, a published policy - and doubles as a basic defacement signal.

previous.body.hash eq body.hash

A number within a range, written the short way

Catches both a stripped-down page and a runaway one in a single rule, without writing out an explicit range.

body.size between 50 and 500kb

A certificate that is about to expire

The single most common TLS check, and one that a plain up/down monitor cannot express at all - a certificate that still validates today but lapses next week gives no other warning sign. Pair it with cert.issuer if you also want to catch a certificate quietly issued by the wrong authority.

cert.days.left gt 14

A database query returning the wrong number of rows

On a Database check, rows is the collection a query affected or returned - counting it catches a query that should return exactly one account record silently starting to return zero (deleted, or a broken join) or more than one (a duplicate that should not exist).

rows.count eq 1

An SNMP value drifting out of range

An SNMP device that answers at all is not the same as one that is healthy - a monitored OID (disk usage, queue depth, temperature) can climb well past a safe threshold while the device itself stays perfectly reachable. Comparing the polled value against a range catches that a simple reachability check cannot.

value lt 90

A TCP service answering with the wrong banner

A port that accepts connections is not the same as the right service listening on it - a hung process, a misconfigured proxy, or a downgraded/substituted service can all still complete a TCP handshake. Reading the greeting text a service sends right after connect (an SMTP, FTP or similar banner) catches what a bare port-open check cannot.

banner contains "220"

Comparing against the previous check

A handful of subjects look backward instead of only at the current response: put previous. in front of a subject to read what it was on the last check, or wrap it in delta(...) to get the difference between then and now, or in delta2(...) to get the change of that change - useful for spotting a value that is not just drifting but accelerating (a response time that is not only rising but rising faster each check). previous.body.hash eq body.hash above is exactly this - "does the body's fingerprint match what it was last time." delta(body.size, h) reads the change as a rate per hour instead of a raw difference, for values you expect to grow steadily rather than jump.

The first time a rule like this runs, there is nothing to compare against yet, so it simply skips rather than failing - the check is marked as "warming up," not as broken, and nobody gets a false alert just for adding the rule. Warming applies only when the CURRENT read is sound and it is the baseline that is missing; if the current read itself fails (say, the body is not valid JSON this time), the rule fails exactly as the plain, non-previous. version of it would - a missing baseline and a broken current read are treated as the different situations they are, never folded into the same "still warming up" excuse. This kind of rule also needs the value to genuinely change between checks, which makes it meaningless on a check method that never advances state (a HEAD request, for instance).

Comparing against the previous check is a feature of higher-tier plans; on a plan without it, those subjects show up grayed out in the editor with an explanation rather than silently doing nothing.

Limits, and what is deliberately missing

How many rules you can add depends on your plan, shown as a live counter next to "Add rule." However many you are allowed, the engine also enforces an absolute ceiling on every check regardless of plan, so a rule list can never grow without bound - a safeguard against abuse rather than a number meant to constrain ordinary use, since a check with anywhere near that many rules would already be unusually large.

There is no way to write custom JavaScript for a rule, and there never will be - a deliberate choice, not a gap waiting to be filled. Every rule is built from a fixed, named vocabulary of subjects and predicates rather than an open scripting sandbox, which is what keeps a rule readable at a glance and safe to run on every monitoring agent worldwide.

Not every rule works everywhere the moment it is written: some newer conditions in the language need a recent version of the monitoring agent that runs your check, so a brand-new capability can take a little time to reach every check on your account as agents update in the background. Rules built on the stable core of the language are unaffected either way.

A handful of entries in this reference are not fully live yet, and each one is labelled in its table above rather than presented as if it worked today:

  • Reserved (declared, not yet populated by any agent): ttfb, httpVersion, cert.subject.
  • Candidate (already rolling out, not yet on every agent): conn.ip.family and conn.failedIps on HTTP, Port and Ping checks, value.type on SNMP checks, banner.hash and banner.size on Port checks, sentCount on Ping checks, and the Counter check's own time.
  • Not available yet: steps, reserved for a future multi-step check type.
  • Requires a plan with stateful assertions: the whole previous / delta / delta2 wrapper family, on every check kind.

Everything else in this reference - the overwhelming majority of it - is live today on every plan that has assertion mode at all.

Where assertion mode works today

Right now, the Response Validation switch for assertion mode appears on HTTP (website) and API monitors, sharing the same rule editor and syntax. The same rule language and evaluator already run underneath Database, Counter, Port, Ping and SNMP checks at the protocol level - the registry documented above genuinely serves all six check kinds, and a rule saved for one reads the same way if the mode reaches another type's editor later.

SNMP is the clearest preview of that: its monitor editor already shows an Assertion rules panel naming value and value.type as the subjects that will be available, but it is explicitly labelled "Coming soon" and does not yet mount a working rule list - the subjects and the evaluator are real, the editor for them is not finished. Database, Counter and Ping checks do not show any assertion-mode panel in the editor yet at all, even though their subjects are equally live in the engine underneath.

Setting up a real check

If you are validating a REST API rather than a plain webpage, start from API response validation, which covers monitoring your own API's uptime, response time and body together. For a page or site that just needs to stay up and answer correctly from everywhere your users are, global uptime monitoring from 300+ locations is the broader picture assertion mode plugs into. If you would rather set rules through code than through the panel, HostTracker's own API v2 lets you create and update a monitor's assertion rules the same way the editor does. For the certificate-expiry rule above and everything else TLS, domain and TLS certificate monitoring covers the check type that cert.days.left and the rest of the TLS/certificate subjects live on. And if you just want to see one rule evaluate against a real URL right now, with no account required, the free HTTP check tool is the fastest way to try it.

Frequently asked questions

What happens to my old status-code and keyword settings if I turn assertion mode on?

They are replaced by the rule list while the mode is on. Turning the mode back off before you save restores the classic fields exactly as they were - nothing is deleted, the two modes just cannot be active at the same time.

Can I combine multiple conditions in one monitor?

Yes - every rule in the list has to pass. There is no "OR" between separate rules yet; to accept one of several acceptable values, use a single rule with in [...] or containsAny [...] instead of writing rules meant as alternatives.

Will assertion mode ever let me write my own script?

No. Every rule is composed from the language's fixed set of subjects and predicates - there is no custom JavaScript, and that is by design rather than a missing feature.

What happens the first time I add a rule that compares to the previous check?

It has nothing to compare against yet, so it is marked as warming up and skipped rather than failed. You will not get a false alert just from adding a "did this change" style rule.

Do I need a specific plan to use assertion mode?

The mode itself and the everyday rules (status, body, headers, timing, redirects) are broadly available; how many rules you can add at once depends on your plan and is shown live in the editor. Rules that compare against a previous check (previous, delta, delta2) need a higher-tier plan and are grayed out with an explanation if your plan does not include them.

Does assertion mode work on API monitors the same way it works on website monitors?

Yes - HTTP and API monitors share the exact same rule editor, the same subjects and the same predicates. A rule you write for one reads identically on the other.

What does where(...) actually do, and when do I need it?

It filters a collection down to the elements matching a condition, before you look at a field of the result - reach for it when a plain projection like requests.status is not specific enough and you need to ask about one particular hop, link or element rather than the collection as a whole (see the dedicated section above).

Can I write a rule against a database query, an SNMP value, a port banner or a ping result today?

The language and the evaluator already support all of these - the subject tables above cover each one. The rule EDITOR for them is not live everywhere yet: only HTTP and API monitors currently show the Response Validation switch. SNMP shows a "Coming soon" preview of its two subjects; Database, Counter and Ping do not show an assertion-mode panel in the editor at all yet.

Why do some subjects in this reference say "reserved" or "candidate" instead of just listing them normally?

Because they are not equally available yet, and presenting them as ordinary live subjects would be misleading. "Reserved" means the field is declared in the registry but no agent populates it yet; "candidate" means it is already rolling out to agents but has not reached all of them; both are called out explicitly wherever they appear in this reference, and in the summary list in the Limits section above.