Skip to content

Browser workflow monitors (browser automation)

A regular HTTP monitor and a content monitor only see the HTML the server returned in its first response. For modern SPAs, JavaScript-heavy pages and anything hidden behind authentication, that is not enough: the text you need appears only after logging in, a few clicks and rendering by the frontend.

A browser workflow monitor runs a given sequence of actions in a real headless Chromium — like a small Selenium script — and checks that the script still reaches the end. If any step fails (an element disappeared, text didn’t match, the page didn’t load) — you get an alert with a screenshot of the page at the moment of failure. The monitor can also extract text from the page and notify you when it changes.

app.notifly.ru → Monitors → Create → “What do you want to monitor?” → “Multi-step scenario” → “Real browser with clicks”. Existing monitors live on the Browser tab.

In the dialog each step is described in words — the “Suggest action” button runs AI step selection, which finds the action and selector itself and immediately shows a screenshot of the result. Login steps are marked with the “Auth” checkbox. “Test run” executes the whole script and captures the session cookies — they are encrypted when the monitor is saved.

A monitor is described by these fields:

  • appid — the channel for alerts (yours or shared with you with send permission).
  • name — name, 1..200 characters.
  • startUrl — the starting URL (required, http/https only). Every run starts from it.
  • steps — a JSON array of steps, from 1 to 20. Executed strictly sequentially.
  • cookies — optional: a session for authenticated scenarios (see below). Stored encrypted and never returned.
  • timeoutSec — the budget for one run of the script (not per step). Default 60, maximum 120; the browser service keeps a run within 30–110 s. A limit for an individual step is set in the step itself.
  • intervalSec — how often to run the script. Minimum 300 (5 minutes), maximum 86400 (a day).
  • alertMessage (required, up to 2000 characters), alertTitle, alertMessageMarkdown, alertPriority (0…10) — what to send on failure and with what priority.
  • notifyOnChange / notifyOnChangeVars — the “notify on change” mode for extracted values (see below).

Each element of the steps array is an object:

FieldPurpose
namehuman-readable step name (required, included in the alert on failure)
descriptionnatural-language description of the step — AI uses it to pick action + selector
actionaction (see table below)
selectortarget selector: CSS, XPath or text=… (see below)
valuevalue: URL / text / key / JS expression; supports {{variable}} substitution
extractNamevariable name for extractText and executeScript
authauthentication step: executed only when the session is invalid
timeoutSectimeout of this step, 1..120 s; 0 or absent — the action’s built-in waits

Selectors. CSS by default. XPath starts with //, (// or xpath=; text=substring finds an element by its visible text. If the main selector stops matching, the browser tries relaxed variants — this reduces false alarms after cosmetic markup changes. {{variable}} substitution works in both value and selector.

Step timeout is the step’s own deadline; the overall run budget remains the upper bound. It caps the step but does not extend the built-in waits: for an element to appear (about 8–14 s per attempt, with one retry if the budget allows) and for the page to load on navigate (25 s). For executeScript it replaces the default 20 s limit.

Ten actions are supported:

actionWhat it doesselector neededvalue needed
navigatego to a URL (value = URL)—yes (URL)
clickclick an elementyes—
typetype value into a fieldyestext
selectselect value in a <select>yesoption value
presspress the value key (e.g. Enter)—key
waitForwait for the selector to appearyes—
assertTextcheck that the selector’s text contains valueyesexpected text
extractTextextract the selector’s text into the extractName variableyes—
scrollscroll the page: value = top | bottom or pixels (300 down, -300 up); with a selector — scroll to the elementoptional—
executeScriptrun the JS expression value on the page; the result (as a string) goes to the extractName variable, if set—JS expression

Steps click, type, select, waitFor, assertText, extractText require a non-empty selector — without it creating/updating the monitor returns 400. navigate requires value (URL), extractText requires extractName, and executeScript requires a value of up to 4000 characters.

executeScript runs inside the page, promises are awaited, and null/undefined become an empty string. The step fails if the expression throws. Network requests from the script go through the same browser and the same SSRF filter.

We check that after login the balance is visible in the dashboard, and remember it:

[
{"name": "Open dashboard", "action": "waitFor", "selector": "#dashboard"},
{"name": "Expand account", "action": "click", "selector": "button.account-toggle"},
{"name": "Wait for balance", "action": "waitFor", "selector": ".balance-value"},
{
"name": "Check currency",
"action": "assertText",
"selector": ".balance-value",
"value": "₽"
},
{
"name": "Remember balance",
"action": "extractText",
"selector": ".balance-value",
"extractName": "balance"
},
{
"name": "Order count",
"action": "executeScript",
"value": "document.querySelectorAll('.order-row').length",
"extractName": "orders",
"timeoutSec": 10
}
]

To monitor pages behind a login you need a valid session. The simplest way is to describe the login right in the script and mark those steps with auth: true:

  • Saved cookies are applied at the start of every run. After a successful run the monitor re-encrypts and saves the fresh cookies — the session extends itself; on failure the saved session is left untouched.
  • If the script has auth steps and a session is saved, only the checking steps run first. If they pass and the browser didn’t end up on the login page — done. Otherwise the session is considered expired: the whole script runs, login included, and the new session is saved.
  • The “login page” is the URL of the first auth navigate step (host and path are compared). So it’s convenient to start the login with a navigate step to the login page.
  • If the checking steps use a variable extracted only in an auth step, the short run is impossible and the script always runs in full. The same applies when there is no saved session.

If the script ends on the login page, the run counts as failed: step session — the session expired, login — logging in again didn’t help.

You can also obtain a session separately: POST /browser-workflow/login executes the login steps in a headless browser and returns the resulting cookies in plaintext — you then pass them as a string in the cookies field when creating/updating the monitor, where they are encrypted before being stored.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow/login" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"startUrl": "https://app.example.com/login",
"steps": [
{"action": "type", "selector": "#email", "value": "me@example.com"},
{"action": "type", "selector": "#password", "value": "secret"},
{"action": "click", "selector": "button[type=submit]"}
],
"timeoutSec": 60
}'

Response:

{
"cookies": [
/* array of cookies: name, value, domain, path, secure, httpOnly, expires */
],
"count": 7,
"url": "https://app.example.com/dashboard",
"screenshot": "/9j/4AAQ..."
}

screenshot is a base64 JPEG of the final page (best-effort, may be missing). If the browser couldn’t execute the steps, the endpoint responds with 502 and the reason.

Picking CSS selectors by hand is tedious and brittle. That’s why there is the POST /browser-workflow/build-step endpoint: you describe the step in words (description), and the AI picks the action and a stable selector itself, then immediately verifies the candidate in a real browser and returns the result.

How it works under the hood:

  1. The service opens startUrl, applies the provided cookies and replays the already-confirmed steps — getting the current state of the page.
  2. Based on the description, the page text and the list of interactive elements, the LLM proposes an action and up to three ranked candidate selectors.
  3. Candidates are cheaply pre-validated against the HTML (syntax + presence on the page), then all are checked in a single browser run.
  4. If the action worked — verified: true is returned along with a sample value (sampleValue) for extractText/assertText. If not — up to two more attempts that take the error into account, then the last proposal with verified: false and an explanation in note.

Request body:

{
"startUrl": "https://app.example.com/login",
"steps": [
/* already confirmed steps: action, selector, value, extractName, timeoutSec */
],
"description": "click the \"Sign in\" button",
"cookies": [
/* optional: session */
],
"sessionId": "",
"timeoutSec": 60
}

sessionId is an optional warm browser session key: pass the same value throughout a wizard session, and neighbouring requests can reuse the open browser (best-effort).

Response:

{
"action": "click",
"selector": "button[type=submit]",
"value": "",
"extractName": "",
"label": "Sign in",
"reason": "...",
"sampleValue": "",
"verified": true,
"availableVars": ["balance"],
"cookies": [
/* resulting session cookies */
],
"screenshot": "/9j/4AAQ..."
}

If the model considered the step slow, the response also includes a recommended step timeoutSec. screenshot is a base64 JPEG of the page after executing the candidate (best-effort).

The description field is 1 to 500 characters. availableVars lists variables already extracted by previous steps (extractText and executeScript with extractName) and available for {{var}} substitution at this step. It’s worth accumulating the returned cookies on the client and passing them to the next build-step, so authenticated steps don’t have to be replayed from scratch.

Before saving, the whole script can be run via POST /browser-workflow/test-step — this is the “Check” button. The service executes all the provided steps in order (without the short-run logic) and reports whether it reached the end.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow/test-step" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"startUrl": "https://app.example.com/dashboard",
"cookies": [ /* saved session */ ],
"steps": [ /* the whole script */ ],
"timeoutSec": 60
}'

Response:

{
"ok": true,
"failedStep": -1,
"url": "https://app.example.com/dashboard",
"screenshot": "/9j/4AAQ...",
"cookies": [
/* resulting cookies of the whole session */
],
"count": 7,
"vars": {"balance": "12 340 ₽", "orders": "3"}
}

If the script failed, ok: false, failedStep gives the index of the broken step (zero-based; -1 — a failure before the first step, e.g. the start page didn’t open) and error gives the reason. On success vars contains all extracted values — handy for checking right away that exactly what you need is being captured. The web UI saves the resulting cookies as the monitor’s session.

With notifyOnChange enabled, the monitor works not only as “failed / didn’t fail” but also as a value tracker: after each successful run it compares the extracted variables (extractText, executeScript with extractName) with the previous successful run and sends a notification when any of them has changed.

  • notifyOnChangeVars — a JSON array of variable names to track (as a string, e.g. "[\"balance\"]"). Empty = track all extracted variables.
  • The notification text contains lines like balance: old → new; the default title is “Browser workflow «‹name›»: данные изменились” (“data changed”).
  • The first observation of a value doesn’t notify; when a run fails, the baseline values are kept.

This way you can, for example, catch changes to a product price, order status or stock level on a page that only renders in a browser.

A monitor is created with the pending status and its first check scheduled “now”. After that it switches to up / down based on run results. On failure the monitor card shows lastError and lastFailedStep — the last error text and the label of the broken step (#3 Wait for balance, or session / login).

The alert has the alertTitle title (if empty — “Browser workflow «‹name›» failed”) and the alertMessage text followed by Шаг: … / Ошибка: … (“Step” / “Error”) lines.

  • An alert is sent on the transition to down and when the script starts failing at a different step. While the same step keeps failing there are no repeats, even if the error text changes: the fact that an alert was sent is stored in alertedAt.
  • Re-arm: a PUT that changes startUrl, steps, intervalSec, timeoutSec, notifyOnChange or notifyOnChangeVars clears alertedAt and schedules a run for “now” — if the script is still failing, a fresh alert arrives. New cookies and text edits restart nothing.
  • There is no separate recovery notification: the monitor simply returns to up.
  • Failures of the browser service itself (the browser didn’t start, etc.) don’t count as the site failing: no alert, the status doesn’t change, and the monitor is re-checked later.
  • During a maintenance window runs continue and are recorded in history, but the failure alert is deferred: alertedAt is not set, and if the script is still failing after the window, the very first check sends the alert. Change notifications are not sent during the window and the baseline doesn’t move — after the window the comparison is against the pre-maintenance values.

An alert is a regular channel message: its forwarding to external channels follows quiet hours. If the monitor is added to a status page, up↔down transitions open and close an automatic incident there.

POST /browser-workflow/:id/pause sets the paused status: the script doesn’t run, and the result of an already started run is discarded. POST /browser-workflow/:id/resume returns it to pending and schedules a run for “now”. Both respond with 200 and no body.

When a step fails, the browser takes a JPEG of the page (up to 1280 px wide) and puts it into a private object storage. The frame key goes into the alert message extras under notifly::screenshot:

{"notifly::screenshot": "browser-shots/browser-workflow/42/1759000000000.jpg"}

In the message feed of the web UI and the Android app a preview is shown under the alert text; click it for full size. Via the API the image is served by GET /browser-shot/<key> (image/jpeg) — only to the monitor’s owner; an API token scoped to channels sees only frames of monitors in its channels. Someone else’s, deleted or evicted frame returns 404.

  • Only a frame attached to an alert is kept. Screenshots of runs that produced no message (the same step keeps failing) are deleted immediately.
  • Frames take up the plan’s storage (storageMB: Free 50 MB, Pro 1 GB, Business 10 GB): when it overflows, the oldest frames are evicted and older messages lose their preview.
  • Deleting a monitor deletes its screenshots too.
  • session and login failures (detected only after the run) have no screenshot.

The monitor’s History tab shows the runs and a chart of failures per step; via the API — GET /monitor-history/browser_workflow_monitor/:id. The event metadata shows whether a re-login was needed (reloggedIn).

Each run of the script is one event of the dailyEvents daily quota, regardless of the number of steps. When the quota is exhausted, runs are deferred until 00:05 Moscow time the next day — see Quotas and plans.

Authentication is the X-Notifly-Key header with an API token (M…): reading works with any token, changes require a token with write access (otherwise 403). A token scoped to specific channels sees and changes only the monitors of those channels.

Method and pathAPI token accessPurpose
GET /browser-workflowreadlist monitors (without cookies)
POST /browser-workflowwritecreate a monitor
PUT /browser-workflow/:idwriteupdate a monitor
DELETE /browser-workflow/:idwritedelete a monitor (with its screenshots)
POST /browser-workflow/:id/pausewritepause (status=paused)
POST /browser-workflow/:id/resumewriteresume (status=pending)
POST /browser-workflow/build-stepwriteAI selection and verification of one step
POST /browser-workflow/loginwriterun the login and return session cookies
POST /browser-workflow/test-stepwritetest run of the whole script
GET /browser-shot/<key>readfailure screenshot by the key from extras

The same monitor operations are available in the Go/Python SDK and the MCP server.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"appid": 12,
"name": "Dashboard balance",
"startUrl": "https://app.example.com/dashboard",
"steps": "[{\"name\":\"Wait for balance\",\"action\":\"waitFor\",\"selector\":\".balance-value\"},{\"name\":\"Remember balance\",\"action\":\"extractText\",\"selector\":\".balance-value\",\"extractName\":\"balance\"}]",
"cookies": "[ /* plaintext cookies from /login */ ]",
"intervalSec": 900,
"timeoutSec": 60,
"notifyOnChange": true,
"alertMessage": "The \"Dashboard balance\" script broke"
}'

See also: Active monitors · Content monitor · Workflow monitor (HTTP) · Full API.