The CLI-job contract
After the rise of LLMs and MCPs, CLIs are back in business, and I have started building CLI tooling of late to get my work done. This article is the initial part of my learning of using CLIs with LLMs.
So I ensure now that every batch job that writes — to a database, to object storage, to an external API — ships with five things from the first version itself:
1. --dry-run. The job runs its full selection logic and prints what it would do — “would tag 4,312 creatives, would skip 289 already-tagged, would update 100 alerts” — and writes nothing. You get to review the plan before any damage exists. It’s also the fastest way to catch a wrong filter: when the dry run says 4,312 and you expected 40, the flag caught the bug before it ran.
2. i/N progress logs. processing 1,204/4,312 every few seconds. A long-running job without progress output is untrackable as you can’t tell hung from slow, can’t estimate completion and can’t decide whether to kill it.
3. Idempotent re-run. A lot of times, I have encountered LLM rate limits, bad inputs, incomplete error handling during my runs. I also prefer iterative runs, as highlighted in the gold loop article. To achieve this, I always ensure that the runs are idempotent. This can be a natural key or the image hash, and new runs check these records first before LLM calls.
4. A printed cost estimate before any batch LLM spend. 4,312 items × gemini-2.5-flash ≈ $12.40 — proceed? [y/N]. In LLM batch jobs, a bug costs real money on every wasted invocation. The estimate takes five lines to compute and is printed along with the progress logs. That way, you are always in charge of costs, while the job is running. This would be very helpful in cases like video analysis, where videos are of different lengths. You don’t want to use the same LLM pipeline for YouTube long-form content vs YouTube Shorts. This helps catch issues like that, while the job is running, and not at the end of the run.
5. A record of model used. I always ensure that I write the model name as part of every record (Claude Opus 4.8, Gemini 3.1 Flash, etc.). This enables me to always compare runs with different models, accuracy, costs later in the journey.
Why a contract and not a judgment call
I have made this mistake enough times, where I iterated through so many models in my env, that I forgot what model was used for analysis at what stage. Same with progress logs, where I have to write simple grep calls in tail -f logs to know the progress. (Old habits).
So it has become non-negotiable for me: the five points are part of what “a writing CLI job” means, the way tests are part of what a feature means.