Marketing Automation
"Automation applied to an efficient operation will magnify the efficiency. Automation applied to an inefficient operation will magnify the inefficiency." — Bill Gates
That Bill Gates quote IS this chapter. Read it twice. Automation is how one person does the work of a team: set up a system once and it does the repetitive grind forever — while you sleep, while you're on holiday, while you do the thinking work a human is actually for.
But here's where I've changed my mind, and it's the whole point of this chapter. I used to build all of this in no-code tools. Now I just write a cron job. Let me show you why, with real code you can ship today.

I used to pay for Make, n8n, and Zapier. Now I write a cron
For years my answer to automation was: prototype in Zapier, rebuild in Make when it got complex, move to self-hosted n8n when it got big. I genuinely liked those tools — n8n was my favourite. I don't reach for any of them first anymore. For most things, I write a cron job and a small script. The honest reasoning:
- Cheaper. Zapier and Make charge per task. A cron on a server you already have costs zero. I'm a cheapskate, so this hits hard.
- More reliable. No third-party platform between me and the job — fewer moving parts, no "the integration broke after their update."
- No per-task pricing anxiety. The script can fire 10 times or 10,000 — same cost. I never think about "task limits" again.
- AI writes the script in seconds. This is the unlock. The old reason for no-code was "I can't write the code" — that reason is gone. I describe the job, AI writes it, I read it, I ship it, faster than dragging nodes on a canvas. No-code tools existed to spare you from code; when AI writes it in seconds, the no-code tax — the monthly fee, per-task pricing, lock-in, vendor outage — stops being worth paying.
I'm not saying never touch n8n. If you DON'T own a server and don't want to, a no-code tool is a fine on-ramp. But if you have a Vercel project or a cheap VPS already running, a cron is cheaper, more reliable, and now just as fast to build. That's where I land almost every time.
Cron 101 (so the rest of this makes sense)
A cron is just "run this thing on this schedule." The schedule is five fields: minute, hour, day-of-month, month, day-of-week. You'll mostly copy these, but know the shape:
# ┌─────── minute (0-59)
# │ ┌───── hour (0-23)
# │ │ ┌─── day of month (1-31)
# │ │ │ ┌─ month (1-12)
# │ │ │ │ ┌ day of week (0-6, Sun=0)
# │ │ │ │ │
0 9 * * * # every day at 09:00
*/15 * * * * # every 15 minutes
0 8 * * 1 # every Monday at 08:00Can't remember the syntax? Nobody can. Ask AI "cron for every weekday at 7am" or paste it into crontab.guru to read it back in English. Don't memorise this.
Option A — a Vercel Cron Job
If your site is already on Vercel, this is the path of least resistance. A cron is two things: a schedule declared in vercel.json, and an API route it hits. Vercel calls the URL on schedule — no server to manage.
// vercel.json
{ "crons": [{ "path": "/api/cron/daily-digest", "schedule": "0 9 * * *" }] }
// app/api/cron/daily-digest/route.ts — a normal Next.js route Vercel hits at 9am
import { NextResponse } from "next/server";
export async function GET(req: Request) {
// protect it so randoms can't trigger it
const auth = req.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`)
return new NextResponse("Unauthorized", { status: 401 });
// ...do the work: pull metrics, send the Slack message, etc.
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
body: JSON.stringify({ text: "☀️ Daily digest: 14 leads, $312 spend." }),
});
return NextResponse.json({ ok: true });
}Vercel cron endpoints are public URLs. Always gate them behind a CRON_SECRET (Vercel sends it as a Bearer token automatically when you set it as an env var). Otherwise anyone who finds the path can trigger your job.
Option B — a VPS crontab
If you've got a cheap VPS (I run a Hetzner box for a few euros a month), this is the most bulletproof, most flexible option there is. Write a script, add one line to crontab, done. It'll run anything — Node, Python, a shell one-liner:
# edit your crontab
crontab -e
# run the script every day at 9am, log output
0 9 * * * /usr/bin/node /home/me/scripts/daily-digest.js >> /home/me/logs/digest.log 2>&1The `>> log 2>&1` bit captures both normal output and errors to a file, so when something breaks you can see why. That's it. No dashboard, no per-task fee, runs forever — the whole "automation platform" replaced by one line.
Use absolute paths in crontab — /usr/bin/node, not node. Cron runs with a bare environment and won't find things on your PATH; the single most common reason a cron "silently doesn't run" is a relative path it can't resolve.
End-to-end: lead → Supabase → Slack, on a cron
Let's build the bread-and-butter automation every marketer should have: a lead comes in, lands in a database, and you get pinged. The way I actually do it — a form posts to an API route that writes to a secure database, then a cron sweeps new leads into Slack. No Zapier, no per-task fee.
Step 1 — the lead lands in your own API route and gets stored (Supabase here):
// app/api/lead/route.ts
import { createClient } from "@supabase/supabase-js";
import { NextResponse } from "next/server";
const db = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY! // server-side key, never client
);
export async function POST(req: Request) {
const { email, utms } = await req.json();
const { error } = await db.from("leads").insert({
email,
source: utms?.source ?? null,
campaign: utms?.campaign ?? null,
notified: false, // the cron will flip this once Slacked
});
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ ok: true });
}Step 2 — a cron route sweeps any un-notified leads, posts them to Slack, then marks them done so you never get pinged twice:
// app/api/cron/notify-leads/route.ts
import { createClient } from "@supabase/supabase-js";
import { NextResponse } from "next/server";
const db = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);
export async function GET(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`)
return new NextResponse("Unauthorized", { status: 401 });
const { data: leads } = await db
.from("leads")
.select("id, email, source, campaign")
.eq("notified", false)
.order("id", { ascending: true }) // stable order = safe pagination
.limit(50);
for (const lead of leads ?? []) {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
body: JSON.stringify({
text: `🔥 New lead: ${lead.email} (${lead.source ?? "direct"} / ${lead.campaign ?? "-"})`,
}),
});
await db.from("leads").update({ notified: true }).eq("id", lead.id);
}
return NextResponse.json({ notified: leads?.length ?? 0 });
}Step 3 — schedule it. Every 5 minutes is plenty for speed-to-lead:
// vercel.json
{ "crons": [{ "path": "/api/cron/notify-leads", "schedule": "*/5 * * * *" }] }That's the entire thing. Lead captured, stored, and you're pinged within 5 minutes — for free, on infrastructure you already pay for. I can't stress how freaking useful this is!! A Zapier flow you'd pay monthly for, replaced by ~40 lines you own and can read.
Speed-to-lead is everything — 5 minutes vs an hour can be the difference between a sale and a ghost. Want it INSTANT? Fire the Slack message straight from the /api/lead route on insert. Use the cron version when you'd rather batch, or when Slack might be down and you want a retry-able sweep. And speed beats perfection: a 40-line cron that pings you in 5 minutes beats the "perfect" CRM integration you never finished. Ship the rough version, watch real leads flow, refine later.
Daily reporting into Slack
Same machinery, different job. I live in Slack, so I pipe everything important into it. Instead of logging into six dashboards every morning, the numbers come to ME — the daily-digest cron from earlier is exactly this. Coffee in hand, whole picture in 30 seconds. Stuff I push into Slack on a cron:
- Daily/weekly digests — spend, leads, conversions, ROAS — per client or project.
- Instant alerts when something breaks — conversions drop to zero, spend spikes, budget runs out.
- New high-value lead pings, so I follow up personally and fast.
- "Tracking might be broken" alerts — if a key conversion hasn't fired in X hours, ping me.
The alerts matter more than the reports. A pretty weekly digest is nice. An instant "your conversions just dropped to zero" ping on a Tuesday saves you from finding out on Friday that you've burned a week of spend for nothing. Build the alerts first. You can't watch everything all the time, so you build little scripts that watch for you and only interrupt when a human is needed.
Drop AI into the cron
Here's where it gets genuinely fun. The old no-code "AI node" was a selling point, but your script can just call an AI API directly — it's one fetch — so the smart step is no longer locked inside a platform. Inside any cron, AI can do the little judgement calls between the mechanical steps:
// inside a cron route — turn raw numbers into a plain-English insight
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-haiku-4-5",
max_tokens: 200,
messages: [{ role: "user", content: `One-line insight from today's numbers: ${JSON.stringify(stats)}` }],
}),
});
const { content } = await res.json();
const insight = content[0].text; // → "Spend up 20% but CPL flat — efficiency improved."Real things I use an AI step for inside a cron: categorise and route an inbound lead's message (support / sales / spam), summarise a long form submission into 3 bullets before it hits Slack, draft a personalised first-reply, or turn raw numbers into the plain-English line above. The mechanical steps stay code; the judgement call is one API call. Keep a human in the loop for anything that goes out under your name or touches money — let AI DRAFT, you hit send, at least until you trust it. Drafting is safe to fully automate; sending is where you go slow.
Don't automate a broken process
We opened with Bill Gates and we'll close with him: never automate a broken process — you'll just produce broken results faster, in more places, and take ages to notice. Do the task by hand a few times first, write down every step, THEN automate the thing you actually understand. Gut-check before building anything: how often do I do this, and how long does it take? A 30-second task twice a year? Leave it. A 5-minute task 20 times a day? That's the gold. My practical guardrails for any cron:
- Test with fake data first. Run garbage through it before you point it at real leads or real money — a throwaway row, a test Slack channel.
- Make failures loud. Wrap the work in try/catch and post the error to Slack. A silent broken cron is worse than no cron, because you THINK it's running.
- Cap the worst case. A .limit(50) on the sweep, a daily send cap, a hard stop — so a bug can't email one contact 200 times at 3am. Ask "what's the most damage this can do if it goes haywire?" and build a wall there.
- Make it idempotent. The notified flag means re-running doesn't re-ping leads. Design so running twice is harmless — crons WILL occasionally double-fire.
- Log everything and leave a comment. >> log 2>&1 on the VPS, console.logs on Vercel, plus a one-line comment on why this cron exists — future-you will have completely forgotten.
The horror story everyone has: a loop fires 200 emails to one contact, charges a card repeatedly, or duplicates every lead 50 times. The cron approach doesn't save you from this automatically — YOU put the cap in. The .limit() and the idempotent flag above are not optional polish, they're the wall.
And review your crons periodically — they rot. An API changes, a field gets renamed, a key expires, and your script quietly stops working. Same energy as the tracking-health check from the last chapter: put it on the calendar. Done right, automation is the single biggest force multiplier a solo marketer has — and now it costs basically nothing, because the no-code tax is gone and AI writes the script. Build the machine once in code you own, and it pays you back every single day after. That's the leverage this whole course is about: the work of a team, by yourself, with tools and AI carrying the load.