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.
- Create a GitHub account if you do not have one at github.com/signup. Free forever for public repos.
- Install Node.js 20 LTS from nodejs.org. Verify:
node --version npm --version
- Install the GitHub CLI (optional, makes pushing the repo easier) from cli.github.com. Then authenticate:
gh auth login
- Install Newman globally for the local-cron approach:
npm install -g newman newman-reporter-htmlextra
Verify with
newman --version. You should see6.x.xor newer.
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:
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.
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'] } },
],
});
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();
});
});
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
*/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.
{
"id": "reqres-env",
"name": "reqres",
"values": [
{ "key": "baseUrl", "value": "https://reqres.in/api", "enabled": true }
],
"_postman_variable_scope": "environment"
}
{
"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.
#!/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
$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=0After 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
- Add Slack notifications on failure. Create a free Slack incoming webhook, store the URL as a repo secret (
SLACK_WEBHOOK), and append a step tomonitor.ymlthat runsif: failure()and POSTs a payload including the run URL. Verify by deliberately breaking the assertion and watching the message land. - Shard the monitor by region. Extend the matrix strategy in the workflow so the job runs in parallel on
ubuntu-latest,windows-latest, andmacos-latest. Confirm the Actions tab shows three parallel jobs per scheduled trigger and that all three upload their own report artifact.