Python Automation Scripts skill for Codex

Scripts that are safe to run twice, and at 3 a.m.. Codex turns manual chores into Python scripts that separate planning from doing, default to a dry run, skip work already done, write files atomically and lock against overlapping runs. Dependencies are declared inline for uv, secrets come from the environment, and the script is scheduled with cron, launchd or a systemd timer only after you approve the entry.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- python-automation-scripts

Installs to ~/.agents/skills/python-automation-scripts/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $python-automation-scripts, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes standalone Python automation scripts that are safe to run unattended - dry-run by default, idempotent, logged, locked against overlap, configured by flags and environment variables, with PEP 723 inline dependencies run through uv - and schedules them with cron, launchd or systemd timers. Use when asked to automate a repetitive task with Python, turn a manual chore into a script, glue two tools or APIs together, or harden a fragile one-off script before putting it on a schedule.

Use it when

  • A repetitive task should become a script you can rerun safely
  • A quick script is about to be put on cron and needs hardening
  • Two tools or APIs need glue code that runs unattended

Use something else when

  • You are building a distributable command-line tool with subcommands: use python-cli-tools
  • The task is mostly shell commands strung together: use bash-scripting

How to check the result

Run with the dry-run default and review the planned changes, apply once, then run again and confirm the second run reports nothing to do and exits zero.

The mistake it prevents

Relying on the terminal's working directory and PATH, so the script works by hand but fails silently under cron.

~/.agents/skills/python-automation-scripts/SKILL.md

--- name: python-automation-scripts description: Writes standalone Python automation scripts that are safe to run unattended - dry-run by default, idempotent, logged, locked against overlap,… license: MIT ---

An automation script is done when a second run does no harm, a crash halfway leaves nothing half-written, --dry-run shows exactly what would change, the log explains what happened without reading the code, and the exit code tells the scheduler whether it worked.

Before writing

  1. Read AGENTS.md and any existing scripts/ folder: match the repo’s Python version, formatter and dependency tool.
  2. Write down the contract in three lines: input (files, API, folder), effect (what changes where), and how often it runs.
  3. Prefer the standard library. Each third-party dependency is something that can break on a scheduled machine. Common justified ones: httpx, openpyxl, python-dotenv.
  4. Identify side effects that are destructive or outward-facing (deleting, overwriting, sending email or messages, posting, paying). Those require --apply (or --confirm) and must be shown to the user in a dry run before the first real run.

Single-file scripts with uv

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx>=0.27"]
# ///

uv run script.py installs the declared dependencies into a cached environment (needs network the first time; in a Codex sandbox with network off, ask for approval or use the project’s existing environment). Add dependencies with uv add --script script.py httpx. Without uv, a requirements.txt next to the script plus a venv is fine; never install into the system Python.

Skeleton

"""Archive processed invoices from inbox/ into archive/YYYY/MM and log a summary."""
from __future__ import annotations

import argparse, fcntl, logging, os, shutil, sys
from datetime import datetime
from pathlib import Path

log = logging.getLogger("archive_invoices")

def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("--src", type=Path, default=Path(os.environ.get("INVOICE_SRC", "inbox")))
    p.add_argument("--dst", type=Path, default=Path("archive"))
    p.add_argument("--apply", action="store_true", help="make changes (default: dry run)")
    p.add_argument("-v", "--verbose", action="store_true")
    return p.parse_args(argv)

def plan(src: Path, dst: Path) -> list[tuple[Path, Path]]:
    moves = []
    for f in sorted(src.glob("*.pdf")):
        ts = datetime.fromtimestamp(f.stat().st_mtime)
        target = dst / f"{ts:%Y}" / f"{ts:%m}" / f.name
        if not target.exists():                       # idempotent: skip done work
            moves.append((f, target))
    return moves

def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
                        format="%(asctime)s %(levelname)s %(message)s")
    lock = open("/tmp/archive_invoices.lock", "w")
    try:
        fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)   # POSIX; see Windows note below
    except BlockingIOError:
        log.warning("another run is in progress; exiting"); return 0
    if not args.src.is_dir():
        log.error("source %s does not exist", args.src); return 2
    moves = plan(args.src, args.dst)
    for src, dst in moves:
        log.info("%s %s -> %s", "MOVE" if args.apply else "WOULD MOVE", src, dst)
        if args.apply:
            dst.parent.mkdir(parents=True, exist_ok=True)
            shutil.move(src, dst)
    log.info("%d file(s) %s", len(moves), "moved" if args.apply else "to move (dry run)")
    return 0

if __name__ == "__main__":
    sys.exit(main())

The shape to keep: plan() computes everything without side effects, main() applies it, and main(argv) is callable from a test.

Rules that prevent 2 a.m. surprises

ConcernRule
Partial writesWrite to path.with_suffix(".tmp"), then os.replace(tmp, path) (atomic on one filesystem)
Pathspathlib.Path everywhere; resolve relative to Path(__file__).parent or flags, never the cwd of cron
SecretsFrom environment or the OS keychain (keyring); never in code, flags or logs
Subprocessessubprocess.run([...], check=True, capture_output=True, text=True, timeout=300); never shell=True with interpolated input
HTTPAlways a timeout; retry 429/5xx with backoff; checkpoint progress for long batches
TimeUse timezone-aware datetimes (datetime.now(tz=ZoneInfo("Europe/London")))
Exit codes0 success or nothing to do, 1 partial failure, 2 bad input/config
Logginglogging, not print; one summary line at the end with counts
OverlapFile lock (POSIX fcntl.flock; on Windows use msvcrt.locking or a lock-file library)

Batch work with partial failure

Process items independently, collect failures, keep going, and report:

failed: list[tuple[str, str]] = []
for item in items:
    try:
        handle(item)
    except Exception as exc:            # narrow this when you know the failure modes
        log.exception("failed on %s", item.id)
        failed.append((item.id, str(exc)))
if failed:
    log.error("%d of %d failed: %s", len(failed), len(items), [i for i, _ in failed])
    return 1

For long jobs, persist a checkpoint (a JSON file of completed ids, or the output itself) after each item so a rerun skips finished work.

Scheduling

PlatformMechanismNotes
Linux serversystemd timer + servicePersistent=true catches up missed runs; logs in journalctl -u name
Linux/macOS, simplecrontabMinimal PATH; use absolute paths to uv/python; redirect output to a log
macOS desktoplaunchd agent in ~/Library/LaunchAgentsRuns missed jobs after wake; load with launchctl bootstrap gui/$(id -u) file.plist
WindowsTask SchedulerSet “Start in” directory explicitly
Repo choresGitHub Actions schedule:See $github-actions-automation

Cron example: 15 6 * * 1-5 cd /opt/jobs && /usr/local/bin/uv run archive_invoices.py --apply >> /var/log/archive.log 2>&1. Installing a crontab, timer or launchd job changes the user’s machine: show the entry and get confirmation first. For overlap, timezone and monitoring detail, see $cron-scheduling.

Testing

  • Unit-test plan() with tmp_path (pytest) on a fake folder tree; assert the planned moves, then assert a second plan() after applying returns nothing (idempotency).
  • Mock the network at the client boundary (httpx.MockTransport) rather than patching internals.
  • Run once with --dry-run on real data and show the user the output before --apply.

Gotchas

  • Cron runs with a different HOME, PATH and no shell profile: what works in the terminal fails on schedule. Log sys.executable and os.getcwd() at startup when debugging.
  • shutil.move across filesystems copies then deletes; an interruption can leave both. Verify the copy before removing the source in cross-device moves.
  • glob("*.PDF") is case-sensitive on Linux, insensitive on macOS default volumes.
  • Laptops sleep: cron skips jobs missed while asleep; launchd and systemd timers can catch up.
  • Mail and chat notifications from scripts are outward-facing; confirm the recipient list with the user before enabling them.

Handoff note

**Script:** scripts/archive_invoices.py (Python 3.11, stdlib only)
**Run:** `uv run scripts/archive_invoices.py` (dry run) / `--apply` to move files
**Config:** INVOICE_SRC env var or --src; --dst defaults to ./archive
**Schedule:** launchd agent com.acme.archive-invoices, weekdays 06:15 (not yet installed)
**Exit codes:** 0 ok, 1 some files failed, 2 bad config
**Tested:** pytest tests/test_archive_invoices.py (plan, idempotency, missing source)

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Python Automation Scripts"
  short_description: "Scripts that are safe to run twice, and at 3 a.m."
  default_prompt: "$python-automation-scripts Write a Python script that moves processed invoices into year/month folders every morning"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem