vegman.dev
All posts

The script does, the model thinks

2026-03-05article

The same payment error message arrived every thirty minutes. Not once a day, not on a new release - exactly every half hour, around the clock, unchanged. A job whose entire purpose was to glance at the queue of finished material and send a short notification had by then stopped checking anything at all, but it kept dutifully knocking and burning money.

That job was mine, and I was also the one who put it together wrong. If you're building your first automation right now and putting a language model - a program that produces a text response from a text request - at the center by default, this article is about not repeating my approach. By the end you'll be able to decide in about a minute whether a model belongs in a given task, and why in a fair share of cases an ordinary script - a small program that runs a fixed sequence of actions - is enough.

The story: how a routine check ate the whole budget

The mechanics were simple. The scheduler - the service that runs a program on a timetable - triggered the job every half hour. The job looked at the queue of finished material and, if something had shown up, sent a notification. That's it.

But it was built as a full-blown agent. An agent is a program that calls a language model and hands it a set of tools (read data, go to storage, send a message), and the model itself decides which tool to call and in what order. Sounds solid. In practice, all the work this job did came down to two actions: read the data and drop it into a fixed text template. Not a single place where anything had to be decided.

Now about what I paid for that.

A model's work is counted in tokens - a unit of text, roughly a piece of a word, and that's what the billing is based on. Every call to a model specifies a ceiling: the maximum number of tokens it may produce in its response. My run requested the model's maximum ceiling rather than what was actually needed - and what was needed was three lines of notification. And here's the key point I learned the expensive way: the provider checks whether you can pay against the requested ceiling, not against how many tokens are actually spent. Which means that every half hour the job showed up and asked to reserve, for its response, the maximum the model is capable of.

While there was a cushion on the balance, none of this showed. When the balance dropped, a trivial check started failing with a payment error. Not because it lacked the money to do its work - the work cost pennies - but because it was asking for a ceiling the balance no longer covered. And it failed every thirty minutes, endlessly, until I switched it off by hand.

The fix took less time than the diagnosis. I rewrote the same job as an ordinary script: read, compare, fill in the template, send. No model, no tools, no agent. The cost per run became zero, the behavior predictable.

flowchart TB
  subgraph M["С моделью"]
    M1["Запуск по расписанию,<br/>48 раз в сутки"] --> M2["Вызов модели<br/>на каждую проверку"] --> M3["Платно за каждый запуск,<br/>поведение непредсказуемо"]
  end
  subgraph S["Скриптом"]
    S1["Тот же запуск<br/>по расписанию"] --> S2["Прочитать, сравнить,<br/>подставить в шаблон"] --> S3["Стоимость запуска - ноль,<br/>поведение одинаково"]
  end
  M ==> S
Одна и та же задача по расписанию: с моделью и без нее

Why this happens: what you pay for convenience

A model doesn't end up in a job like this out of malice. It ends up there because with it you don't have to think anything through in advance. You don't feel like describing every branch - the model will sort it out. The data format might change - the model will understand. That's genuine convenience, and sometimes it even pays for itself. But it comes with a bill, and here's what makes it up.

  • The price has nothing to do with the difficulty of the task. Checking "is there anything in the queue" costs about the same as substantive work. You pay for the call, not for the value.
  • Reservation goes by the ceiling, not by actual use. A tiny response can run into a limit sized for a big one.
  • You acquire an external point of failure. A script depends on your data. A job with a model also depends on someone else's service, on your balance, and on their rules. And the failure doesn't arrive at deploy time - it arrives weeks later, when you've already forgotten what's inside.
  • Behavior stops being reproducible. On the same data you can get slightly different text. For a notification that's a trifle; for a check someone relies on, it isn't.
  • The job is hard to test. You can't write an assertion that "this input gives exactly this output" if the output is a little different every time.
  • Everything breaks when you reword something. The prompt - the model's text instruction - becomes part of the logic, but doesn't behave like code: you rewrite a paragraph to read better, the behavior shifts, and the compiler won't warn you.

Worth remembering separately: a recurring job amplifies any mistake. A one-off script fails once, and you see it. A scheduled job will fail forty-eight times a day, and spend money forty-eight times a day just the same, if the failure costs money.

The rule I now apply before writing any code

The rule is short, and I apply it before I open the editor.

If a task comes down to reading data, comparing, and fixed formatting - it's a script. No exceptions.

The three verbs work as a filter here. Reading - take the data from where it sits. Comparing - check a condition: empty or not, more or less, changed or the same. Fixed formatting - put values into a text template written in advance. If all the work breaks down into these three actions, a model has nothing to do there: it will add nothing but cost and unpredictability.

The easiest way to check yourself is with one question: is there a step in this task that I can't write as an if condition? If every step can be written - write a script. If there's a step that needs a judgment of "does this fit by meaning or not" - that, and only that, is where a model appears.

Hence the phrasing I use as a reminder: the script does, the model thinks. Action is code. Judgment is a model.

The rule has a pleasant side effect that makes it worth following even if everything were free. A script has reproducible behavior: the same input gives the same output. Which means you can cover it with tests and confirm once that it does exactly what's needed. And it won't break because someone reworded a prompt.

The second habit: empty output means silence

There's one more thing I now build in from the start. A recurring job that finds nothing on most of its runs should produce empty output. Not "nothing found," not "check complete, no changes" - empty.

Running every half hour, that's the difference between silence and forty-eight messages a day that you'll stop reading within a week. And once you stop reading the stream, you'll also miss the one message the whole thing was built for. A notification should mean an event. No event means silence.

Where a model really is needed

The rule isn't that models aren't needed. It's that they should stand where you can't do without them. And you can't do without them where there's reasoning, judgment, or generation.

  • Selection by meaning. Not "take all records from the past day" - that's a condition, that's a script. But "pick out of the stream what's relevant to the topic," where the criterion can't be expressed as a list of words.
  • Writing text. Not substituting values into a template, but coherent text that didn't exist before: a summary, a draft, an explanation.
  • Interpreting unstructured input. The data arrived any which way - as an email, a free-form message, mismatched records - and you have to extract meaning from it, not a field at a fixed position.

Most often the right construction is neither an "agent" nor a "script," but a script with one model step inside. The script fetches the data, filters, counts, formats, and sends. The model is called once, at exactly the point where judgment is needed, and gets an already prepared chunk as input. That way you pay for the thinking and don't pay for the shuffling.

Put it to work

What to check in practice

If you already have automation running, here's what you can do today.

  • Write out all your recurring jobs and next to each one what it does, step by step, in verbs. Not "analyzes data," but "reads, compares, sends."
  • Mark the jobs whose list contains not a single judgment step. Those are candidates for rewriting as a script.
  • For the remaining jobs with a model, look at what response ceiling they request. If it's set to the model's default maximum while the response is actually short - lower the ceiling to something reasonable.
  • Check what happens on failure. A job that fails on a schedule should go quiet after several failures in a row, or at least report itself once rather than on every run.
  • Find the jobs that send a message even when they found nothing, and remove that message.
  • Take one job from the candidate list and rewrite it as a script, all the way through. Just one, to feel the difference in the amount of work - usually there's less of it than it seems.

How to tell it worked

The criterion is simple and checkable: run the rewritten job twice on the same data and compare the output character by character. If it matches - you have a script, and its behavior can be pinned down with a test. If it doesn't - there's still a model inside, and you need to answer honestly what it's doing there.

The second sign: over a day of running, a job that had nothing to report sent not a single message, and the cost of its runs stayed at zero. The third, the calmest one, you'll notice later - when something changes on the provider's side and your queue check simply doesn't notice.