I have a rule that has saved me, very conservatively, several hundred hours of work over the last few years. It is not clever. It is not original. It is the oldest rule in software, dressed up slightly: if I do something manually twice, I assume I will do it a third time, and I automate it before that third time arrives.
The first time, I do the thing by hand. Run the query, format the CSV, deploy the branch, whatever. The second time, I do it by hand again, but this time with a tiny voice in the back of my head saying “you have done this before”. That voice is the cue. I stop, and I write the script. By the third time, the script is already there, I run one command, I move on. The whole point is to catch the pattern early, while it is still small and cheap to wrap a function around.
What surprises people, when I describe this, is not the principle. The principle is uncontroversial. What surprises them is how often it applies, and how aggressively I apply it.
What “doing it twice” actually looks like
The famous examples of automation are the big-budget ones: the deploy pipeline, the nightly Airflow DAG, the dbt model that replaces a hand-built SQL report. Those are the easy cases. Everyone knows you should automate those. They are projects with budgets, tickets, owners.
The cases I care about are the ones below the radar, the ones nobody puts on a roadmap.
A stakeholder asks me on Slack for a quick count of active users for a region. I write the SQL, I paste the result, I move on. The next week, somebody else asks the same question with a different region. That is the moment. Two people, one shape of question. I take the SQL, I parametrise the region, I drop it into a Notion page with a code block titled “Active users by region (run in Postgres)”, and I link it the next time. From then on, anyone in the company who asks gets pointed at the page. The third request never reaches my inbox.
A weekly report I generate by joining three CSVs in a notebook. The first week, the notebook is forgivable, I am still figuring out the shape of the data. The second week, I notice I am repeating the same three cells. I refactor it into a just report command that runs a Python script and writes a Markdown file. Total time invested: maybe forty minutes. Time saved every week after that: about an hour, plus the mental tax of remembering which cell to run first.
A staging deploy that requires three commands in three different terminals. Twice in a row I almost ran the migration on prod. The third time, I would have. So instead I wrote a just deploy-staging recipe that runs the three commands in the right order, with the right env, and refuses to run if the current branch is main. Total time invested: twenty minutes. Things that did not happen: a prod outage I would have spent a weekend cleaning up.
The pattern: tiny things, the kind nobody would put on a Jira board, the kind that feel too small to optimise. Those are exactly the ones where the math is most lopsided.
The hidden cost of not automating
When people argue against automating these small tasks, the argument is usually “it would take longer to automate it than to just do it”. Sometimes that is true. Often it is not, because the cost of doing the thing manually is not just the time spent doing it.
There is the time, yes. Five minutes per run, ten runs a month, an hour a month, twelve hours a year, and that is for a task you forget about. Multiply by the number of small recurring tasks in a normal week and you are easily losing a day a month to things you could automate.
But the bigger cost is the one that does not show up on a timesheet. There is decision fatigue: every time you sit down to do the manual thing, you have to remember the steps, the flags, the order. There is the error rate: hand-running a query, copying numbers into a spreadsheet, eyeballing the output. The hand-run version is wrong some non-trivial fraction of the time, and when you find out, you have to go back and re-do the work. There is the context switch, which is the most underrated cost in engineering. Stopping what you are doing, opening Postgres, finding the query, running it, formatting the output, going back to your previous task, takes a lot longer than the literal time spent on the steps. The activation energy is the killer.
I had a colleague who was hand-running a backfill every Monday. The data team had inherited a slightly broken upstream feed, and the fix on our side was to re-run a script over the weekend’s data. He did this for six months, every Monday, for about an hour. He kept saying “I should automate this, but it feels like a big job”. When he finally sat down, the actual automation, an Airflow task with a sensor on the upstream feed, took him ninety minutes. He had spent twenty-six hours on the manual version. The automation paid for itself the next Monday and kept paying for itself for the rest of the year.
The trap was not laziness. It was a wrong estimate of how big the automation actually was, and a vastly underweighted estimate of how much the manual version was costing him.
The other side: when automation is the wrong call
I want to be honest about the other failure mode, because the “automate everything” people are wrong too, and I have been one of them.
The first kind of bad automation is premature. You did the thing once. You decide to automate it before you really understand the shape of the problem. You spend a day building a generic tool, with config files and flags and a help page, and then it turns out the second time the requirements were slightly different, and your beautiful generic tool does not fit. You end up either rewriting it or doing the second iteration by hand anyway. The rule of three exists precisely to prevent this. Two data points are enough to draw a line. One is not.
The second kind of bad automation is what I think of as Rube Goldberg automation. A pipeline that strings together five tools, each of which can fail in a different way, to do something that a single Bash one-liner could have done. A GitHub Action that calls a Lambda that writes to S3 that triggers a notification that opens a Jira ticket, when the underlying task is “send me an email when this number is below 100”. The maintenance cost of these contraptions, when one of the five pieces breaks, is enormous. You have not automated the work, you have just moved the work into “diagnosing why the automation broke”.
A useful test: when the automation fails, how long does it take to fall back to doing the thing by hand? If the answer is “fifteen minutes”, you are fine. If the answer is “I have no idea anymore, I have not done this manually in a year and the runbook is out of date”, you have over-engineered yourself into a corner.
The tools I actually reach for
The boring honest answer is that most of my automations live in one of three places.
A justfile (or a Makefile, same idea) at the root of every project. Recipes for everything I do more than once: just test, just lint, just deploy-staging, just backfill DATE. The barrier to adding one is so low that I add them constantly. They are documentation as much as automation: anyone who joins the project can read the justfile and see what the project is capable of doing.
uv run for one-off Python scripts. A single file with a shebang and an inline # /// script block listing dependencies. No virtualenv ceremony, no requirements.txt to maintain, no “wait, which Python is this”. I have a ~/scripts folder full of these. They are how I automate the personal repetitive tasks: renaming photos by EXIF date, parsing a weekly bank statement, sanity-checking a CSV before I import it.
Pre-commit hooks for the things I keep forgetting. Run ruff before commit. Refuse commits to main. Refuse commits with a TODO REMOVE BEFORE PUSH. None of these are clever. All of them have saved me from a stupid mistake in the past month.
For the small social automations, Slack slash commands and a few Notion templates do the job. A /oncall command that pages the right person. A “weekly report” Notion template that I duplicate every Monday. None of this is glamorous. All of it compounds.
The reason this rule has stuck with me is that it is not really about saving time. The time savings are real and they add up, but they are a side effect. The real win is that automating the second occurrence of a task forces me to actually understand the task. I have to write down the steps, the inputs, the failure modes. And once it is written down, I can hand it to a teammate, an LLM, or a future me who has forgotten everything. The script is the fossilised understanding. The rule of three is just the trigger that makes me sit down and write it.