Skip to content

Course · Codex · Automation

Generate the monthly report headless with codex exec

Every turn you’ve watched so far had you standing at the end of it, reading a diff or answering a y/n. This lesson removes you from one specific job - the month-end report - and the honest way to start is by naming what that removal actually costs, not just what it saves. The loop has four beats: read, propose, approve, apply. codex exec still runs three of them directly. What’s gone is the one beat where you were the thing standing in the loop - and the tension for this whole lesson is that everything the approve beat used to catch on the fly now has to be caught before the run ever starts, or not at all.

codex exec (alias codex e) runs the agent non-interactively: it takes a prompt, runs to completion, prints the result, and exits. No chat, no back-and-forth - a command that returns, which the docs describe as the path for scripted or CI-style runs that should finish without human interaction. That’s exactly the shape a month-end report needs:

codex exec "Run the categoriser over last month's transactions, then write a budget report: spend by category, budget vs actual, and every category that went over."

Because it’s an ordinary command-line program, it composes with anything a shell can do - redirect it, drop it in a cron entry, hand it to a CI step. That composability is the whole point of the subcommand, and it’s why the rest of this chapter never has to reinvent the run - only what surrounds it.

Before pricing anything, get the comparison you actually want: what did this class of job cost the last time a human ran it? Go back to your first change - closing one ticket interactively, on a repo you didn’t trust yet, took 4 turns: a plan, an edit, a caught mistake, a fix. That’s the ground truth. Now watch what the same shape of job looks like from outside a codex exec call.

codex exec reads stdin, so you can pipe exactly the slice of data you want it to see:

cat statements/2026-04.csv | codex exec "These are last month's transactions. Categorise them, then total spend per category against the budgets in budgets.toml."

Piping the CSV in has a real cost benefit, not just a style preference: the agent doesn’t have to find the file, only read what you handed it. The read beat still happens - it still has to open the categoriser at src/budgetcli/categorise/rules.py, 512 lines, to know the rules it’s applying, which is 5,120 estimated tokens at 10 tokens per line. Plant that number; it’s the one this trace turns on. The propose beat still happens too, invisibly - there’s no chat window for it to show up in, but the agent still decides a plan before it writes anything. Then apply: the report gets written.

codex exec "Generate April's budget report from the categorised transactions." > reports/2026-04.md

Read, propose, apply. Three beats, present and accounted for, at a cost you can put a number on: 5,120 tokens, paid fresh every month, because a headless run starts exactly as blind as an interactive one does. No approval prompt fired, because nothing in this run reached outside the working directory or the network. That’s not the sandbox saving you yet - that’s just a report that didn’t need saving from.

Where the fourth beat went, and what “1 turn” is hiding

Section titled “Where the fourth beat went, and what “1 turn” is hiding”

Now count turns the way the loop taught you to: how many round trips did this cost? From the calling script’s side, the honest answer is one - one command, one result, done. Compare that against the 4 turns the interactive fix took, and it looks like automation just quartered the cost. It didn’t. It changed what the number is measuring. “Turns” was built to count how many times you had to come back - and if nobody’s coming back, the count collapses to one by definition, not by improvement. A one-turn run and a four-turn run can contain the exact same number of places something quietly went wrong; the instrument just stopped being able to tell you which.

This is the piece that makes codex exec safe to script around at all, and it’s worth naming: fails shut. The command runs to completion and exits with a status code - zero when it finished, non-zero when it couldn’t - and if it reaches for something its sandbox or approval policy doesn’t allow, it has nowhere to pause to. There’s no y/n waiting for an answer that will never come. It just fails.

codex exec "Generate April's budget report." > reports/2026-04.md \
&& echo "report written" \
|| echo "report failed - check the log"

A dead job that exited non-zero is the safe outcome - better than one that quietly waved itself through, and much better than one that hangs forever on a prompt nobody’s there to answer. Fail shut is the whole reason approve can be removed from this loop without the loop becoming reckless by default.

Here’s the honesty this lesson owes you, and it belongs right here, not at the bottom: apply is the point of no return, and the exit code doesn’t cover it. In your first change, the float-bug line got caught in the gap between apply and commit - a human read the diff before it became permanent. That gap doesn’t exist in a headless run. The sandbox decides whether apply is allowed - a permissions question, settled in advance. Nothing after it decides whether apply was right, until the exit code says so, and the exit code only speaks for whatever you actually told it to check. A wrong-but-permitted write - a mis-totalled category, a currency line quietly parsed as a float again - can sit in reports/2026-04.md for a month, unread, correct-looking, wrong. That’s not a crash. It’s a number nobody caught, waiting for the day someone budgets against it.

You now have the report generating itself, and you know exactly which stage in that run nothing is watching. But everything in this lesson still assumes you fired the command. The transaction check needs something else to press the button - a push - and that changes what has to be granted, and when. That’s the next lesson.