Level 5 · Architect · Practice 02

Scheduled Monitoring

Run your tests on a schedule two ways, both free: a GitHub Actions cron workflow that executes Playwright every 30 minutes, and a local cron (macOS / Linux) or Task Scheduler (Windows) job that runs Newman against a Postman collection.

1 Goal

You want to know within half an hour if one of your target sites breaks — even at 2 am on a Sunday, even when no one is deploying. You will wire up two independent monitors: a cloud-hosted GitHub Actions workflow that runs a Playwright smoke test against playwright.dev on a cron trigger, and a locally-scheduled Newman job that exercises the reqres.in REST API every 15 minutes on your own laptop or server. When you finish, both schedules will be proven by green runs in the GitHub Actions tab and a timestamped log file on disk.

2 Tool install

Everything here is free. GitHub Actions gives you unlimited minutes on public repositories and 2,000 free minutes per month on private repos for personal accounts (see GitHub's billing docs). Newman is MIT-licenced, and cron / Task Scheduler ship with the OS.

  1. Create a GitHub account if you do not have one at github.com/signup. Free forever for public repos.
  2. Install Node.js 20 LTS from nodejs.org. Verify:
    node --version
    npm --version
  3. Install the GitHub CLI (optional, makes pushing the repo easier) from cli.github.com. Then authenticate:
    gh auth login
  4. Install Newman globally for the local-cron approach:
    npm install -g newman newman-reporter-htmlextra

    Verify with newman --version. You should see 6.x.x or newer.

Public repo → unlimited free minutes. Keep this practice repo public and you never have to think about GitHub Actions billing. If you later make it private, you get 2,000 minutes per month on the free tier, which is still enough for a 30-minute schedule running ~90 seconds per job.

3 Project setup

Create one repo that holds both approaches side by side:

mkdir architect-02-monitoring
cd architect-02-monitoring
git init
npm init -y
npm install --save-dev @playwright/test
npx playwright install --with-deps chromium

On macOS / Windows you can drop --with-deps (that flag is a Linux-only helper for apt). Final layout:

architect-02-monitoring/ ├── .github/ │ └── workflows/ │ └── monitor.yml ├── tests/ │ └── monitor.spec.ts ├── newman/ │ ├── reqres.postman_collection.json │ ├── reqres.postman_environment.json │ └── run-newman.sh # macOS/Linux │ └── run-newman.ps1 # Windows ├── playwright.config.ts └── package.json

4 Write the workflow, spec, and collection

Approach A — GitHub Actions cron + Playwright

Runs every 30 minutes on GitHub's free runners. Results show in the Actions tab, failures email you automatically.

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  retries: 2,
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    baseURL: 'https://playwright.dev',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});
tests/monitor.spec.ts
import { test, expect } from '@playwright/test';

test.describe('playwright.dev uptime monitor', () => {
  test('home page loads and shows hero heading', async ({ page }) => {
    const response = await page.goto('/');
    expect(response?.status(), 'HTTP status should be 2xx').toBeLessThan(300);
    await expect(page).toHaveTitle(/Playwright/);
    await expect(page.getByRole('heading', { name: /Playwright enables reliable/i })).toBeVisible();
  });

  test('docs navigation still works', async ({ page }) => {
    await page.goto('/');
    await page.getByRole('link', { name: 'Docs', exact: true }).first().click();
    await expect(page).toHaveURL(/\/docs\//);
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
  });
});
.github/workflows/monitor.yml
name: Scheduled Playwright monitor

on:
  schedule:
    # Every 30 minutes. Cron is UTC on GitHub runners.
    - cron: '*/30 * * * *'
  workflow_dispatch:       # lets you trigger the job manually from the UI
  push:
    branches: [main]       # also run on every push so you can verify changes

concurrency:
  group: monitor-${{ github.ref }}
  cancel-in-progress: false

jobs:
  playwright:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci || npm install

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run monitor
        run: npx playwright test --reporter=list,html

      - name: Upload HTML report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ github.run_id }}
          path: playwright-report/
          retention-days: 14
Cron caveat: GitHub will skip scheduled runs if your repo has had no commits for 60 days. Push anything — even a README typo — once a month to keep the schedule alive. Also note that GitHub's scheduler often runs a few minutes late under load; a */30 cron is "roughly every 30 minutes", not "on the dot".

Approach B — local cron / Task Scheduler + Newman

Runs on your own machine (or any box with Node.js). Good for monitoring APIs behind a VPN, or when you do not want to depend on GitHub.

newman/reqres.postman_environment.json
{
  "id": "reqres-env",
  "name": "reqres",
  "values": [
    { "key": "baseUrl", "value": "https://reqres.in/api", "enabled": true }
  ],
  "_postman_variable_scope": "environment"
}
newman/reqres.postman_collection.json
{
  "info": {
    "name": "reqres uptime monitor",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "GET /users returns 200 and a page",
      "request": {
        "method": "GET",
        "url": { "raw": "{{baseUrl}}/users?page=2", "host": ["{{baseUrl}}"], "path": ["users"], "query": [{"key":"page","value":"2"}] }
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('status is 200', function () { pm.response.to.have.status(200); });",
              "pm.test('response time under 2s', function () { pm.expect(pm.response.responseTime).to.be.below(2000); });",
              "const body = pm.response.json();",
              "pm.test('payload has data array', function () { pm.expect(body).to.have.property('data').that.is.an('array'); });",
              "pm.test('page echoed correctly', function () { pm.expect(body.page).to.eql(2); });"
            ],
            "type": "text/javascript"
          }
        }
      ]
    },
    {
      "name": "GET /users/2 returns a single user",
      "request": {
        "method": "GET",
        "url": { "raw": "{{baseUrl}}/users/2", "host": ["{{baseUrl}}"], "path": ["users","2"] }
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('status is 200', function () { pm.response.to.have.status(200); });",
              "const body = pm.response.json();",
              "pm.test('user id is 2', function () { pm.expect(body.data.id).to.eql(2); });",
              "pm.test('user has an email', function () { pm.expect(body.data.email).to.be.a('string'); });"
            ],
            "type": "text/javascript"
          }
        }
      ]
    }
  ]
}

Now write a tiny wrapper script for each OS. The script is what cron / Task Scheduler actually invokes — it captures exit codes and writes a timestamped HTML report.

newman/run-newman.sh
#!/usr/bin/env bash
set -euo pipefail

# Resolve the directory this script lives in so cron works from any CWD.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
REPORT_DIR="$DIR/reports"
mkdir -p "$REPORT_DIR"

newman run "$DIR/reqres.postman_collection.json" \
  --environment "$DIR/reqres.postman_environment.json" \
  --reporters cli,htmlextra,json \
  --reporter-htmlextra-export "$REPORT_DIR/reqres-$TS.html" \
  --reporter-json-export "$REPORT_DIR/reqres-$TS.json" \
  >> "$REPORT_DIR/monitor.log" 2>&1

echo "[$TS] run complete, exit=$?" >> "$REPORT_DIR/monitor.log"

Make it executable, then register the cron entry with crontab -e:

chmod +x newman/run-newman.sh

# Every 15 minutes, log to monitor.log next to the script.
*/15 * * * * /full/path/to/architect-02-monitoring/newman/run-newman.sh
newman/run-newman.ps1
$ErrorActionPreference = 'Stop'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$ts = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
$reportDir = Join-Path $here 'reports'
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null

$args = @(
  'run', (Join-Path $here 'reqres.postman_collection.json'),
  '--environment', (Join-Path $here 'reqres.postman_environment.json'),
  '--reporters', 'cli,htmlextra,json',
  '--reporter-htmlextra-export', (Join-Path $reportDir "reqres-$ts.html"),
  '--reporter-json-export', (Join-Path $reportDir "reqres-$ts.json")
)

$log = Join-Path $reportDir 'monitor.log'
& newman @args *>> $log
"[$ts] run complete, exit=$LASTEXITCODE" | Add-Content $log

Register the Task Scheduler job from an elevated PowerShell:

$action  = New-ScheduledTaskAction `
  -Execute 'powershell.exe' `
  -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\path\to\architect-02-monitoring\newman\run-newman.ps1"'

$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
  -RepetitionInterval (New-TimeSpan -Minutes 15)

Register-ScheduledTask -TaskName 'Resync Newman Monitor' `
  -Action $action -Trigger $trigger -RunLevel Highest

5 Run & verify

Approach A: push the repo to a public GitHub repo, then trigger the workflow manually so you do not have to wait 30 minutes:

git add .
git commit -m "feat: scheduled monitor"
gh repo create architect-02-monitoring --public --source=. --push
gh workflow run monitor.yml

Watch the run:

gh run list --workflow=monitor.yml
gh run watch

Expected final lines:

✓ monitor-chromium › monitor.spec.ts:4:7 › playwright.dev uptime monitor › home page loads and shows hero heading (2.1s)
✓ monitor-chromium › monitor.spec.ts:12:7 › playwright.dev uptime monitor › docs navigation still works (1.8s)

  2 passed (4.2s)

completed   success   monitor.yml   Scheduled Playwright monitor

Open the repo on github.com and confirm the Actions tab shows a queued run for your cron trigger within the next 30 minutes (look for the clock icon and "Scheduled" label on the run).

Approach B: run the wrapper script once by hand to confirm it works before cron takes over:

# macOS / Linux
./newman/run-newman.sh
cat newman/reports/monitor.log
ls newman/reports/

# Windows
powershell -ExecutionPolicy Bypass -File .\newman\run-newman.ps1
type newman\reports\monitor.log
dir newman\reports\

Expected tail of monitor.log:

reqres uptime monitor

→ GET /users returns 200 and a page
  GET https://reqres.in/api/users?page=2 [200 OK, 812B, 184ms]
  ✓  status is 200
  ✓  response time under 2s
  ✓  payload has data array
  ✓  page echoed correctly

→ GET /users/2 returns a single user
  GET https://reqres.in/api/users/2 [200 OK, 274B, 112ms]
  ✓  status is 200
  ✓  user id is 2
  ✓  user has an email

                           executed    failed
             iterations          1         0
               requests          2         0
           test-scripts          2         0
     prerequest-scripts          0         0
             assertions          7         0
[20260422T101500Z] run complete, exit=0

After 15 minutes, a second HTML file should appear in newman/reports/. That proves the schedule fired automatically.

6 Troubleshooting

GitHub schedule never fires even though the workflow is green

Three common causes: (1) the default branch is not main and GitHub only honours cron on the default branch — rename or push the workflow to the correct branch; (2) the repo has had zero commits for 60+ days — push anything to reset the timer; (3) the cron syntax is wrong — GitHub requires 5 fields (no seconds), and * /30 * * * * with a stray space is invalid. Validate on crontab.guru.

cron job runs but the log stays empty

On macOS and most Linux distros cron inherits a minimal PATH that does not include /usr/local/bin where newman lives. Either put the absolute path to newman in the script (/usr/local/bin/newman), or add PATH=/usr/local/bin:/usr/bin:/bin at the top of your crontab. Confirm by running env | grep PATH from a one-shot cron line first.

Task Scheduler task says "last result 0x1" and no reports appear

PowerShell could not launch because of the execution policy. The action must include -ExecutionPolicy Bypass explicitly (as shown in the install block) — the machine policy is not enough. Also check that "Run whether user is logged on or not" is enabled if you want it to fire after you log out, and that the user has write permissions to the reports/ folder.

Actions workflow fails with "No tests found"

Playwright can't locate specs. Your testDir in the config probably points somewhere the CI checkout doesn't have — e.g. /Users/you/.... Keep it relative (./tests) and verify locally with npx playwright test --list before pushing.

7 Challenge

Level up

  1. Add Slack notifications on failure. Create a free Slack incoming webhook, store the URL as a repo secret (SLACK_WEBHOOK), and append a step to monitor.yml that runs if: failure() and POSTs a payload including the run URL. Verify by deliberately breaking the assertion and watching the message land.
  2. Shard the monitor by region. Extend the matrix strategy in the workflow so the job runs in parallel on ubuntu-latest, windows-latest, and macos-latest. Confirm the Actions tab shows three parallel jobs per scheduled trigger and that all three upload their own report artifact.