Ten Configuration Habits That Keep Automations Alive
An automation that runs unattended has two jobs: finish the work, and make it obvious when it did not. Most scheduled jobs die of the second failure first, quietly, while everyone assumes the first is fine. The ten habits below are configuration decisions, not architecture, and each one closes a specific way a job goes silent.
The ten habits, and the failure each one prevents
| # | Habit | Failure it prevents |
|---|---|---|
| 1 | Config in files, not code | A behaviour change that needs a code edit and redeploy instead of one value. |
| 2 | One secret store | A rotated credential that survives in a stale copy somewhere else. |
| 3 | Explicit timeouts | A hung network call holding a worker while the queue backs up behind it. |
| 4 | Meaningful exit codes | A scheduler that cannot tell “retry me” from “page a human”. |
| 5 | A dry-run flag | Test runs that write real data because running for real was the only check. |
| 6 | Idempotent steps | Duplicate records every time a partial failure is retried. |
| 7 | Log rotation | A disk that fills up and takes the whole host down with it. |
| 8 | Alerts on outcomes | A job that stopped running weeks ago and nobody noticed. |
| 9 | Pinned dependency versions | An unattended upgrade that changes behaviour between two identical runs. |
| 10 | A documented re-run command | An incident where nobody knows the exact invocation to redo one day’s work. |
Habits 1, 2 and 9: keep configuration and dependencies out of the code
Anything that differs between environments โ paths, table names, recipient addresses, model names, concurrency โ belongs in a config file read at startup, not in a branch inside the script. The failure this prevents: every environment ends up running a slightly different version of the logic, and diffs stop being reviewable. Two details make it real: validate the config on startup and exit non-zero when a required key is missing, and keep the config in version control with a separate untracked file for machine-specific values.
One secret store. Pick a single place that holds credentials โ a managed secrets service, an encrypted file in the repo, or the environment injected by your platform โ and read from it and nowhere else. No copies in shell profiles, no tokens pasted into the workflow file, no silent fallback to a hardcoded default. The failure this prevents: rotating a key in the one place you remembered while the job quietly keeps using a stale copy.
Pinned dependency versions. Commit lockfiles, pin container images by digest, and avoid latest tags on anything scheduled. The failure this prevents: an unattended upgrade between two otherwise identical runs changes behaviour, and you spend an afternoon hunting a bug that is really a version bump. That problem shows up in model work too, which is why swapping a model in place is its own kind of change; see Swapping Models on a Single-GPU Box Without Downtime Drama.
# config/pipeline.yaml
schedule: "0 6 * * *"
concurrency: 4
source:
url: "https://internal.example.com/export"
timeout_seconds: 30
retries: 3
output:
table: "daily_export"
mode: "upsert"
set -euo pipefail
CONFIG="${CONFIG_PATH:-./config/pipeline.yaml}"
# Fail fast: a missing key should stop the run, not default to something wrong.
python - "$CONFIG" <<'PY'
import sys, yaml
cfg = yaml.safe_load(open(sys.argv[1]))
required = {"source": ["url", "timeout_seconds"], "output": ["table", "mode"]}
missing = [f"{s}.{k}" for s, keys in required.items()
for k in keys if k not in (cfg.get(s) or {})]
if missing:
print("config error:", ", ".join(missing), file=sys.stderr)
sys.exit(2)
PY
Habits 3, 4 and 8: make time and failure explicit
Every outbound call needs a deadline: --max-time on curl, a timeout= argument on the HTTP client, subprocess.run(..., timeout=...) on any child process, and a wall-clock limit on the whole job. A timeout is not a performance setting; it is the difference between a job that fails and a job that hangs. Set values from observed worst-case latency plus headroom, and keep them in config so you can raise one without touching code. The failure this prevents: a single stuck socket holds the worker, the queue grows behind it, and nothing in your logs says anything is wrong.
Use a small, fixed exit-code vocabulary and document it: 0 success, 1 unexpected error, 2 bad configuration, 75 (EX_TEMPFAIL) for something worth retrying. The failure this prevents: a scheduler that treats every non-zero code the same, either retrying a job that will never succeed or dropping one that needed a second attempt.
Alert on outcomes, not just errors. Two alerts matter: the job exited with a code that needs a human, and the job did not run at all. The second is the one people skip. Have the job ping a heartbeat on success and alert when the ping is missing, then route both alerts to one channel a person actually reads. The failure this prevents: a cron entry removed during a migration three weeks ago that nobody notices until a report comes up empty. That silent-failure class deserves its own habits, which I covered in Triage Habits for Automation That Fails While You Sleep.
Habits 5, 6, 7 and 10: make re-runs and logs boring
A dry-run flag: --dry-run prints every write, request and side effect it would perform, and performs none of them. The failure this prevents: the only way to check a change is to run it against live data and clean up afterwards.
Idempotent steps: writes are upserts keyed on something stable โ a date, a source ID, a hash of the input โ never blind inserts. The failure this prevents: a retry after a partial failure duplicates rows, and someone spends a morning deduplicating.
Log rotation: decide where output goes and cap it, with a RotatingFileHandler using maxBytes and backupCount, a logrotate rule, or plain stdout under a supervisor that already handles rotation. The failure this prevents: a verbose loop writes gigabytes, the disk fills, and the host stops running everything, not just this job.
A documented re-run command: one line in the README next to the schedule, with placeholders for the date or run ID. The failure this prevents: during an incident, nobody can reproduce the exact invocation from memory.
# Re-run the same job for a past date. Upserts make this safe to repeat.
./run.sh --date 2026-03-04 --dry-run # inspect first
./run.sh --date 2026-03-04 # real run, safe to repeat
# Exit codes: 0 ok | 1 unexpected | 2 bad config | 75 retryable
timeout 900 ./run.sh --date 2026-03-04 || rc=$?
case "${rc:-0}" in
0) curl -fsS -m 10 "$HEARTBEAT_URL" >/dev/null ;;
75) echo "retryable failure" >&2; exit 75 ;;
*) echo "needs a human, exit ${rc}" >&2; exit "${rc}" ;;
esac
# /etc/logrotate.d/pipeline
/var/log/pipeline/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
What to do next
- Move every environment-dependent value out of your scripts into one config file, and add a startup check that exits 2 when a required key is missing.
- Add deadlines to every outbound call in your longest-running job. If you cannot name the timeout on a call, it does not have one.
- Write three lines into the README of your most important automation: the schedule, the exact re-run command, and what each exit code means.
- Add a heartbeat ping on success and an alert when the ping is missing. The absent-run alert catches more failures than the error alert does.
- If you are coordinating several agents rather than one cron job, the coordination problem grows: Cowork Pro ($59 one-time) is a dashboard for organising and orchestrating multiple AI agents on real projects, with task routing and run history.
Get Cowork Pro
Cowork Pro โ $59, one-time payment, instant download. See the full breakdown on the review page.
Related reading
- The Ultimate Guide to AI Automation 2026
- How I Built a 10-Product Digital Business
- Building an AI Content Factory
Published by slashman413 โ writing practical, evergreen guides on money, productivity, developer tooling and the web. More about this site โ
Frequently Asked Questions
What is the single highest-value habit to add first?
Alerts on missing runs. Most automation outages are silent non-runs, not crashes, so a heartbeat ping on success plus an alert when it is absent catches failures that error-based alerting never sees.
Do I need a dry-run flag if my job is idempotent?
Yes. Idempotency makes a re-run safe after the fact; a dry run lets you inspect what a changed job would do before it touches live data at all. They solve different problems.
How do I choose timeouts without guessing?
Measure the worst-case latency you have actually seen for each call, add headroom for normal variability, and put the number in config so it can be raised without a code change. A missing timeout is a hang waiting for a quiet moment.
๐ Recommended Tools
๐ Related Articles
- Best AI Tools for Small Business in 2026 (Tested, Budget-Priced)The best AI tools for small business in 2026 โ tested for budget, ease of use, and real ROI. Writing, โฆ
- AI Email Marketing Automation: From Signup to Sale on AutopilotBuild an AI email marketing automation pipeline: capture leads, personalize with AI, send triggered campaigns, โฆ
- What Is Context Engineering? The New Skill After Prompt EngineeringContext engineering explained: giving AI models the right context at the right time. Techniques, examples, and โฆ
- AI Automation for Small Business: The No-Code Guide (2026)How small businesses can automate operations with AI โ no engineers, no code, small budget. Practical playbook โฆ
๐ฌ Free Weekly AI Product Guides
New tools, templates and automation walkthroughs โ plus hands-on updates on the Slashman Tools catalogue. One email a week, zero fluff.
Free forever ยท No spam ยท Unsubscribe anytime ยท Sent instantly