ScoutSentinelScoutSentinel
Menu

Step DSL reference

The deterministic step language every Watch compiles to, the Watch definition that wraps it, and JSON examples that validate against the published schema.

v1Updated

Every Watch is stored as an explicit, versioned JSON definition. The natural-language composer proposes this format; you can also write or edit it directly in the Steps tab or through the API (POST /v1/journeys/{journeyId}/watches and POST /v1/watches/{watchId}/versions). Definitions are validated at the edge against the same schema the platform uses (WatchDefinitionSchema in @scoutsentinel/core), then cross-checked: unique probe, step and assertion ids; every assertion stepId resolves to a step; step types match the probe type; cron expressions parse.

Every JSON example on this page is validated against that schema as part of the website build.

Watch definition shape

Field Type Meaning
intent string, 1 to 2000 chars The capability in plain language. Used in Event titles and reports.
channel web, api, voice, email The customer channel this Watch assures.
probes[] 1 to 20 probe definitions Each probe runs its steps in order on one executor.
assertions[] 1 to 100 assertions The contract; see the assertions reference.
schedule interval or cron See schedule.
materiality policy object When failures become an Event; see materiality.
locationPolicy policy object Which location classes and regions run the Watch.
notifications policy object Notification channel ids and when to send.
costClass http, browser, voice, email, mixed Cost class recorded for fair-use budgets.
requiredEvidence[] evidence kinds Kinds that must be present for an Observation to count as complete.

Probe definition

Field Type Meaning
id string, up to 64 chars Unique within the Watch.
type http, api, dns, tls, browser, ct, origin_bypass, voice, email The executor. Step types are restricted per probe type (table below).
steps[] 1 to 100 steps Run in order. The first fail or error stops the run; later steps are recorded as skipped.
timeoutMs 1000 to 600000 Whole-probe budget.
captures capture policy screenshotOn (every_step, failure_and_boundary, first_run_and_change), html, har, trace booleans. The failing step always gets a screenshot and page HTML regardless of policy.
locationPolicy optional Overrides the Watch-level location policy for this probe.
Probe type Allowed step types
browser navigate, click, fill, select, press, waitFor, expect, capture, http.request
http, api http.request
dns dns.resolve
tls tls.inspect
origin_bypass origin.probe, http.request
voice voice.call
email email.send, email.expectInbound

Schedule

Two kinds. Interval schedules run on slots aligned to the Unix epoch, so every scheduler tick agrees on the same slot and a re-delivered job is recognised by its idempotency key {probeId}:{slot}. The platform minimum is 60 seconds; your plan’s minimum interval (minIntervalSeconds in GET /v1/entitlements) applies on top.

{ "kind": "interval", "everySeconds": 300 }
{ "kind": "cron", "expression": "*/15 6-22 * * 1-5", "timezone": "Europe/London" }

Cron expressions have five fields (minute, hour, day of month, month, day of week) and accept numbers, *, */n, a-b, a-b/n and lists. timezone is an IANA name.

Materiality policy

{
  "consecutiveFailures": 2,
  "corroboratingLocations": 1,
  "windowSeconds": 1800,
  "autoResolveAfterPasses": 2,
  "behaviourOpensEvent": false
}

Those are the defaults; every field may be omitted. An outcome Event opens after consecutiveFailures failing runs with the same failure boundary inside windowSeconds, seen from at least corroboratingLocations distinct locations. It resolves automatically after autoResolveAfterPasses consecutive passes. unknown runs never extend or break a failure streak and never open an outcome Event; three consecutive unknowns open a low-severity coverage Event instead. With behaviourOpensEvent true, a passing run whose behaviour fingerprint changed opens a behaviour Event.

Location policy

{ "classes": ["edge", "regional"], "regions": ["lhr", "fra", "syd"], "minLocations": 2 }

classes is one or more of edge, regional, private, partner; regions is optional; minLocations (default 1) is the number of distinct locations expected per slot and feeds coverageLocations assertions. See runner locations.

Notifications

{ "channels": ["<notification channel id>"], "onOpen": true, "onResolve": true, "onBehaviourChange": false, "minSeverity": "low" }

channels holds notification channel IDs from GET /v1/notification-channels. Select the destinations explicitly: an empty list does not select channels marked as default. Legacy organisation-level severity routes may also receive matching events. An enabled channel still needs an active integration and matching delivery rules; saving a policy does not prove delivery. Review notification channels and the delivery log under Integrations.

Locators

Browser steps address elements with a target locator that follows Playwright semantics. A locator sets at least one of:

Field Meaning
testId data-testid attribute
role and name ARIA role plus accessible name ({ "role": "button", "name": "Get quote" })
label Form control label text
placeholder Placeholder text
text Visible text
css CSS selector, last resort

Resolution order is testId, then role with name, then label, placeholder, text and finally css. Prefer the stable end of that list: test ids and accessible names survive redesigns, CSS selectors change most often. Every locator shape below is valid.

[
  { "testId": "quote-price" },
  { "role": "button", "name": "Get quote" },
  { "label": "Postcode" },
  { "placeholder": "Search destinations" },
  { "text": "Continue to payment" },
  { "css": "form[data-step=travellers] button[type=submit]" }
]

Secrets

Plaintext never appears in a definition. Secrets are stored per organisation under Settings, Secrets (POST /v1/secrets, names in UPPER_SNAKE_CASE), envelope-encrypted, and referenced by name:

  • fill steps take secretRef instead of value.
  • http.request header values may be { "secretRef": "NAME" } and are resolved directly.
  • http.request bodies and string header values may contain {{secret:NAME}} placeholders; list each name in the step’s secretRefs so the platform knows which secrets the run needs.

Values are decrypted only inside the execution step, are redacted from screenshots, HTML, HAR, logs and step messages, and never reach a model. GET /v1/watches/{watchId} reports requiredSecrets and missingSecrets so you can see a reference that has no value yet.

Step types

Every step has an id (unique in the Watch, up to 64 chars), a type, and an optional description (up to 500 chars) used in Event titles and the steps list. Fields marked optional may be omitted.

Browser steps

navigate: url (absolute).

{ "id": "open", "type": "navigate", "url": "https://shop.example.com/quote", "description": "Open quote form" }

click: target.

{ "id": "submit", "type": "click", "target": { "role": "button", "name": "Get quote" } }

fill: target plus exactly one of value or secretRef.

[
  { "id": "postcode", "type": "fill", "target": { "label": "Postcode" }, "value": "SW1A 1AA" },
  { "id": "password", "type": "fill", "target": { "label": "Password" }, "secretRef": "TEST_ACCOUNT_PASSWORD" }
]

select: target and value (the option value).

{ "id": "cover", "type": "select", "target": { "label": "Cover type" }, "value": "comprehensive" }

press: key and an optional target to focus first.

{ "id": "search", "type": "press", "key": "Enter", "target": { "role": "searchbox" } }

waitFor: target or urlPattern (at least one) and optional timeoutMs (up to 120000).

[
  { "id": "wait-price", "type": "waitFor", "target": { "testId": "quote-price" }, "timeoutMs": 15000 },
  { "id": "wait-confirm", "type": "waitFor", "urlPattern": "/confirmation" }
]

expect: a check discriminated on kind.

kind Fields Passes when
textVisible text The text is visible on the page
elementVisible target The element is visible
elementHidden target The element is absent or hidden
urlMatches pattern The current URL matches the pattern
title contains The document title contains the string
latencyBelow ms The step so far completed inside the budget
[
  { "id": "price-visible", "type": "expect", "check": { "kind": "elementVisible", "target": { "testId": "quote-price" } } },
  { "id": "no-error", "type": "expect", "check": { "kind": "elementHidden", "target": { "role": "alert" } } },
  { "id": "on-quote", "type": "expect", "check": { "kind": "urlMatches", "pattern": "/quote/result" } },
  { "id": "title-ok", "type": "expect", "check": { "kind": "title", "contains": "Your quote" } },
  { "id": "fast", "type": "expect", "check": { "kind": "latencyBelow", "ms": 3000 } }
]

capture: kind (screenshot, html, trace, har) and an optional label (up to 100 chars). Adds an evidence object at that point in addition to what the probe’s capture policy records.

{ "id": "shot", "type": "capture", "kind": "screenshot", "label": "Quote result" }

HTTP and API steps

http.request: method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), url, optional headers (string or { secretRef } values), optional string body, optional secretRefs, optional expect.

expect field Meaning
status Exact status code
statusRange [min, max] inclusive
headerContains Map of header name to substring
bodyContains Substring of the response body
jsonPath { "path": "$.a.b[0]", "equals": <value> } compared by deep equality
latencyBelowMs Total request time budget
{
  "id": "post-quote",
  "type": "http.request",
  "method": "POST",
  "url": "https://shop.example.com/api/quotes",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": { "secretRef": "QUOTE_API_TOKEN" },
    "X-Client": "scoutsentinel/{{secret:CLIENT_ID}}"
  },
  "body": "{\"destination\":\"LIS\",\"travellers\":2,\"nights\":14,\"apiKey\":\"{{secret:QUOTE_API_KEY}}\"}",
  "secretRefs": ["CLIENT_ID", "QUOTE_API_KEY"],
  "expect": {
    "statusRange": [200, 299],
    "headerContains": { "content-type": "application/json" },
    "jsonPath": { "path": "$.status", "equals": "quoted" },
    "latencyBelowMs": 3000
  }
}

HTTP probes send the ScoutSentinel user agent and the X-ScoutSentinel-Probe header on every request; see synthetic traffic. Redirects are followed manually and the chain is recorded; credentials are stripped when a redirect crosses hosts. The whole body is hashed and an excerpt kept as evidence.

DNS, TLS and origin steps

dns.resolve: name, recordType (A, AAAA, CNAME, MX, TXT, NS, CAA), optional resolver (cloudflare, google, authoritative; the runner’s system resolver when omitted), optional expect with contains and notContains lists.

{
  "id": "apex-a",
  "type": "dns.resolve",
  "name": "shop.example.com",
  "recordType": "A",
  "resolver": "authoritative",
  "expect": { "contains": ["203.0.113.10"], "notContains": ["198.51.100.1"] }
}

tls.inspect: host, optional port (default 443), optional expect with validForDays, issuerContains, sanIncludes, minProtocol (TLSv1.2 or TLSv1.3).

{
  "id": "cert",
  "type": "tls.inspect",
  "host": "shop.example.com",
  "port": 443,
  "expect": { "validForDays": 14, "issuerContains": "Let's Encrypt", "sanIncludes": ["shop.example.com"], "minProtocol": "TLSv1.2" }
}

origin.probe (exposure, origin_bypass probes): host, ip, optional path, and compareWithEdge. Dials the IP directly with the Host header and SNI set to host; with compareWithEdge it also fetches through normal DNS and reports whether the responses match. The step passes whenever it could measure; whether a match is an exposure Event is decided by the exposure assertions.

{ "id": "origin", "type": "origin.probe", "host": "shop.example.com", "ip": "203.0.113.10", "path": "/", "compareWithEdge": true }

Voice and email steps

voice.call (voice probes): to, optional from, and a script of at least one action: waitForPrompt (containsText, timeoutMs), sendDtmf (digits, characters 0-9*#), expectSpeech (containsText), hangup.

{
  "id": "claims-line",
  "type": "voice.call",
  "to": "+442079460000",
  "script": [
    { "kind": "waitForPrompt", "containsText": "press 1 for claims", "timeoutMs": 20000 },
    { "kind": "sendDtmf", "digits": "1" },
    { "kind": "expectSpeech", "containsText": "claims team" },
    { "kind": "hangup" }
  ]
}

email.send (email probes): to, subject, body, optional expectReplyWithinSeconds. email.expectInbound: mailboxId (a ScoutSentinel probe mailbox), optional subjectContains, withinSeconds.

[
  { "id": "send-enquiry", "type": "email.send", "to": "support@shop.example.com", "subject": "Quote reference Q-1234", "body": "Synthetic enquiry from ScoutSentinel.", "expectReplyWithinSeconds": 900 },
  { "id": "auto-reply", "type": "email.expectInbound", "mailboxId": "probe-mailbox-1", "subjectContains": "We have received", "withinSeconds": 900 }
]

A complete Watch definition

Two probes, one browser and one API, with outcome, behaviour and coverage assertions. This document validates against WatchDefinitionSchema and passes the cross-field checks.

{
  "intent": "UK customers can get a travel insurance quote every five minutes from three edge locations.",
  "channel": "web",
  "probes": [
    {
      "id": "quote",
      "type": "browser",
      "timeoutMs": 60000,
      "captures": { "screenshotOn": "failure_and_boundary", "html": true, "har": false, "trace": true },
      "steps": [
        { "id": "open", "type": "navigate", "url": "https://shop.example.com/quote", "description": "Open quote form" },
        { "id": "accept-cookies", "type": "click", "target": { "role": "button", "name": "Accept all" }, "description": "Accept cookies" },
        { "id": "postcode", "type": "fill", "target": { "label": "Postcode" }, "value": "SW1A 1AA", "description": "Enter postcode" },
        { "id": "cover", "type": "select", "target": { "label": "Cover type" }, "value": "comprehensive", "description": "Choose cover" },
        { "id": "submit", "type": "click", "target": { "role": "button", "name": "Get quote" }, "description": "Get quote" },
        { "id": "wait-price", "type": "waitFor", "target": { "testId": "quote-price" }, "timeoutMs": 15000, "description": "Wait for quote" },
        { "id": "price-visible", "type": "expect", "check": { "kind": "elementVisible", "target": { "testId": "quote-price" } }, "description": "Quoted price shown" },
        { "id": "shot", "type": "capture", "kind": "screenshot", "label": "Quote result" }
      ]
    },
    {
      "id": "quote-api",
      "type": "api",
      "timeoutMs": 15000,
      "captures": { "screenshotOn": "failure_and_boundary", "html": false, "har": true, "trace": false },
      "steps": [
        {
          "id": "post-quote",
          "type": "http.request",
          "method": "POST",
          "url": "https://shop.example.com/api/quotes",
          "headers": { "Content-Type": "application/json", "Authorization": { "secretRef": "QUOTE_API_TOKEN" } },
          "body": "{\"destination\":\"LIS\",\"travellers\":2,\"nights\":14}",
          "expect": { "status": 200, "jsonPath": { "path": "$.status", "equals": "quoted" }, "latencyBelowMs": 3000 },
          "description": "POST quote"
        }
      ]
    }
  ],
  "assertions": [
    { "id": "quote-all", "kind": "outcome", "severity": "critical", "description": "Quote journey completes", "definition": { "type": "allStepsPass" } },
    { "id": "quote-latency", "kind": "outcome", "severity": "medium", "description": "Quote returned within 10 s", "definition": { "type": "latencyBelow", "stepId": "wait-price", "ms": 10000 } },
    { "id": "api-status", "kind": "outcome", "severity": "high", "description": "Quote API responds 200", "definition": { "type": "httpStatus", "stepId": "post-quote", "status": 200 } },
    { "id": "api-body", "kind": "outcome", "severity": "high", "description": "Quote API reports quoted", "definition": { "type": "jsonPath", "stepId": "post-quote", "path": "$.status", "equals": "quoted" } },
    { "id": "quote-behaviour", "kind": "behaviour", "severity": "info", "description": "Quote form behaviour stable", "definition": { "type": "behaviourStable", "tolerance": "normal" } },
    { "id": "two-locations", "kind": "coverage", "severity": "low", "description": "At least two locations report each slot", "definition": { "type": "coverageLocations", "min": 2 } }
  ],
  "schedule": { "kind": "interval", "everySeconds": 300 },
  "materiality": { "consecutiveFailures": 2, "corroboratingLocations": 2, "windowSeconds": 1800, "autoResolveAfterPasses": 2, "behaviourOpensEvent": false },
  "locationPolicy": { "classes": ["edge"], "regions": ["lhr", "fra", "syd"], "minLocations": 2 },
  "notifications": { "channels": [], "onOpen": true, "onResolve": true, "onBehaviourChange": false, "minSeverity": "low" },
  "costClass": "mixed",
  "requiredEvidence": ["screenshot", "html", "trace", "response", "har"]
}

Journey templates

GET /v1/watch-templates lists six starter definitions, and POST /v1/journeys accepts templateId with templateParams.baseUrl (and an optional path) to create a Journey from one. Each template is a complete, valid Watch definition you can then edit as a new version.

templateId Name Channel Cost class Default path Schedule What it proves
public_page_loads Public page loads web browser / every 300 s Homepage renders with its main heading within 5 s; behaviour stable
sign_in_reachable Sign-in form reachable web browser /sign-in every 300 s Email, password and a “Sign in” button are visible
quote_form Get a quote web browser /quote every 300 s Postcode and cover type submitted, a quoted price appears within 10 s
api_health API health api http /api/health every 60 s 200, $.status equals ok, under 2 s
checkout_reachable Checkout reachable web browser /checkout every 300 s A “Pay now” button is visible and the URL stays on /checkout
contact_support Contact support page web browser /contact every 600 s A “Send message” button and “Call us” text are visible

Browser templates capture screenshots on failure and at the boundary, page HTML and a trace, and require screenshot, html and trace evidence. The API template records a HAR and requires response and har. All six use the default materiality policy and edge locations. The api_health template built for https://shop.example.com is:

{
  "intent": "Confirm the API health endpoint at https://shop.example.com/api/health responds 200 with a JSON body reporting status ok.",
  "channel": "api",
  "probes": [
    {
      "id": "health",
      "type": "api",
      "steps": [
        {
          "id": "health-get",
          "type": "http.request",
          "method": "GET",
          "url": "https://shop.example.com/api/health",
          "headers": { "Accept": "application/json" },
          "expect": { "status": 200, "jsonPath": { "path": "$.status", "equals": "ok" }, "latencyBelowMs": 2000 },
          "description": "GET health"
        }
      ],
      "timeoutMs": 15000,
      "captures": { "screenshotOn": "failure_and_boundary", "html": false, "har": true, "trace": false }
    }
  ],
  "assertions": [
    { "id": "health-status", "kind": "outcome", "severity": "critical", "description": "Health returns 200", "definition": { "type": "httpStatus", "stepId": "health-get", "status": 200 } },
    { "id": "health-body", "kind": "outcome", "severity": "high", "description": "Health body reports ok", "definition": { "type": "jsonPath", "stepId": "health-get", "path": "$.status", "equals": "ok" } },
    { "id": "health-latency", "kind": "outcome", "severity": "medium", "description": "Health responds within 2s", "definition": { "type": "latencyBelow", "stepId": "health-get", "ms": 2000 } }
  ],
  "schedule": { "kind": "interval", "everySeconds": 60 },
  "materiality": { "consecutiveFailures": 2, "corroboratingLocations": 1, "windowSeconds": 1800, "autoResolveAfterPasses": 2, "behaviourOpensEvent": false },
  "locationPolicy": { "classes": ["edge"], "minLocations": 1 },
  "notifications": { "channels": [], "onOpen": true, "onResolve": true, "onBehaviourChange": false, "minSeverity": "low" },
  "costClass": "http",
  "requiredEvidence": ["response", "har"]
}

Validation errors

A definition that fails validation is rejected with a 400 problem+json response of type https://api.scoutsentinel.com/problems/validation_failed whose errors[] lists each issue as { path, code, message }, for example probes.0.steps.3.type: step type "dns.resolve" is not allowed in a "browser" probe or assertions.2.definition.stepId: assertion "api-status" references unknown step "post-quotes". The evaluator version and probe version are recorded on every Observation so results stay comparable across upgrades.