From Script to Schedule: Jobs That Run Themselves
Your transfer script works. It has worked every morning for a month — you type the command, watch the log lines scroll, glance at the result, get on with your day. So you give it to the scheduler, set it for 02:10, and go home. And on its very first night alone, it fails in a way it has never failed before: not the network, not the partner, but something baffling like sftp: command not found — on a machine where sftp is obviously installed.
Welcome to the most instructive jump on the automation ladder. A script that runs attended and a job that runs unattended are different animals, because the script was quietly leaning on you: your environment, your account, your ability to answer a prompt, your eyeballs on the output. This article catalogs everything that changes when nobody is watching, shows how to hand a script to cron and to Task Scheduler correctly, and covers the two disciplines that stop being optional the moment you walk away — logging and monitoring. It is the fourth article in our Automation Ladder series, and it assumes you have a script worth scheduling, built the way your first scripted transfer describes.
The Gap Between "Works for Me" and "Works Alone"
Picture your interactive session as a warm, furnished room. It has your environment variables, your PATH, your home directory with your SSH keys and your known_hosts file, your mapped drives, your locale, and — most importantly — you, ready to notice anything odd and type answers to anything that asks. Your script grew up in that room and absorbed its comforts without either of you noticing.
A scheduler runs the same script in a bare concrete cell. The environment is minimal or different. The account is often not yours. There is no terminal, so anything that tries to prompt does not get an answer — it waits forever. And there is no human, so anything that goes wrong is simply... gone, until someone looks. Nothing about the script changed; everything about its world did. The failures this produces are so consistent across platforms that you can list them in advance — which is exactly what the next section does, so you can fix each one before the first unattended night instead of after it.
Everything That Breaks When Nobody Is Watching
The environment shrinks
Interactive shells load startup files that set dozens of environment variables; schedulers do not. Cron famously provides a near-empty environment with a minimal PATH — often just /usr/bin:/bin — which is why a script that calls a tool living in /usr/local/bin works for you and dies at 02:10 with "command not found." On Windows, a task running as another account gets that account's environment, not yours, and may run without loading a user profile at all, so variables and per-user settings you rely on are absent.
The fix is a habit, not a trick: a scheduled script assumes nothing from its environment. Call programs by absolute path or set PATH explicitly at the top of the script. Reference every file and folder by absolute path — never assume the working directory, because the scheduler decides that, not you. If the script needs a proxy variable or anything similar, set it in the script.
The account changes
By hand, the script ran as you. Scheduled, it should run as a dedicated service account — an identity created for the job, with only the permissions the job needs, per service account hygiene. That is the right design, and it breaks three things you set up as yourself: the SSH private key sitting in your home directory, the host key recorded in your known_hosts, and any folder permissions granted to your account. All three must be re-established for the service account: its own key (registered with the partner), its own verified known_hosts entry — see host keys and known_hosts — and explicit permissions on every path the job touches.
Windows adds a classic trap of its own: mapped drive letters do not exist in non-interactive sessions. The X: drive you see is a convenience of your logged-in session; the scheduled task running as the service account has never heard of it. Use full UNC paths (\\fileserver\export\outbox) in anything a task will run.
Prompts become deadlocks
Every interactive question is now a hang. A password prompt because the key was not found; a host-key confirmation because this account has never connected before; an "overwrite? (y/n)" from a copy command — with no terminal attached, each of these stops the job cold, silently, sometimes leaving it "running" in the scheduler for days. Hunt prompts down before scheduling: connect once as the service account to settle the host key, use BatchMode=yes so authentication fails loudly instead of prompting, and pass the force/quiet flags that tell each tool never to ask. A useful smoke test: run the script with its input redirected from nothing — ./upload_daily_report.sh < /dev/null — and anything that was going to prompt will fail immediately instead of waiting.
The world at 02:10 is different
Your morning runs enjoyed a fully awake network: VPN up, DNS warm, the export finished hours ago. The small hours have their own weather. The export job may still be running at 02:10 on month-end — so the file is missing or, worse, half-written. Backup jobs may be saturating the link. A nightly reboot window may have just restarted the machine, and the job fires before the network or a dependency service is ready. None of these are exotic; all of them are invisible until the job runs at the real hour. Two defenses: order the schedule around upstream reality (ask what time the export finishes on its slowest day, then add margin), and keep the preflight checks strict so a missing or empty file produces a clean, logged failure rather than a garbage transfer. If you also operate the receiving side on Windows, run the server as a service rather than an app in someone's session — a server like Sysax Multi Server runs as a Windows service precisely so it is listening after every reboot with nobody logged in.
Everything above compresses into one pre-flight checklist. Run it before the first scheduled night — every line is a real 2 a.m. failure someone has already had:
GOING-UNATTENDED CHECKLIST
[ ] Every program called by absolute path, or PATH set in the script
[ ] Every file and folder referenced by absolute path (UNC, not drive letters)
[ ] Script tested with: ./script.sh < /dev/null (no hidden prompts)
[ ] Runs as the service account, not me (su/runas test performed)
[ ] Service account has: its own key, its own known_hosts entry,
permissions on every path the job touches
[ ] BatchMode=yes (or equivalent): auth failure is loud, never a prompt
[ ] Schedule set AFTER the upstream file is reliably ready (worst day + margin)
[ ] Preflight check rejects missing/empty input with a logged error
[ ] Log file path is absolute, and the service account can write to it
[ ] Failure is visible: nonzero exit reaches the scheduler (next section)
Giving the Job a Clock: cron and Task Scheduler
With the script hardened, the scheduling itself is the easy part — one line or one command per platform. On Unix-like systems, the scheduler is cron, and jobs live in a crontab (the table of scheduled commands). Edit the service account's crontab with crontab -e and add:
10 2 * * 1-5 /usr/local/bin/upload_daily_report.sh >> /var/log/transfers/acme_daily.cron.log 2>&1
The five fields before the command are minute, hour, day of month, month, and day of week — so this reads "at minute 10 of hour 2, any date, any month, Monday through Friday" (days 1–5; 0 is Sunday). The redirection matters as much as the timing: >> appends the job's standard output to a file, and 2>&1 sends standard error to the same place, so nothing the script or sftp prints can vanish. Without it, cron tries to mail the output to the local account — a destination nobody reads on most systems. Cron has more personality than one paragraph can cover (environment quirks, staggering, testing the way cron runs things); the bash and cron series gives it the full treatment.
On Windows, the scheduler is Task Scheduler, and the same job is created from an elevated prompt like this:
schtasks /create /tn "ACME daily report upload" ^ /tr "C:\transfers\upload_daily_report.cmd" ^ /sc weekly /d MON,TUE,WED,THU,FRI /st 02:10 ^ /ru CORP\svc-transfer /rp *
Reading the flags: /tn names the task (make it say what and whose — this name is what a colleague sees in the task list), /tr is the action to run, /sc weekly with /d MON,TUE,WED,THU,FRI gives the weekday schedule, /st sets the start time, /ru sets the account the task runs as — the service account — and /rp * makes the command prompt for that account's password rather than putting it on the command line (where it would linger in shell history). Creating the task this way registers it to run whether or not anyone is logged on, which is exactly what a transfer job needs. The same options exist in the Task Scheduler GUI — the checkbox "Run whether user is logged on or not" is the one that matters. Scheduler mechanics on both platforms, including missed-run behavior after reboots and patches, get a full series of their own in scheduled jobs done right.
Remember: test the job the way the scheduler will run it, not the way you run it. One-off run commands exist for exactly this — schtasks /run /tn "ACME daily report upload" fires the task now, under the task's account and settings. If it only works when you launch it from your own shell, it does not work.
Why Logging Stops Being Optional
While the script was attended, the log was a courtesy; your eyes were the real record. Unattended, the log is the only witness. When Thursday's file is missing, the log is how you distinguish "the job never ran" from "it ran and found nothing to send" from "it sent the file and the partner lost it" — three different problems with three different fixes, indistinguishable without evidence.
A scheduled transfer job's log has to answer four questions for every run: when did it start, what did it decide to transfer, what happened, and how did it end. Timestamps on every line, the file name and size, the client's own output captured, and the exit status recorded. The wrapper script from the previous article already does this; the crontab line above adds the belt-and-suspenders layer, catching anything that escapes to standard output or error. A healthy night and an unhealthy night should be obviously different at a glance:
Mar 13 02:10:01 START uploading report_YYYYMMDD.csv to sftp.example.com
Mar 13 02:10:04 OK upload complete
Mar 13 02:10:04 DONE local copy moved to sent/
Mar 14 02:10:01 ERROR report_YYYYMMDD.csv missing or empty in
/data/export/outbox - nothing sent
That Mar 14 entry is a success story, not a failure story: the job met a bad situation, refused to make it worse, and wrote down exactly what a responder needs. Deciding what belongs in transfer logs — and what must never be logged, like credentials — is covered in what to log. One more habit worth adopting on day one: pick a log location with room to grow and rotate the files, because a job that runs nightly for years writes a lot of history, and a full disk is itself a classic cause of 2 a.m. failures.
The First Monitoring a Scheduled Job Needs
Logging records what happened; monitoring makes sure a human finds out. The minimum viable setup is three layers, in order of how little they cost.
Layer one: the exit code must reach the scheduler. Your script already exits nonzero on failure; make sure nothing swallows it. If a wrapper batch file or shell layer sits between the scheduler and the script, it must pass the status through (in a Windows .cmd wrapper, end with exit /b %ERRORLEVEL%). This is what makes the scheduler's own records — cron's mail, Task Scheduler's Last Run Result column — mean something.
Layer two: failure becomes a message. Nobody reads scheduler columns daily, so wire a notification: the simplest portable pattern is a final step that sends a short email when the run failed, with the job name, host, and the last few log lines in the body. Ugly and effective. This is also where configuration-based tools earn their keep — Sysax FTP Automation pairs its built-in scheduler with email notifications, so a failed or successful task can mail the operators without you building the plumbing.
Layer three: watch for absence, not just failure. The sneakiest scheduled-job problem is the run that never happens — the task got disabled during maintenance, the machine was off, the schedule was edited wrong — or the run that "succeeds" while transferring nothing night after night. No failure alert will ever fire, because nothing failed. The antidote is a freshness check: something that independently asks "did today's file actually land where it should?" and alerts when the answer is no. Even a second tiny scheduled job that checks the sent folder's newest timestamp is enough to start. This absence-detection mindset is the heart of the transfer job monitoring series.
The Attended-to-Unattended Differences, Side by Side
The table condenses the whole transition into one reference you can review before any script meets any scheduler.
| In your session | Under the scheduler | What to do |
|---|---|---|
| Rich environment, full PATH | Minimal or different environment | Absolute paths; set PATH in the script |
| Runs as you, with your keys | Runs as the service account | Account gets its own key, known_hosts, permissions |
| Prompts get answered | Prompts hang forever | BatchMode, quiet flags, test with input closed |
| Mapped drives available | Drive letters do not exist | UNC paths everywhere |
| You see every failure | Failures are silent by default | Log everything; alert on failure and absence |
| You run it when things are ready | The clock fires blind | Schedule after worst-case readiness; strict preflight |
The First Unattended Week
Even a hardened job deserves a supervised transition, and a calm sequence beats a leap of faith.
- Fire it through the scheduler while you watch. Use the scheduler's run-now facility, then read the entire log and check the scheduler's recorded result. This catches the environment and account problems in daylight.
- Schedule the real time, and check the next morning. For the first few days, the morning check is part of the job: log says OK, file is in sent/, partner confirms arrival. You are doing manually what the freshness check will do forever.
- Break it once, on purpose. Rename the source file some evening and confirm the whole failure chain works at night: ERROR in the log, nonzero result in the scheduler, notification received in the morning. A monitoring path that has never fired is a rumor, not a system.
- Watch one boundary day. The first Monday (weekend gap), the first month-end (bigger file, slower export), the first patch-window night. Boundary days are where schedules meet reality.
- Then stop checking. This is the point of the whole exercise. After a clean week including a proven failure drill, the morning ritual ends — the log, the alerts, and the freshness check have taken over. A job you still check by hand every day is not finished being scheduled.
One more thing before you file this flow under done: give a thought to what happens if a run overlaps itself — tonight's job still running when tomorrow's fires, after a huge file or a slow network. For a nightly job with normal volumes the risk is low, but the fix (a lock that prevents a second copy from starting) is cheap insurance, and the patterns live in the bash and cron series.
The Quiet Milestone
The first morning you realize the transfer happened without you — and that you can prove it from the log without leaving your chair — is a real milestone. The flow now runs on the business's clock instead of yours, fails loudly instead of silently, and no longer depends on anyone's memory. Record the job while the details are fresh: name, machine, schedule, account, script path, log path, one line of purpose. That entry is the seed of the estate-wide discipline in the automation inventory, and it costs two minutes now versus an afternoon of archaeology later.
From this rung, the ladder continues upward for the flows that need it: moving up to event-driven transfers replaces the fixed clock with reaction to arriving files — worth it for some flows, overkill for many. And whichever rung each flow settles on, the placement logic in the maturity stages helps you decide deliberately rather than by momentum.
Frequently Asked Questions
My script works when I run it but fails from cron. Where do I start?
Why did my scheduled job hang for days without failing?
Should the job run as my account or a service account?
Is emailing myself on failure really enough monitoring?
What time should I schedule a nightly transfer?
From the Sysax team: we build secure file transfer software for Windows — Sysax Multi Server, an FTP, FTPS, SFTP, and HTTPS server, and Sysax FTP Automation for scheduled, scripted transfers. Free trials are on the download page.
