{"course":{"slug":"agents-and-automation","title":"Agents and Automation","url":"https://addaly.com/learn/agents-and-automation","level":"intermediate","track":"build"},"lesson":{"slug":"automation-before-agents","title":"The boring stack that already does this","url":"https://addaly.com/learn/agents-and-automation/automation-before-agents","position":2,"of":72,"minutes":8,"answer":"Most automation requests are a scheduler and thirty lines of code; put a model only in the one step where the input is genuinely unstructured.","keyIdea":"Most automation requests are a scheduler and thirty lines of code; put a model only in the one step where the input is genuinely unstructured."},"body":{"format":"markdown","text":"## Walk the request backwards\n\nSomeone asks whether AI can automate the Monday report. Before answering, find out what the report is. Usually: three numbers pulled from a database, arranged in an email, sent at six on Monday morning.\n\nThere is nothing in that description a model does better than a `SELECT` and a scheduler. It will do it more expensively, more slowly, and with a small chance of inventing a number.\n\nThis is not a reason to avoid models. It is a reason to find the step that actually needs one.\n\n## The five fields, and the trap in them\n\n```\n0 6 * * 1   /usr/bin/python3 /opt/reports/weekly.py\n```\n\nMinute, hour, day-of-month, month, day-of-week. That line means 06:00 every Monday.\n\nHere is the part that catches people: when you set **both** day-of-month and day-of-week, cron treats them as OR, not AND. `0 6 13 * 5` runs on the 13th of every month *and* on every Friday. Most people read it as \"Friday the 13th\" and write a job that fires about 62 times a year instead of one to three times: roughly 36 times as often as they intended.\n\n## Four parts, and only one of them is clever\n\nNearly every automation is:\n\n1. **A trigger**: a schedule, a webhook, a new row, a person pressing a button.\n2. **A fetch**: an API call, a query, a file read.\n3. **A transform**: the actual work.\n4. **A delivery**: an email, a message, a row written back, a file saved.\n\nSteps 1, 2 and 4 have had good deterministic answers for thirty years. Step 3 is where you decide.\n\n## Where a model earns its place\n\nPut a model in the transform when the input is unstructured and varied:\n\n- free text a human wrote, which needs classifying, summarising or extracting\n- a document whose layout changes between senders\n- a decision that depends on meaning rather than on a field\n\nEverything else (arithmetic, formatting, routing on a known value, sending) stays as code. A pipeline with one model call in the middle is cheaper to run, easier to test, and fails in ways you can read.\n\n## The failure the boring stack has, and its mechanism\n\nDeterministic pipelines break silently when the shape of the data changes, and the mechanism is almost always a default value.\n\n```python\ntotal = row.get(\"total\", 0)      # silent: the report says revenue is zero\ntotal = row[\"total\"]             # loud: KeyError, and you find out tonight\n```\n\nUpstream renames `total` to `total_amount`. The first line keeps running and reports zero revenue every night. The second stops and pages you. **Never default a missing field to a plausible value.** A crash is a message; a zero is a lie.\n\nAdd one sanity assertion at the end of the transform (this week's total is within some sane multiple of last week's) and you catch the entire class of failure that no exception will ever throw.\n\n## The free path\n\nYou do not need a platform to start.\n\n- **cron**, on any Linux box you already pay for.\n- **systemd timers**, if you want a missed run to catch up after a reboot: `OnCalendar=Mon *-*-* 06:00:00` plus `Persistent=true`. Cron skips a run it slept through.\n- **GitHub Actions** `schedule:`: free minutes on public repositories, five-minute minimum granularity. Be aware that scheduled workflows get disabled automatically after around 60 days without repository activity, and that runs at popular times drift by minutes.\n- **Cloudflare Workers cron triggers**, if the work is short and lives at the edge.\n\nJob adverts name **Zapier** and **Make**, and you should know them: both are excellent at connectors and both meter you per task or per operation. **n8n** self-hosted, **Node-RED** and **Windmill** do the same job for the price of a small server, and n8n has its own cloud if you would rather not run it.\n\nStart with the boring one. You can always add a model to step 3 later; it is much harder to take an agent apart once a business depends on it."},"check":{"question":"A nightly job pulls yesterday's orders and emails a revenue total. The upstream team renames the `total` column to `total_amount`. The job keeps running, and the email reports revenue of zero every night for nine days before anyone notices. Which change would most directly have surfaced the break on night one?","options":[{"text":"Read the field with `row['total']` instead of `row.get('total', 0)`, so a missing column raises instead of quietly producing a plausible number.","correct":true},{"text":"Add a model to the pipeline to work out which column now holds the total.","correct":false,"tempting":"Reaches for intelligence where the problem is a silent default; the model cannot know a value is missing either, because nothing failed."},{"text":"Move the job from cron to a workflow tool with a retry policy.","correct":false,"tempting":"Retries a job that is succeeding. Nothing errored on any of the nine nights, which is exactly the problem."},{"text":"Run the job hourly instead of nightly so problems surface sooner.","correct":false,"tempting":"Produces the wrong answer more often rather than making a wrong answer detectable."}]},"prev":{"slug":"chatbot-workflow-agent","title":"Three machines, one word","url":"https://addaly.com/learn/agents-and-automation/chatbot-workflow-agent"},"next":{"slug":"mapping-the-process-first","title":"Watch the process before you automate it","url":"https://addaly.com/learn/agents-and-automation/mapping-the-process-first"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The boring stack that already does this', in Agents and Automation. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/agents-and-automation/automation-before-agents (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The boring stack that already does this","courseTitle":"Agents and Automation","module":{"index":1,"slug":"deciding-what-to-build","title":"Before you build: script, workflow, or agent"},"answer":null,"keyIdea":"Most automation requests are a scheduler and thirty lines of code; put a model only in the one step where the input is genuinely unstructured.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"walk-the-request-backwards","runs":[{"text":"Walk the request backwards"}]},{"t":"p","runs":[{"text":"Someone asks whether AI can automate the Monday report. Before answering, find out what the report is. Usually: three numbers pulled from a database, arranged in an email, sent at six on Monday morning."}]},{"t":"p","runs":[{"text":"There is nothing in that description a model does better than a "},{"text":"SELECT","code":true},{"text":" and a scheduler. It will do it more expensively, more slowly, and with a small chance of inventing a number."}]},{"t":"p","runs":[{"text":"This is not a reason to avoid models. It is a reason to find the step that actually needs one."}]},{"t":"h2","id":"the-five-fields-and-the-trap-in-them","runs":[{"text":"The five fields, and the trap in them"}]},{"t":"code","lang":"","label":"Text","text":"0 6 * * 1   /usr/bin/python3 /opt/reports/weekly.py"},{"t":"p","runs":[{"text":"Minute, hour, day-of-month, month, day-of-week. That line means 06:00 every Monday."}]},{"t":"p","runs":[{"text":"Here is the part that catches people: when you set "},{"text":"both","b":true},{"text":" day-of-month and day-of-week, cron treats them as OR, not AND. "},{"text":"0 6 13 * 5","code":true},{"text":" runs on the 13th of every month "},{"text":"and","i":true},{"text":" on every Friday. Most people read it as “Friday the 13th” and write a job that fires about 62 times a year instead of one to three times: roughly 36 times as often as they intended."}]},{"t":"h2","id":"four-parts-and-only-one-of-them-is-clever","runs":[{"text":"Four parts, and only one of them is clever"}]},{"t":"p","runs":[{"text":"Nearly every automation is:"}]},{"t":"ol","start":1,"items":[[{"text":"A trigger","b":true},{"text":": a schedule, a webhook, a new row, a person pressing a button."}],[{"text":"A fetch","b":true},{"text":": an API call, a query, a file read."}],[{"text":"A transform","b":true},{"text":": the actual work."}],[{"text":"A delivery","b":true},{"text":": an email, a message, a row written back, a file saved."}]]},{"t":"p","runs":[{"text":"Steps 1, 2 and 4 have had good deterministic answers for thirty years. Step 3 is where you decide."}]},{"t":"h2","id":"where-a-model-earns-its-place","runs":[{"text":"Where a model earns its place"}]},{"t":"p","runs":[{"text":"Put a model in the transform when the input is unstructured and varied:"}]},{"t":"ul","items":[[{"text":"free text a human wrote, which needs classifying, summarising or extracting"}],[{"text":"a document whose layout changes between senders"}],[{"text":"a decision that depends on meaning rather than on a field"}]]},{"t":"p","runs":[{"text":"Everything else (arithmetic, formatting, routing on a known value, sending) stays as code. A pipeline with one model call in the middle is cheaper to run, easier to test, and fails in ways you can read."}]},{"t":"h2","id":"the-failure-the-boring-stack-has-and-its-mechanism","runs":[{"text":"The failure the boring stack has, and its mechanism"}]},{"t":"p","runs":[{"text":"Deterministic pipelines break silently when the shape of the data changes, and the mechanism is almost always a default value."}]},{"t":"code","lang":"python","label":"Python","text":"total = row.get(\"total\", 0)      # silent: the report says revenue is zero\ntotal = row[\"total\"]             # loud: KeyError, and you find out tonight"},{"t":"p","runs":[{"text":"Upstream renames "},{"text":"total","code":true},{"text":" to "},{"text":"total_amount","code":true},{"text":". The first line keeps running and reports zero revenue every night. The second stops and pages you. "},{"text":"Never default a missing field to a plausible value.","b":true},{"text":" A crash is a message; a zero is a lie."}]},{"t":"p","runs":[{"text":"Add one sanity assertion at the end of the transform (this week’s total is within some sane multiple of last week’s) and you catch the entire class of failure that no exception will ever throw."}]},{"t":"h2","id":"the-free-path","runs":[{"text":"The free path"}],"role":"free"},{"t":"p","runs":[{"text":"You do not need a platform to start."}]},{"t":"ul","items":[[{"text":"cron","b":true},{"text":", on any Linux box you already pay for."}],[{"text":"systemd timers","b":true},{"text":", if you want a missed run to catch up after a reboot: "},{"text":"OnCalendar=Mon *-*-* 06:00:00","code":true},{"text":" plus "},{"text":"Persistent=true","code":true},{"text":". Cron skips a run it slept through."}],[{"text":"GitHub Actions","b":true},{"text":" "},{"text":"schedule:","code":true},{"text":": free minutes on public repositories, five-minute minimum granularity. Be aware that scheduled workflows get disabled automatically after around 60 days without repository activity, and that runs at popular times drift by minutes."}],[{"text":"Cloudflare Workers cron triggers","b":true},{"text":", if the work is short and lives at the edge."}]]},{"t":"p","runs":[{"text":"Job adverts name "},{"text":"Zapier","b":true},{"text":" and "},{"text":"Make","b":true},{"text":", and you should know them: both are excellent at connectors and both meter you per task or per operation. "},{"text":"n8n","b":true},{"text":" self-hosted, "},{"text":"Node-RED","b":true},{"text":" and "},{"text":"Windmill","b":true},{"text":" do the same job for the price of a small server, and n8n has its own cloud if you would rather not run it."}]},{"t":"p","runs":[{"text":"Start with the boring one. You can always add a model to step 3 later; it is much harder to take an agent apart once a business depends on it."}]}],"check":{"question":[{"text":"A nightly job pulls yesterday’s orders and emails a revenue total. The upstream team renames the "},{"text":"total","code":true},{"text":" column to "},{"text":"total_amount","code":true},{"text":". The job keeps running, and the email reports revenue of zero every night for nine days before anyone notices. Which change would most directly have surfaced the break on night one?"}],"options":[{"text":[{"text":"Read the field with "},{"text":"row['total']","code":true},{"text":" instead of "},{"text":"row.get('total', 0)","code":true},{"text":", so a missing column raises instead of quietly producing a plausible number."}],"correct":true,"tempting":null},{"text":[{"text":"Add a model to the pipeline to work out which column now holds the total."}],"correct":false,"tempting":[{"text":"Reaches for intelligence where the problem is a silent default; the model cannot know a value is missing either, because nothing failed."}]},{"text":[{"text":"Move the job from cron to a workflow tool with a retry policy."}],"correct":false,"tempting":[{"text":"Retries a job that is succeeding. Nothing errored on any of the nine nights, which is exactly the problem."}]},{"text":[{"text":"Run the job hourly instead of nightly so problems surface sooner."}],"correct":false,"tempting":[{"text":"Produces the wrong answer more often rather than making a wrong answer detectable."}]}],"order":[1,2,3,0],"why":null},"prev":{"slug":"chatbot-workflow-agent","title":"Three machines, one word","minutes":6},"next":{"slug":"mapping-the-process-first","title":"Watch the process before you automate it","minutes":9}}}