A weekly check of ten vendor pricing pages uses about 150 Make credits a month, which fits inside Make’s free 1,000. The same job written as a 50-line Node script and run by GitHub Actions in a private repo uses around five minutes of runner time a month. At GitHub’s Linux rate that is about three cents, taken out of the $12 of compute a free GitHub account includes.
Both cost nothing, so the choice comes down to who fixes the job when something changes, where you look when it goes quiet, and how quickly you can edit it. Coding agents like Claude Code, Codex, and Cursor now write a script like that from a paragraph of plain English, which moves a lot of small automations out of visual tools. Some should stay there.
The short answer
- Scheduled jobs that fetch something, compare it, and message you: ask a coding agent for a script and run it on a GitHub Actions schedule in a private repo. Every run gets a log and every change lives in Git.
- Cloudflare Workers with a Cron Trigger suit very light jobs if your site already runs on Cloudflare. The Free plan allows 10 ms of CPU per run and 5 cron triggers per account, so plan around that.
- Keep Make or n8n when a workflow signs in to several apps through OAuth (Gmail, Google Sheets, Notion), when a non-developer has to change it, or when you edit it every week.
- Give every scheduled job a failure alert and a monthly “still running” message, whichever route you pick.
Three routes for the same job
All three run code on a timer and send you a message. They differ in who writes the steps, who maintains the connectors, and which free-plan limit you hit first.
Limits and prices below come from the vendors’ own docs and pricing pages, checked November 9, 2026.
| Make or n8n | AI-written script on GitHub Actions | AI-written Cloudflare Worker with a Cron Trigger | |
|---|---|---|---|
| Who builds it | You, on a visual canvas | A coding agent writes it, you review | A coding agent writes it, you review |
| Cheapest way in | Make Free (1,000 credits/month); n8n Community edition on your own server | Free in public repos; GitHub Free includes $12/month of compute for private repos | Workers Free; Workers Paid has a $5/month minimum |
| Shortest schedule | Every 15 minutes on Make Free | Every 5 minutes, and runs can be delayed | Standard cron syntax, in UTC |
| Limit that bites first | Credits (Make) or executions (n8n Cloud) | Delays at busy times; public-repo schedules switch off after 60 days without activity | 10 ms of CPU and 50 outgoing requests per run on Free |
| Where it keeps state | Built-in data stores and tables | Files committed back to the repo | Workers KV (Free: 1,000 writes a day) |
| Run history | Execution history in the app | A log for every run in the Actions tab | Cron Events (last 100 runs) and Workers Logs |
| Alert on failure | Make error handlers; n8n Error Trigger workflow | GitHub emails you about failed runs | You write it into the code |
| When an app’s API changes | The vendor updates the connector | You and the agent update the script | You and the agent update the script |
What the weekly pricing monitor costs on each route
The example job runs once a week, fetches ten pages, compares each with last week’s copy, and posts to Slack if anything changed or failed. Assume about 4.3 runs a month.
- Make Free: about 30 to 35 credits a run (three actions per page plus a few fixed steps), so roughly 150 credits a month. Free. We price this workflow in detail in Make vs n8n for solo builders.
- n8n Cloud Starter: about 5 executions a month out of 2,500, but the plan is €20/month billed annually. Paying that for one weekly job makes no sense; it only pays off if you run many workflows there.
- GitHub Actions, private repo: assume one minute per run (checkout, set up Node, ten fetches). GitHub bills a Linux 2-core runner at $0.006 a minute, so about 5 minutes comes to $0.03 against the $12 included. In a public repo, standard runners are free.
- Cloudflare Workers Free: $0. Each run makes 10 page fetches plus one Slack call, inside the 50-request limit. KV use is about 10 reads and at most 10 writes a week.
With ten scheduled jobs the gap opens. Make’s credits add up per action, n8n Cloud has a fixed fee, and GitHub’s included $12 covers roughly 2,000 Linux 2-core minutes (our math from the per-minute rate).
GitHub Actions’ free cron has sharp edges
Five details from GitHub’s schedule event docs matter for a solo builder.
- The shortest interval is every 5 minutes. Fine for monitoring, too slow for anything that should react in seconds.
- Runs can be late, and some can be dropped. GitHub says scheduled runs “can be delayed during periods of high loads,” load peaks at the top of each hour, and if load is high enough “some queued jobs may be dropped.” Schedule at an odd minute, like 06:17, and design jobs so a missed week does no harm.
- Schedules run in UTC by default on the latest commit of the default branch. You can now add a
timezonekey with an IANA name such asAmerica/New_York. - Public repos lose their schedules after 60 days without activity. A monitor that only commits when something changes can switch itself off during a quiet stretch. A private repo avoids this, and the cost difference for a weekly job is cents.
- Failure emails go to one person. Notifications for scheduled workflows go to whoever created the workflow, or to whoever last changed the cron line or re-enabled it. You can set GitHub to notify you only about failed runs.
Cloudflare Workers fit if the job does almost no computing
A Worker with a Cron Trigger runs on Cloudflare’s network with nothing to start up. LeverSolo is built on Astro and hosted on Cloudflare Pages, so for a site like ours the account already exists. The Free plan’s limits decide which jobs fit.
- 10 ms of CPU per cron run. Time spent waiting on
fetch()or KV doesn’t count, so ten fetches are fine. Parsing and cleaning ten large HTML pages does count, and we haven’t measured whether that stays under 10 ms. If runs start failing on CPU, move the job to GitHub Actions or to Workers Paid, which allows 30 seconds of CPU for schedules under an hour and 15 minutes for hourly or slower ones. - 50 outgoing requests per run on Free. Each redirect hop counts, so a page that redirects twice uses three.
- 5 Cron Triggers per account on Free (250 on Paid). Your other Workers share that pool.
- Cron runs in UTC, and adding or changing a trigger can take up to 15 minutes to reach Cloudflare’s network.
- History: Cron Events keeps the 100 most recent runs; Workers Logs keeps more with filtering. The Cron Triggers page doesn’t describe a failure email, so put the alert in your code.
Secrets, logs, and who fixes it when an API changes
Secrets. On GitHub, add them under the repo’s Settings > Secrets and variables > Actions, or run gh secret set SLACK_WEBHOOK_URL. The workflow reads them as ${{ secrets.SLACK_WEBHOOK_URL }}, and secrets aren’t passed to workflows triggered from forks. GitHub’s secure use reference warns that automatic redaction in logs “is not guaranteed,” so never print a secret, and store each value as its own secret instead of one JSON blob. On Cloudflare, run npx wrangler secret put SLACK_WEBHOOK_URL and read it as env.SLACK_WEBHOOK_URL; the value is hidden in Wrangler and the dashboard once set. Make and n8n keep credentials inside the platform.
Maintenance. This is where the routes differ most. When Slack, Google, or Notion changes an API, Make and n8n update their connectors and you might have to reconnect an account. With a script, you open the coding agent, paste the error from the log, and review the fix. For a job that only fetches public pages and posts to one webhook, there is almost nothing to break. For a job that reads Gmail, writes to Google Sheets, and updates Notion, the script has to manage three OAuth token flows, and that is a lot of code for one person to keep alive.
Dead jobs. The quiet failure is a job that stops running: a dropped schedule, a disabled workflow, an expired credential. Have the job post a short “checked 10 pages, no changes” message on the first run of each month. If that message doesn’t arrive, you know to look.
Worked example: a weekly pricing-page monitor
If a tool you recommend changes its price, an article is now wrong. Our competitor pricing page monitor guide covers what to watch and what to do when a page changes; here is the build on GitHub Actions.
Step 1: The prompt for your coding agent
Start in an empty private repo and give the agent the whole job in plain language, including the failure cases.
Build a weekly pricing-page monitor that runs on GitHub Actions.
- Read a list of pages from pages.json: [{ "name": "...", "url": "..." }].
- For each page, fetch it with a 20-second timeout, strip scripts, styles and
tags, collapse whitespace, and compare the visible text with the copy saved
in snapshots/<name>.txt. Save the new copy.
- On the first run for a page, save a baseline and do not alert.
- If any page changed, post one Slack message listing the changed pages.
- Treat HTTP errors, timeouts and suspiciously short text (under 500
characters) as "check failed", not as "changed", and list them in the
same message.
- If every page fails, exit with a non-zero code so the run shows as failed.
- On the first run of each month, post "checked N pages, no changes" even
when nothing changed.
- The Slack webhook URL comes from the SLACK_WEBHOOK_URL secret. Never log it.
- Use Node's built-in fetch and fs. No npm dependencies.
- Workflow: run Mondays at 06:17 UTC, allow manual runs, commit changed
snapshots back to the repo, set the minimum permissions it needs,
and add a 10-minute timeout.
Step 2: The workflow file the agent should produce
Expect something close to this skeleton in .github/workflows/pricing-watch.yml.
name: pricing-watch
on:
schedule:
- cron: '17 6 * * 1' # Mondays 06:17 UTC, off the top of the hour
workflow_dispatch: # lets you run it by hand from the Actions tab
permissions:
contents: write # only needed to commit snapshots back
concurrency: pricing-watch # never two runs at once
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7 # pin to a full commit SHA in your copy
- uses: actions/setup-node@v7
with:
node-version: lts/*
- run: node watch.mjs
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Commit changed snapshots
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add snapshots
git diff --cached --quiet || git commit -m "pricing snapshots $(date -u +%F)"
git push
Committing snapshots back means Git becomes your change log. git log -p snapshots/make.txt shows exactly what moved on a pricing page and when.
Step 3: The script skeleton
// watch.mjs (Node 20 or later: built-in fetch, no dependencies)
import { readFile, writeFile, mkdir } from 'node:fs/promises';
const pages = JSON.parse(await readFile('pages.json', 'utf8'));
await mkdir('snapshots', { recursive: true });
const visibleText = (html) => html
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const changed = [];
const failed = [];
for (const page of pages) {
const file = `snapshots/${page.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.txt`;
try {
const res = await fetch(page.url, { signal: AbortSignal.timeout(20_000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = visibleText(await res.text());
if (text.length < 500) throw new Error('text too short (blocked or rendered by JavaScript?)');
const previous = await readFile(file, 'utf8').catch(() => null);
if (previous !== null && previous !== text) changed.push(`${page.name}: ${page.url}`);
await writeFile(file, text);
} catch (err) {
failed.push(`${page.name}: ${err.message}`);
}
}
const firstRunOfMonth = new Date().getUTCDate() <= 7;
const lines = [
...changed.map((c) => `Changed: ${c}`),
...failed.map((f) => `Check failed: ${f}`),
];
if (lines.length === 0 && firstRunOfMonth) lines.push(`Checked ${pages.length} pages, no changes.`);
if (lines.length > 0) {
const res = await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text: lines.join('\n') }),
});
if (!res.ok) throw new Error(`Slack webhook returned ${res.status}`);
}
if (failed.length === pages.length) process.exit(1);
For Discord instead of Slack, the webhook body uses content in place of text.
Step 4: Check what the agent wrote before you trust it
Read the output against this list. Each item is a mistake coding agents make on this kind of job.
- The cron line means what you think.
17 6 * * 1is Monday 06:17 UTC. If the agent added atimezonekey, confirm it’s the zone you meant. - Permissions are minimal.
contents: writeis the only grant this job needs. Delete anything broader. - Secrets never reach the log. Search the script for
console.lognear the webhook URL. A debug line printing the request is a common slip. - A failed fetch is reported as a failure. If a 403 or a timeout counts as “changed,” you’ll get noise every week and start ignoring the alerts.
- The first run is silent. Without a baseline check, run one sends ten false “changed” alerts.
- The text cleanup fits your pages. Some pricing pages embed dates, rotating testimonials, or build IDs in visible text, so they’ll show as changed every week. Ask the agent to compare only a section of the page (a CSS selector or the text between two headings) for those.
- Pages built by JavaScript show up as “too short.” A plain fetch can’t see prices rendered in the browser. For those pages, the agent can add a headless browser such as Playwright, which runs fine on GitHub’s runners but adds a dependency and run time.
- Third-party actions are pinned. GitHub recommends pinning actions to a full commit SHA. Anything beyond
actions/checkoutandactions/setup-nodedeserves a second look.
Then test it: run the workflow once by hand from the Actions tab to create baselines, edit one snapshot file in the repo, and run it again. You should get one “Changed” message for that page.
The same job as a Worker
If you’d rather run it on Cloudflare, ask the agent for a Worker with a scheduled handler, a KV namespace for snapshots, and the trigger in the Wrangler config.
// wrangler.jsonc (excerpt)
{
"triggers": { "crons": ["17 6 * * 1"] },
"kv_namespaces": [{ "binding": "SNAPSHOTS", "id": "<your-namespace-id>" }]
}
// src/index.js (skeleton)
export default {
async scheduled(controller, env, ctx) {
// For each page: fetch, clean the text, compare with env.SNAPSHOTS.get(url),
// put the new text only when it differs, then POST changes and failures
// to env.SLACK_WEBHOOK_URL. Same rules as the GitHub version.
},
};
Writing only when the text differs keeps KV writes far under the free 1,000 a day. Test locally by sending a request to /cdn-cgi/local/scheduled on the dev server, as Cloudflare’s docs describe.
Where Make and n8n still win
Visual tools earn their price in four situations.
- The workflow signs in to several apps. Make and n8n connectors handle OAuth sign-in and token refresh for Gmail, Sheets, Notion, and the rest. In a script, that plumbing is yours.
- Someone else edits it. A virtual assistant can change a filter on a Make canvas. Asking them to edit YAML and open a pull request is unrealistic.
- You change it every week. Dragging in a new step and clicking Run once takes a minute. Each script change is a prompt, a review, a commit, and a test run.
- You want to see the data at every step. Both tools show what each module received and sent for every run, which makes debugging a five-app workflow far easier than reading a log.
If a job needs to run programs on a server you control (a video render, a long headless-browser session), self-hosted n8n with the Execute Command node is a third option. Our self-hosting checklist for n8n covers what that takes.
Which route fits your job
| Your job | Use |
|---|---|
| Fetch public pages or an API with a key, compare, post to one webhook, on a schedule | AI-written script on GitHub Actions (private repo) |
| Same, very light, and your site is already on Cloudflare | Cloudflare Worker with a Cron Trigger |
| Reads and writes three or more apps that use OAuth sign-in | Make (Free or Core) |
| A non-developer maintains it, or it changes often | Make |
| Long workflows running many times a day, or jobs that need your own server | n8n (Cloud or self-hosted) |
| Has to react within seconds to an incoming webhook | Make or n8n webhooks, or a Worker’s HTTP handler |
| Unsure | Build it in Make Free first, then move it to a script once it stops changing |
Who this is for, and who should skip it
Good fit: you already keep your site in Git, you’re comfortable reading 50 lines of code you didn’t write, and your jobs touch public pages or APIs that take a single key.
Skip it if you don’t use GitHub, you’d never open a log file, or your automations live inside Gmail, Sheets, and a CRM. Make Free will serve you better.
Quick answers
Is GitHub Actions really free for a scheduled job? In a public repo, standard GitHub-hosted runners are free. In a private repo on GitHub Free, usage comes out of $12 of included compute a month, and a weekly one-minute job uses a few cents of it.
Can the AI-written script break without me noticing? Yes, if it’s written badly. Make it exit with an error when every check fails so GitHub emails you, and have it post a monthly “still running” message so a dropped or disabled schedule shows up as silence.
Do I need to know how to code for this? You need to read code well enough to run the eight checks above and to paste an error back to the agent. If that sounds like too much, use Make; its free plan covers this job.
New to automation? Start at the Automation Workflows pillar. Weighing the two visual tools? Read Make vs n8n for solo builders. Want the free static stack these scripts sit next to? See the $0-hosting static site on Astro and Cloudflare.