Whenever an automation conversation starts, the first question is almost always “how far can you automate this?”

Yet looking back over the harnesses I actually run in production — about 30 scripts and 10,000 lines for business-unit forecasting and results management, 12 scripts and 6,000 lines for running a newly opened site — none of them are optimizing for automation coverage. If anything, the places where I explicitly wrote “this part is not automated” turned out to be the most important part of the design.

This article covers those design principles and safety rails in detail. Not abstractions: the rules the running code actually follows.

Sort connections into three routes

There is exactly one policy: match the connection method to the target system, in three categories.

1. If there is a public API, call it directly.

This is the most stable route, and when it fails, the side effects are legible. Put credentials in environment variables and make REST calls — when it breaks, the cause is easy to locate. Reading and writing the shared drive, posting to chat, fetching to-dos from our PMO system, pulling user records from the groupware — I pushed all of that onto this route.

2. If the screen exists only behind SSO, drive a browser.

Systems with no API exposed, or whose UI sits behind single sign-on. Here I automate a browser to log in, then save the session and reuse it. The groupware calendar and workflow, a corporate e-commerce portal, and cloud expense filing all fall into this category.

It is neither fast nor durable, so if route 1 reaches, don’t use a browser. Reverse that order and you will regret it later, without exception.

3. If nothing reaches, leave it explicitly manual.

And this is the most important of the three as a design matter.

Externally shared channels cannot be posted to. Writes to one particular chat tool don’t pass the permission model. There are paper and telephone counters. The moment such a constraint becomes clear, write one line in the docs saying “a human does this,” and do not pretend it is automated.

Half-automating creates a stretch of the process that nobody owns. A message stalled at “the script probably sent it” is the most dangerous state there is. If it doesn’t reach, say so. That alone stabilizes operations.

Never let an irreversible operation happen by accident

The other pillar is safety rails. Here are the ones common to every harness.

The default is always dry-run. Sends, submissions, and orders do not happen until an explicit flag is passed. For approval requests, expenses, and orders alike, the default behavior is to advance to the confirmation screen and cancel. You eyeball the counts and unmatched items before letting it through.

Stop writes at “draft.” The approval-request harness does not carry a submit flag in the code at all. I implemented one once, then deleted it when the policy was settled. The instant something is submitted, approvers get notified, and the cost of unwinding is in a different league from anything on the collection side. Separate “automating the input” from “deciding to submit.” Do that, and when a form revision breaks the input, the damage stops at “the draft looks wrong.”

Overwrite shared files with a version check. Fetch the version → download → edit → re-check the version immediately before upload → upload → re-download and verify. A spreadsheet’s version increments just from somebody having it open, so without this you silently erase other people’s edits. Since introducing that sequence, version-clobbering incidents on our registers have become structurally impossible.

Keep screenshots as evidence. Browser automation writes every screen it passes through to a file. Being able to confirm afterwards whether it really got to that screen is the condition on which I tolerate the browser route at all.

“It clicked” and “it worked” are verified separately. This one I learned from an accident. In a bulk HR-evaluation submission, the code treated opening the confirmation modal as success, and recorded dozens of people as “submitted” when in fact every one of them had only been saved.

Since then, every write re-fetches the state from the server to confirm. The approval-request harness also reads actual values back off the screen before saving and compares them. That check caught two real defects where an unintended project was being silently selected. That a button could be clicked guarantees nothing about the intended outcome.

Make it idempotent. Running the same command twice must not break anything. Idempotency keyed on request ID, upsert by content hash, a posting log that skips already-processed rows on re-run, a file lock preventing overlapping scheduled runs. Once something is in production, “it died partway through and I don’t know how far it got” will happen. Whether you can safely re-run at that moment turned out to be the deciding factor in whether a harness gets used day to day.

One core, thin wrappers. The core of the approval-request collector is 240 lines; the revenue, quote, and invoice variants are 44-line wrappers each. They change the form name and output destination, and delegate all flag handling to the core. When you multiply scripts with AI assistance, similar-but-different implementations pile up if left alone. Extract the core before you multiply — make that a habit, or in two months it becomes unmanageable.

Don’t add dependencies. The shared-drive script runs on the standard library alone, from OAuth through upload. A convenient SDK would make it shorter, but it won’t run when the environment changes. Harnesses are disposable by design, so running where you put it matters more than being short.

Change the format per destination. Most chat tools don’t render Markdown. Both ** and # appear on screen as literal characters. When the destination of a message is fixed, emit it in a form that can be pasted there. It’s unglamorous, but neglect it and a stream of “obviously AI-written, hard-to-read” text flows to the people on the ground.

Build on the assumption that the other system is not internally consistent

Line up the pitfalls hit during implementation and a common property emerges.

  • Within a single API, naming conventions differ per endpoint (one is snake_case, another camelCase; get it backwards and it fails with a required-field error)
  • The standard-library HTTP client gets blocked, but the same request goes through with curl
  • The wrong auth header type returns 401; the right one can still return 403 for unrelated reasons
  • A calendar shows as free while the actual bookings live in a different calendar system
  • A payment-method option gets an approval request bounced back for a one-word mismatch

None of these are preventable by reading documentation in advance. You build on the assumption that the other system is not internally consistent, and write each discovered behavior back into the docs as you find it. Conversely, as long as that write-back continues, the pace at which you can add harnesses with AI does not slow down.

Report effects starting from what you did not measure

Finally, on how to report results.

For the site-operations harnesses, I did not measure elapsed time before and after adoption. So I cannot write “X% faster.” The volume and mix of work before and after are different, so there is no comparable denominator to begin with.

What I can report instead are two things: which operations were replaced, and which failures stopped occurring. Both are verifiable as fact.

  • Updating a shared register went from “download → edit → overwrite” to a version-checked update, and erasing someone else’s edits became structurally impossible
  • Messages to the site went from “typed and sent on the spot” to “written to a file, then sent,” so what was communicated and when can now be traced
  • Scheduling went from eyeballing several people’s calendars to having candidate slots produced

Rather than inflating an estimated hours-saved figure, I think this framing is both verifiable and more useful to the reader.


The principles here don’t depend on any particular tool. Which order to adopt them in, and which processes to connect first, are things I’m happy to go into in a conversation.