Docs
Dashboard
Examples / MonitoringBeta

Changelog watcher

A task reads the newest released entry of a product's changelog into JSON (version, date, title, the changes and a one-sentence summary) every morning, and the example says whether it is new since the last time it looked. A new entry is posted to your team's chat.

When to use it: You want to know when a product you rely on ships a new release.

Usestasks.create with output and schedule, tasks.run and waitForRun (wait_for_run)
Needsa plan with task schedules, and a model on the API
Sitethe changelog to watch (CHANGELOG_URL, required). The runner uses its stand-in: an unreleased section first, then released entries
Outputoutput/latest.json and output/last-seen.json; a chat message when the entry is new
LanguagesNode and Python

The example runs the task once right away, then switches the schedule off unless KEEP_SCHEDULE=1. With the schedule on, compare in your task_run.finished webhook receiver (see "Watch a price").

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.

/** * Changelog watcher: a task reads the newest released entry of a product's changelog into JSON (version, date, title, * changes, a one-sentence summary) every morning, and the example says whether it is new since the last time it looked. * A new entry is posted to your team's chat (a Slack-style incoming webhook) when SLACK_WEBHOOK_URL is set. * *   CHANGELOG_URL=https://… npx tsx node/index.ts     (BOXLINE_API_KEY; SCHEDULE; KEEP_SCHEDULE=1 to leave it running; *                                                      SLACK_WEBHOOK_URL optional) * * The example runs the task once right away instead of waiting for the schedule, then switches the schedule off unless * KEEP_SCHEDULE=1. The last version seen is kept in output/last-seen.json. Writes output/latest.json. */import { existsSync, mkdirSync, readFileSync, 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 changelog = process.env.CHANGELOG_URL;if (!changelog) throw new Error("Set CHANGELOG_URL to the changelog or release notes page to watch");const cron = process.env.SCHEDULE ?? "0 8 * * *"; // every day at 08:00 UTCconst keep = process.env.KEEP_SCHEDULE === "1";const chatHook = process.env.SLACK_WEBHOOK_URL;const bx = new Boxline();mkdirSync(out, { recursive: true });interface Entry {  version: string;  date: string;  title: string;  changes: string[];  summary: string;}const name = `Changelog watcher: ${new URL(changelog).host}`.slice(0, 100);const spec: TaskCreateParams = {  name,  instruction:    "Open %changelog% and find the newest RELEASED entry of the changelog (skip anything marked unreleased or upcoming). " +    "Give its version, its release date as YYYY-MM-DD, its title, the list of changes exactly as written, and a " +    "one-sentence summary of what is new.",  variables: [{ name: "changelog", default: changelog, description: "The changelog page" }],  output: {    type: "object",    properties: {      version: { type: "string", minLength: 1 },      date: { type: "string", description: "YYYY-MM-DD" },      title: { type: "string" },      changes: { type: "array", items: { type: "string" }, minItems: 1 },      summary: { type: "string" },    },    required: ["version", "date", "title", "changes", "summary"],    additionalProperties: false,  },  maxSteps: 10,  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<Entry>;try {  const started = await bx.tasks.run<Entry>(task.id);  console.log(`Run ${started.id} (agent run ${started.runId}) · Session: ${started.sessionId}`);  run = await bx.tasks.waitForRun<Entry>(started, { timeoutMs: 5 * 60_000 });} finally {  if (!keep) await bx.tasks.update(task.id, { schedule: { enabled: false } });}if (run.status !== "completed" || !run.result) throw new Error(`the run ${run.status}: ${run.error}`);const entry = run.result;// New since last time? (A scheduled run would do this in your task_run.finished webhook receiver.)const stateFile = join(out, "last-seen.json");const last = existsSync(stateFile) ? (JSON.parse(readFileSync(stateFile, "utf8")) as { version: string }) : null;const isNew = !last || last.version !== entry.version;writeFileSync(stateFile, JSON.stringify({ version: entry.version, date: entry.date, seenAt: new Date().toISOString() }, null, 2));console.log(`${isNew ? "NEW" : "no change"}: ${entry.version} (${entry.date}) ${entry.title}: ${entry.summary}`);for (const c of entry.changes) console.log(`  - ${c}`);// A new entry goes to the team's chat, written like a changelog post.let posted = false;if (isNew && chatHook) {  const text = [`New release at ${changelog}: *${entry.version}* (${entry.date}) ${entry.title}`, "", entry.summary, "", ...entry.changes.map((c) => `• ${c}`)].join("\n");  const res = await fetch(chatHook, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }) });  posted = res.ok;  console.log(res.ok ? "Posted the new entry to the chat webhook." : `The chat webhook answered ${res.status}.`);}writeFileSync(join(out, "latest.json"), JSON.stringify(entry, null, 2));writeFileSync(  join(out, "result.json"),  JSON.stringify({ changelog, task: { id: task.id, name, schedule, enabledAtEnd: keep }, run: { id: run.id, status: run.status }, entry, isNew, previous: last?.version ?? null, posted, agentRuns: [{ id: run.runId }], made: { tasks: [task.id] } }, null, 2),);

Inputs

Environment variables the example reads:

  • CHANGELOG_URL: the changelog or release notes page (required).
  • SCHEDULE: the cron (UTC) (default 0 8 * * *).
  • KEEP_SCHEDULE: leave the schedule on at the end (for example 1).
  • SLACK_WEBHOOK_URL: a Slack-style incoming webhook ({"text": …}) that gets new entries (optional).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/changelog-watcher. 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.

The schedule's next run is 08:00 UTC, and on the stand-in the entry is 2.4.0 (2026-09-24) with its 3 changes (the unreleased section skipped), reported as new on the first look and posted once to the stand-in's chat webhook with its version and every change.

Source

github.com/Boxline-dev/examples/changelog-watcher 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.