Skip to content

It works when I run it, but fails on a schedule (Windows, Mac, Linux)

Why scripts that run fine by hand break under Task Scheduler, launchd or cron, and a checklist that finds the cause: identity, paths, environment, permissions and logging.

intermediateUpdated 2026-11-02

A script works perfectly when you run it, then does nothing (or fails silently) once a scheduler runs it. There are two very different failures hiding behind that symptom, so first work out which one you have:

  • It never launched. The trigger didn’t fire, or a condition stopped it.
  • It launched and failed. Usually the scheduler ran it as a slightly different “you”: a different account or session, starting folder, environment or permissions, with nowhere to show errors.

0. Did it launch at all?

  • Windows: check the task’s History tab and Last Run Result, as Microsoft’s troubleshooting guide suggests. Then check the Conditions and Settings tabs. “Start only if on AC power”, “only if idle”, “only if a network is available” and “If the task is already running…” can each stop a run without any error in your script.
  • macOS: launchctl print gui/$(id -u)/<Label> (use the Label from your plist) shows whether the agent is loaded and its last exit code. For a LaunchDaemon, use sudo launchctl print system/<Label>. A sleeping Mac also doesn’t run calendar jobs at the exact time.
  • Linux: look for the run in the system log (journalctl -u cron or grep CRON /var/log/syslog, depending on the distro).

If there’s no sign it ever started, fix the trigger, conditions or loaded job first. Nothing inside your script matters yet.

1. Capture what happens

You can’t fix what you can’t see. Make sure the log folder exists and that the scheduled account can write to it.

Windows (PowerShell):

  • Capture output at the launcher, so even startup errors are recorded. One way is a wrapper .cmd the task runs: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\job.ps1" >> "C:\Scripts\job.log" 2>&1.
  • Inside the script, Start-Transcript -Path C:\Scripts\job.log -Append helps too. Microsoft notes, though, that a transcript doesn’t record everything, and it can’t see failures that happen before that line runs.

macOS (launchd):

  • Add StandardOutPath and StandardErrorPath to the plist. Editing a plist that’s already loaded doesn’t change the running job, so reload it:

    launchctl bootout gui/$(id -u)/com.example.myjob
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
  • On Apple Community, etresoft recommends launchctl print to see what launchd thinks happened.

Linux (cron): redirect inside the crontab line:

*/15 * * * * /home/me/bin/job.sh >> /home/me/job.log 2>&1

2. Who is it running as?

  • Windows: “Run only when user is logged on” runs in your session. “Run whether user is logged on or not” runs in a separate, non-interactive session.
    • In a Microsoft Q&A thread where a batch file failed only with the second option, the accepted answer explains that this session may not see network shares the user has mounted.
    • If the task needs network access, the account it runs as must be able to authenticate to that resource on its own. Storing the account’s password (“Do not store password” unticked) is what lets it do that.
  • macOS: a LaunchAgent runs as you, inside your login session. A LaunchDaemon runs outside it. Apple’s launchd guide notes that an agent runs only while its user is logged in.
  • Linux: a user’s crontab runs as that user. Entries in /etc/crontab name the user explicitly.

3. Check exactly what gets launched (Windows especially)

In the task’s Action:

  • Program/script should be just the interpreter: powershell.exe, or pwsh.exe for PowerShell 7, which is a different program with separate settings.

  • Add arguments holds the rest, with quotes around any path containing spaces:

    -NoProfile -ExecutionPolicy Bypass -File "C:\My Scripts\job.ps1"
  • -NoProfile makes runs predictable. Whether a PowerShell profile loads depends on how PowerShell is started, not on the scheduler.

4. Paths: explicit, and not tied to your session

  • Mapped drive letters (Z:) belong to one logon session. Microsoft explains that drive mappings are created per logon session and aren’t shared between sessions. Use UNC paths (\\server\share\folder) instead.
    • A UNC path only fixes the name. The scheduled account still needs permission on the share, and any VPN still has to be connected.
  • Don’t rely on the “current folder”. Set Start in on the task’s action (or -WorkingDirectory in New-ScheduledTaskAction), or use full paths. Normal local paths like C:\Scripts\job.ps1 are fine.
  • macOS launchd: ProgramArguments are passed straight to the program, not through a shell, so ~ and $VARIABLES aren’t expanded. The working agent in the Apple Community thread used full paths only.
  • cron: the command line is run by a shell (/bin/sh), so ~ usually works there. Watch %, though: the crontab(5) man page explains that an unescaped % becomes a newline, which silently breaks date +%Y-%m-%d unless you write \%.

5. Environment: your interactive setup isn’t there

Your terminal has loaded .zshrc or .bashrc, which set PATH, aliases and variables. Unix schedulers don’t run your interactive shell setup:

  • cron: the crontab(5) man page lists what cron sets for you: SHELL=/bin/sh, plus HOME and LOGNAME from your account. Anything else must be set explicitly, either as NAME=value lines at the top of the crontab or inside the script.
  • launchd starts the program directly. Set variables with the plist’s EnvironmentVariables key, or in the script.
  • Shells:
    • cron’s /bin/sh only parses the crontab command line. A script you call directly runs with the interpreter in its shebang (#!/bin/bash, #!/bin/zsh).
    • Running it as sh script.sh overrides the shebang, and bash-only syntax then breaks. Either make the script executable and call it by full path, or call the interpreter explicitly: /bin/bash /home/me/bin/job.sh.
  • Everywhere: call tools by full path (/usr/bin/rsync, /opt/homebrew/bin/jq, C:\Program Files\...).

6. Permissions and privacy

  • macOS privacy protection is a common cause when the script touches protected data. In the Apple Community thread, the poster’s job only worked after the script moved out of ~/Documents, and etresoft pointed to Full Disk Access.
    • First try keeping the script and the files it uses outside protected folders like Documents and Desktop.
    • If it truly needs protected data, grant the narrowest access, to the specific app or interpreter that runs the job, under System Settings → Privacy & Security.
    • Granting Full Disk Access to a general-purpose shell gives every script it runs that power.
  • Windows: “Run with highest privileges” only matters if the script needs admin rights. It doesn’t grant access to files or shares the account can’t already reach.
  • Linux:
    • The script must be executable (chmod +x).
    • The user needs read access to its inputs, search (x) permission on every parent folder in each path, and write access only where it writes output.

Quick test that isolates the problem

Microsoft’s guide suggests testing the script directly first, then through the scheduler. To narrow it down:

  1. Keep your real script safe, and point the job at a two-line test script that writes the date and whoami to a log in a folder you’ve confirmed is writable.
  2. Let it run from its real trigger at least once. Starting it by hand doesn’t test the trigger.
  3. The test fails: compare against step 0 (did it launch?) and step 3 (what gets launched). The test works: your real script’s own needs are the problem, so add it back one step at a time with logging on.

Sources