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.
Creating in the web UI
Section titled “Creating in the web UI”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.
What a script consists of
Section titled “What a script consists of”A monitor is described by these fields:
appid— the channel for alerts (yours or shared with you with send permission).name— name,1..200characters.startUrl— the starting URL (required,http/httpsonly). 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). Default60, maximum120; 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), maximum86400(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).
Step (BrowserStep)
Section titled “Step (BrowserStep)”Each element of the steps array is an object:
| Field | Purpose |
|---|---|
name | human-readable step name (required, included in the alert on failure) |
description | natural-language description of the step — AI uses it to pick action + selector |
action | action (see table below) |
selector | target selector: CSS, XPath or text=… (see below) |
value | value: URL / text / key / JS expression; supports {{variable}} substitution |
extractName | variable name for extractText and executeScript |
auth | authentication step: executed only when the session is invalid |
timeoutSec | timeout 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.
Actions
Section titled “Actions”Ten actions are supported:
action | What it does | selector needed | value needed |
|---|---|---|---|
navigate | go to a URL (value = URL) | — | yes (URL) |
click | click an element | yes | — |
type | type value into a field | yes | text |
select | select value in a <select> | yes | option value |
press | press the value key (e.g. Enter) | — | key |
waitFor | wait for the selector to appear | yes | — |
assertText | check that the selector’s text contains value | yes | expected text |
extractText | extract the selector’s text into the extractName variable | yes | — |
scroll | scroll the page: value = top | bottom or pixels (300 down, -300 up); with a selector — scroll to the element | optional | — |
executeScript | run 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.
Example steps array
Section titled “Example steps array”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 }]Authenticated scenarios
Section titled “Authenticated scenarios”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
cookiesare 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
navigatestep (host and path are compared). So it’s convenient to start the login with anavigatestep 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.
Capturing cookies (login)
Section titled “Capturing cookies (login)”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.
AI-assisted step selection (build-step)
Section titled “AI-assisted step selection (build-step)”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:
- The service opens
startUrl, applies the providedcookiesand replays the already-confirmedsteps— getting the current state of the page. - 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.
- Candidates are cheaply pre-validated against the HTML (syntax + presence on the page), then all are checked in a single browser run.
- If the action worked —
verified: trueis returned along with a sample value (sampleValue) forextractText/assertText. If not — up to two more attempts that take the error into account, then the last proposal withverified: falseand an explanation innote.
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.
Test run (test-step)
Section titled “Test run (test-step)”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.
”Notify on change” mode
Section titled “”Notify on change” mode”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.
Alerts and statuses
Section titled “Alerts and statuses”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
downand 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 inalertedAt. - Re-arm: a
PUTthat changesstartUrl,steps,intervalSec,timeoutSec,notifyOnChangeornotifyOnChangeVarsclearsalertedAtand schedules a run for “now” — if the script is still failing, a fresh alert arrives. Newcookiesand 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:
alertedAtis 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.
Failure screenshot
Section titled “Failure screenshot”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.
sessionandloginfailures (detected only after the run) have no screenshot.
History and quota
Section titled “History and quota”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.
REST API
Section titled “REST API”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 path | API token access | Purpose |
|---|---|---|
GET /browser-workflow | read | list monitors (without cookies) |
POST /browser-workflow | write | create a monitor |
PUT /browser-workflow/:id | write | update a monitor |
DELETE /browser-workflow/:id | write | delete a monitor (with its screenshots) |
POST /browser-workflow/:id/pause | write | pause (status=paused) |
POST /browser-workflow/:id/resume | write | resume (status=pending) |
POST /browser-workflow/build-step | write | AI selection and verification of one step |
POST /browser-workflow/login | write | run the login and return session cookies |
POST /browser-workflow/test-step | write | test run of the whole script |
GET /browser-shot/<key> | read | failure screenshot by the key from extras |
The same monitor operations are available in the Go/Python SDK and the MCP server.
Example: creating a monitor
Section titled “Example: creating a monitor”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.