Docs
Dashboard
Examples / MonitoringBeta

Visual uptime check

Two checks of your key pages:

When to use it: You want to know when a key page is down or looks broken, not only when it stops answering.

Usessessions.create and session.goto (the load check), tasks.create with output and schedule, tasks.run, agent.get (the run's steps), screenshot
Needsa plan with task schedules, and a model on the API
Siteyour key pages (PAGES); the default is books.toscrape.com and quotes.toscrape.com. The runner uses its stand-in: one page fine, one that loads but shows an error, one down (503), one slow (6.2 s), and a host that does not exist
Outputoutput/load.json (the load check), output/status.json (the visual check) and output/shots/N.png
LanguagesNode and Python
  1. Load check, no model: each page is opened in a real browser and timed.
    • up;
    • slow: over SLOW_MS;
    • down: an error status, or no page at all, with the reason (the name does not resolve, a certificate problem, the connection refused, no answer).
  2. Visual check, on a schedule: a task opens each page, takes a screenshot, looks at it, and says whether it looks broken (an error message, a blank page, missing images). A page can load with 200 and still be broken.

A screenshot of each page is also saved for the record.

The example runs the task once right away instead of waiting for the schedule, then switches the schedule off unless KEEP_SCHEDULE=1 (a check every 15 minutes costs a model run each time). Each run's result is in the task's run history (tasks.runs) and in the task_run.finished webhook.

The code

The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.

/** * Visual uptime check, in two parts: * 1. A load check, no model: each page opened in a real browser and timed. Up, slow (over SLOW_MS), or down: an error *    status, or no page at all with the reason (the name does not resolve, a certificate problem, refused, no answer). * 2. A task opens each page, takes a screenshot and looks at it, and reports whether it looks broken (an error message, *    a blank page, missing images), on a schedule: a page can load fine and still be broken. * A screenshot of each page is also saved here for the record. * *   PAGES=https://…,https://… npx tsx node/index.ts    (BOXLINE_API_KEY; SLOW_MS; SCHEDULE; KEEP_SCHEDULE=1 to leave it running) * * The example runs the task once right away instead of waiting for the schedule, then switches the schedule off unless * KEEP_SCHEDULE=1 (a check every 15 minutes costs a model run each time). Writes output/status.json, output/load.json * and output/shots/. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline, type TaskCreateParams, type TaskRun } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const pages = (process.env.PAGES ?? "https://books.toscrape.com/,https://quotes.toscrape.com/").split(",").map((p) => p.trim()).filter(Boolean);const cron = process.env.SCHEDULE ?? "*/15 * * * *";const keep = process.env.KEEP_SCHEDULE === "1";const slowMs = Number(process.env.SLOW_MS ?? 5000);const bx = new Boxline();// 1. The load check: each page in a real browser, timed (from here, so it includes the trip to the API).const REASONS: [RegExp, string][] = [  [/ERR_NAME_NOT_RESOLVED|does not resolve/, "the name does not resolve (DNS)"],  [/ERR_CERT_|ERR_SSL_/, "a certificate or TLS problem"],  [/ERR_CONNECTION_REFUSED|refused the connection/, "the server refused the connection"],  [/ERR_CONNECTION_TIMED_OUT|ERR_TIMED_OUT|could not be reached|timeout/i, "the server did not answer"],];interface Load {  url: string;  state: "up" | "slow" | "down";  loadMs: number;  httpStatus: number | null;  reason: string | null;}const load: Load[] = [];const browser = await bx.sessions.create({ timeout: 600, userMetadata: { example: "visual-uptime-check" } });console.log(`Load check · Session: ${browser.id}`);try {  for (const url of pages) {    const t0 = Date.now();    try {      const r = await browser.goto(url);      const loadMs = Date.now() - t0;      const failed = r.status !== null && r.status >= 400;      const slow = !failed && loadMs > slowMs;      load.push({ url, state: failed ? "down" : slow ? "slow" : "up", loadMs, httpStatus: r.status, reason: failed ? `HTTP ${r.status}` : slow ? `${(loadMs / 1000).toFixed(1)} s to load` : null });    } catch (err) {      const message = (err as Error).message;      const why = REASONS.find(([re]) => re.test(message))?.[1];      const code = /ERR_[A-Z_]+/.exec(message)?.[0];      load.push({ url, state: "down", loadMs: Date.now() - t0, httpStatus: null, reason: why ? `${code ?? "no page"}: ${why}` : message.replace(/^page\.goto:\s*/, "").slice(0, 200) });    }  }} finally {  await browser.stop();}for (const l of load) console.log(`${l.state.toUpperCase().padEnd(5)} ${String(l.loadMs).padStart(6)} ms  ${l.url}${l.reason ? `  (${l.reason})` : ""}`);const count = (state: string) => load.filter((l) => l.state === state).length;console.log(`Load check: ${count("up")} up · ${count("slow")} slow · ${count("down")} down\n`);interface Status {  pages: { url: string; httpStatus: number | null; up: boolean; looksBroken: boolean; note: string }[];}const name = `Visual uptime check: ${new URL(pages[0]!).host}`.slice(0, 100);const spec: TaskCreateParams = {  name,  instruction:    "Check each of these pages, one at a time: %pages%. For each one: open it, note the HTTP status, take a screenshot and " +    "look at it. A page is up when it loads with a 2xx status. It looks broken when the screenshot shows an error message, " +    "a blank or half-drawn page, or missing images. Describe what you see in one sentence.",  variables: [{ name: "pages", default: pages.join(", "), description: "The pages to check, comma-separated" }],  output: {    type: "object",    properties: {      pages: {        type: "array",        items: {          type: "object",          properties: {            url: { type: "string" },            httpStatus: { type: ["integer", "null"] },            up: { type: "boolean" },            looksBroken: { type: "boolean" },            note: { type: "string" },          },          required: ["url", "httpStatus", "up", "looksBroken", "note"],          additionalProperties: false,        },      },    },    required: ["pages"],    additionalProperties: false,  },  maxSteps: 20,  schedule: { cron, timezone: "UTC", enabled: true },};const existing = (await bx.tasks.list({ limit: 100 })).data.find((t) => t.name === name);const task = existing ? await bx.tasks.update(existing.id, spec) : await bx.tasks.create(spec);const schedule = { cron: task.schedule!.cron, nextRunAt: task.schedule!.nextRunAt, setAt: new Date().toISOString() };console.log(`Task ${task.id} "${name}": runs "${cron}" (UTC), next at ${schedule.nextRunAt}`);let run: TaskRun<Status>;try {  // Run it now instead of waiting for the schedule.  const started = await bx.tasks.run<Status>(task.id);  console.log(`Run ${started.id} (agent run ${started.runId}) · Session: ${started.sessionId}`);  run = await bx.tasks.waitForRun<Status>(started, { timeoutMs: 8 * 60_000 });} finally {  if (!keep) await bx.tasks.update(task.id, { schedule: { enabled: false } });}if (run.status !== "completed" || !run.result) throw new Error(`the check ${run.status}: ${run.error}`);for (const p of run.result.pages) console.log(`${p.up ? (p.looksBroken ? "BROKEN" : "up    ") : "DOWN  "}  ${p.httpStatus ?? "–"}  ${p.url}  ${p.note}`);// A screenshot of each page for the record (one call each, in a fresh browser).mkdirSync(join(out, "shots"), { recursive: true });const shots: string[] = [];for (const [i, url] of pages.entries()) {  const png = await bx.screenshot(url, { timeoutMs: 30_000 }).catch(() => null);  if (!png) continue;  const file = `shots/${i + 1}.png`;  writeFileSync(join(out, file), png);  shots.push(file);}const agentRun = await bx.agent.get(run.runId!);const screenshotsTaken = agentRun.steps.filter((s) => s.type === "tool" && (s.name === "browser_screenshot" || s.name === "computer")).length;console.log(keep ? `The schedule stays on: next run at ${(await bx.tasks.get(task.id)).schedule?.nextRunAt}` : "The schedule is switched off; KEEP_SCHEDULE=1 leaves it on.");writeFileSync(join(out, "status.json"), JSON.stringify(run.result, null, 2));writeFileSync(join(out, "load.json"), JSON.stringify(load, null, 2));writeFileSync(  join(out, "result.json"),  JSON.stringify(    { pages, slowMs, load, task: { id: task.id, name, schedule, enabledAtEnd: keep }, run: { id: run.id, status: run.status, result: run.result, screenshotsTaken }, shots, agentRuns: [{ id: run.runId }], made: { tasks: [task.id] } },    null,    2,  ),);

Inputs

Environment variables the example reads:

  • PAGES: the pages, comma-separated (default https://books.toscrape.com/,https://quotes.toscrape.com/).
  • SLOW_MS: a page that takes longer than this to load is slow (default 5000).
  • SCHEDULE: the cron (at most every 5 minutes; UTC) (default */15 * * * *).
  • KEEP_SCHEDULE: leave the schedule on at the end (for example 1).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/visual-uptime-check. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.

# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=…   # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts

Then check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.

What its check proves

check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.

npx tsx check.ts output reads what the example wrote and exits with 0 only when the result is right:

Source

github.com/Boxline-dev/examples/visual-uptime-check on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.