Skip to content

Monitor a web page

A page monitor is a durable, GET-only watch an agent starts on a URL. The platform re-fetches the page on a fixed interval and evaluates your conditions on every tick — text or CSS-selector rules on HTML, path and comparator rules on JSON APIs, plus change detection. When a condition trips, the watch delivers an event back into Hadron that mints an agent run, so your agent wakes up and acts on the change.

Reach for it when you want "notify my agent when this page changes" or "wake my agent when this API field crosses a threshold" — a price drops below a number, a status flips to open, a phrase appears on a page.

A monitor only ever reads. It issues a GET, never submits a form or writes anything, and the URL is frozen when the watch starts.

Prerequisites

Step 1: Give the agent the polling tools

Monitoring is exposed as three run tools, all governed by the single policy action tool.web_poll:

Tool Does
web_poll_start Begin a watch. Returns a jobId and an expiresAt.
web_poll_cancel Stop a watch by jobId.
web_poll_list List the org's active watches — status and counters only, never a secret.

Two separate things must both be true, or the agent silently won't monitor anything:

1. Declare the tools on the node that calls them. data.tools is per node — a run only has the tools declared on the node making the call. In a single-node watcher flow that's the entry node; in a multi-step flow, put the tools on whichever node calls web_poll_start. Miss this and the agent never calls the tool — the run completes and the model answers in prose, with no error.

hadron node update acme.com::ops::flow:watcher \
  --data-merge '{"tools":["web_poll_start","web_poll_cancel","web_poll_list"]}'

2. Make sure the policy chain permits tool.web_poll. Each call is authorized against the run's policy chain — org ∧ user ∧ app ∧ trigger, where every present layer must allow the action. If none of those layers pins an allow-list, the tool is permitted by default. If you do pin the trigger (the recommended posture), the allow-list must include the run-time verbs the flow needs and the tool — otherwise the call fails ACTIVATION_DENIED (or the run won't launch at all):

hadron webhook create --app acme.com:ops --name start-watch \
  --entry acme.com::ops::flow:watcher \
  --policy '{"allow":["run.execute","llm.invoke","tool.web_poll"]}'

Then fire that webhook once to start the watch. (A manual hadron run trigger also works when a higher layer already permits tool.web_poll; the --policy allow-list lives on schedule/webhook triggers, not on node update.)

The tool must be declared and permitted

Permitting tool.web_poll does not add the tool — it only allows it. Listing it in data.tools while a pinned policy omits it lets the model call a tool the chain then denies. You need both. (And remember a node that declares tools can't also carry an extractionSpec — they're mutually exclusive per node.)

Step 2: Start a watch

The agent calls web_poll_start. Its arguments:

Argument Required Meaning
url The absolute http(s) URL to watch. Frozen at creation — to watch a different URL, start a new monitor.
conditions 1–10 condition objects (see Step 3).
intervalSeconds How often to re-fetch. The service enforces a floor; a value below it is rejected.
ttlSeconds How long the watch lives. Mandatory and capped — every watch expires, and you get a poll.expired event when it does, so a watch never silently vanishes.
contentKind auto (default), html, or json — how to parse the response.
mode any (default) or all — whether one matching condition or every condition must match to fire.
firePolicy once (default), every_change, or cooldown (see Step 4).
cooldownSeconds Suppression window when firePolicy is cooldown.
headers Non-secret request headers only. Never put authorization or cookie material here.
credentialsNodeUrn A server-encrypted credential for authenticated pages — see the caveat below.

A minimal watch — fire once when a phrase appears on a page, and expire after 15 minutes:

{
  "url": "https://example.com/",
  "conditions": [
    { "id": "phrase", "type": "text_contains", "value": "Example Domain" }
  ],
  "intervalSeconds": 60,
  "ttlSeconds": 900,
  "firePolicy": "once"
}

web_poll_start returns the jobId (use it to cancel) and the expiresAt timestamp.

Step 3: Write conditions

Each condition is an object with a caller-chosen id (so you can tell which one fired). A watch holds up to 10. The top-level mode decides whether any (default) or all of them must match.

On HTML pages:

Type Fields Fires when
selector_exists {id, selector} A CSS selector matches at least one element.
selector_text {id, selector, op, value?} The first match's text satisfies op: contains, equals, regex, or changed.
text_contains {id, value} The page's visible text contains value.
content_changed {id} The page's normalized content differs from the previous observation.

On JSON APIs:

Type Fields Fires when
json_path {id, path, op, value?} The value at a dot/bracket path (e.g. items[0].price) satisfies op: eq, ne, gt, gte, lt, lte, contains, exists, or changed.

A JSON field watch — wake when a status endpoint reports open:

{
  "url": "https://api.example.com/v1/status",
  "contentKind": "json",
  "conditions": [
    { "id": "status", "type": "json_path", "path": "data.status", "op": "eq", "value": "open" }
  ],
  "intervalSeconds": 120,
  "ttlSeconds": 3600
}

First tick: change vs. absolute conditions

A change condition (content_changed, or any changed op) records a baseline on the first tick and does not fire from it — it fires on a later change, including a watched element or value appearing or disappearing.

An absolute condition (e.g. json_path … lt 100) may fire on the very first tick if it's already true. "Notify me when the price is under 100" firing immediately because it already is, is correct.

Step 4: Choose a fire policy and TTL

firePolicy controls what happens after a condition trips:

  • once (default) — fire the first time it triggers, then the watch is done.
  • every_change — re-fire only when the observed content actually changes from the last notification. A condition that merely stays true does not re-fire every tick.
  • cooldown — after firing, suppress re-fires for cooldownSeconds.

Regardless of fire policy, ttlSeconds is the backstop: when it elapses the watch ends and emits a poll.expired event. Set it deliberately — there is a cap, and a long watch still expires.

Step 5: Handle the run that wakes

When a watch fires, it delivers an event into Hadron, which mints a new agent run — trigger kind INTEGRATION, parented to the run that started the watch. The event carries which condition matched and a capped excerpt of the page. Three event kinds reach your agent:

  • poll.triggered — a condition matched.
  • poll.expired — the TTL elapsed.
  • poll.failed — the watch could not fetch or evaluate the page.

The page excerpt is untrusted external content

The excerpt is whatever was on the page — it may be attacker-authored. Hadron wraps it in its untrusted-content framing (a preamble telling the model to treat it as data, never as instructions), but design the woken agent accordingly: use the excerpt as a signal to act, and never let page text redirect what the agent does. This is the same posture as any tool that pulls in outside content.

Step 6: Manage watches

  • List the org's active watches — web_poll_list returns each jobId with its status and counters (checks, triggers, consecutive failures). It never returns credentials or page content.
  • Cancel a watch — web_poll_cancel with its jobId. Cancelling an already-cancelled job is a no-op, not an error.

A watch you never cancel still ends on its own when ttlSeconds elapses.

Monitoring authenticated pages

web_poll_start accepts a credentialsNodeUrn that points at a Hadron node holding a server-encrypted credential (a bearer token, basic auth, or a custom header). The secret is never shown to the model, and it's bound to a URL prefix so it can only be sent to the origin it belongs to.

The injection side is shipped, but there is not yet a builder-facing way to create such a credential node — that write surface is still to come. Until it lands, use page monitors on public or unauthenticated URLs. Authenticated monitoring will get its own how-to once you can mint the credential node from the portal or CLI.