Most teams reach for Apache Airflow when they need to schedule something that runs daily. A few weeks later, they are maintaining a DAG configuration, a Docker environment, and a database connection pool for a job that fetches 200 rows from an API and appends them to a CSV. The infrastructure cost is not proportional to the problem.
Recurring data tasks -- syncs, exports, transforms, health checks -- often do not need a dedicated orchestration platform. They need a scheduled script, a way to detect and log failures, and a place to store the output. Getting those three pieces right without over-engineering the surrounding system is the focus of this guide.
When a Full Pipeline Is Overkill
A workflow orchestrator earns its complexity when you need dependency graphs between tasks, parallel execution across multiple workers, automatic retries with backoff, and a web interface for monitoring run history across dozens of jobs. These are real requirements at a certain scale. They are not the requirements of most businesses running two or three recurring data jobs.
For a single recurring job with no upstream dependencies and one destination, an orchestrator is overhead. The maintenance burden of keeping Airflow, Prefect, or a similar platform healthy in production is non-trivial. It requires container orchestration, persistent storage, worker management, and version control for the pipeline definitions themselves. Teams frequently underestimate this cost when they adopt an orchestrator for their first scheduled job.
The signal that a full pipeline is overkill: if you can describe the entire job in a single function call and the worst-case failure scenario is "the job did not run and someone needs to know," you do not need an orchestrator.
The signal that you do need one: when you have multiple jobs with dependencies between them, when a failure in one job should cascade to block downstream tasks, or when you need to backfill historical date ranges on demand with the same logic that runs on schedule.

Photo by Daryana Vasson on Pexels
Cron as the Scheduling Layer
Cron has been scheduling tasks on Unix-based systems since 1975. For recurring data jobs, it remains the most reliable and portable option available. It requires no additional dependencies, runs at the system level so it survives application restarts, and uses a standard expression format that every operations engineer understands.
A cron expression has five fields covering minute, hour, day of month, month, and day of week. A schedule of 0 6 * * * runs at 6:00 AM every day. A schedule of 0 0 * * 1 runs at midnight every Monday. Crontab Guru provides an interactive expression editor where you can paste any cron expression and receive a plain-English explanation of when it fires, which catches mistakes before they reach production.
One limitation of standard cron: it runs on a single machine, and if that machine restarts between scheduled runs, the job does not run. There is no built-in retry. For jobs that can tolerate a missed run without business impact, this is acceptable. For jobs that cannot, a scheduling service from your cloud provider or a persistent queue with a retry mechanism is more appropriate than plain cron.
Python as the Task Layer
For most data automation work, a Python script is the right abstraction for the task itself. Python's standard library covers file I/O, HTTP requests, CSV and JSON parsing, and process management without any external dependencies. The ecosystem adds clients for every major database, API, and data format.
A well-structured data automation script has a single entry point, takes configuration from environment variables rather than hardcoded values, and writes structured output to a log. Environment variables for configuration keep secrets out of the script and make the same code work across development, staging, and production by changing only the environment. Python's standard library provides everything needed for this pattern.
For relational storage, Python's built-in sqlite3 module handles local data without a server. For production databases with concurrent access, PostgreSQL is a common choice that integrates cleanly with Python's database API.
The key design principle for automation scripts is that running them manually on the command line should produce exactly the same behavior as running them from cron. This makes testing straightforward and eliminates the class of failures where a script works in development but behaves differently under the automated schedule.

Photo by panumas nikhomkhai on Pexels
Error Handling Without an Orchestrator
An orchestrator handles failures automatically, retrying jobs, marking dependent tasks as blocked, and surfacing failures in a dashboard. Without one, you need to build three behaviors explicitly into the script: detect the failure, log it in a structured way, and notify someone who can act on it.
Detection is straightforward. Wrap the main task in error handling and return a non-zero exit code on failure. Cron on Linux can be configured to send email on job failure by adding a MAILTO variable at the top of the crontab with an address that routes to someone on the team. When a job exits with an error, cron sends the stderr output to that address.
Structured JSON logging makes failures searchable without a dedicated log aggregation platform. Writing each log line as a JSON object means you can filter failures with a simple grep, count error types over time, and ship the logs to any aggregation service later without modifying the script.
"The most reliable automation systems we build for clients are usually the simplest ones: a Python script that knows what it does, writes structured logs, and exits with a non-zero code when something goes wrong. The monitoring and alerting layers attach to those exit codes and log lines. You do not need an orchestrator to build that chain." - Dennis Traina, 137Foundry
Data Storage Decisions
For small recurring jobs, three storage options cover most cases. Local files work for jobs that produce exports consumed by another system. JSON, CSV, and Parquet are the most portable formats. Append a datestamp to each output filename and implement a rotation policy that removes files older than your retention window.
SQLite handles jobs that need queryable storage without a database server. The SQLite module is built into Python's standard library and stores a full relational database in a single file. For jobs producing data on a single machine, SQLite outperforms a remote database by eliminating network latency entirely and requiring no server management.
A managed database makes sense when multiple jobs write to shared tables, when applications consume the data directly, or when you need concurrent read access from multiple services. Adding a managed database before these requirements exist is premature complexity that adds operational surface area without providing proportional value.
The storage choice affects failure recovery. File-based storage fails cleanly: a failed write produces an incomplete file that is easy to detect and discard on the next run. Database writes can partially succeed, which requires explicit transaction boundaries to prevent corrupted state.

Photo by Elina Emurlaeva on Pexels
Testing Before You Trust It
A scheduled script you cannot test manually is a script you cannot debug effectively. Before relying on a cron job in production, verify that the script runs correctly when invoked directly on the command line with the same environment variables that cron will use.
The most common reason a script works on the command line but fails in cron is the PATH. Cron runs with a minimal environment, and commands that resolve correctly in your shell session may not resolve under cron's reduced environment. The simplest fix is to use the absolute path to the Python interpreter in the cron entry itself. Redirecting both stdout and stderr to a log file in the cron entry captures everything the script prints, including exceptions, so failed runs do not disappear silently.
After the first few successful runs, check the log file to verify the output looks correct and that runtime matches expectations. A sync that takes 30 seconds in development may take several minutes under the load profile of a production machine. Verifying the actual runtime before setting the schedule interval prevents jobs from overlapping, which can produce duplicate records or corrupt partial writes.
When You Actually Need Airflow
The boundary where lightweight automation stops being sufficient has clear markers. You need a dedicated orchestrator when jobs have dependencies between them and a failure in one should block downstream tasks, when you need to run logic across a backfill of historical dates, or when multiple engineers need visibility into run history and the ability to manually trigger reruns.
Apache Airflow is the standard choice for production data engineering teams at this scale. The operational overhead is real, but it matches the operational requirements when those requirements genuinely exist. The mistake is reaching for it before they do.
A team that starts with a Python script and a cron job has something working in an afternoon. Migrating to a proper pipeline when requirements outgrow the simple setup is a clear, scoped project. Starting with Airflow before you understand how the job behaves in production means maintaining infrastructure that is larger than your current problem, and paying that maintenance cost every week.
Getting Started
The data automation and integration services at 137Foundry cover the full range from lightweight scheduled scripts to full pipeline architecture. The AI automation practice at 137Foundry adds an intelligence layer to data tasks where that fits the problem. The 137Foundry services overview describes where each approach applies.
A production data sync that runs reliably on cron and writes logs you can actually read is more valuable than a pipeline running on a platform that requires dedicated engineering time to maintain. Start with the simpler system and let real requirements drive the upgrade.