Mission Briefing // NO. 021

A transfer-news briefing that runs itself

25 August 2026· 6 min read· Case № 021

A daily transfer briefing that nags is worse than none. I built a Sunderland news roundup that runs at 08:00 every day, delivers only when something is genuinely new, and stays completely silent otherwise. The trick is a hard split: a deterministic script owns the mechanics, and the language model only formats.

I am a Sunderland fan, and during the transfer window I wanted a daily briefing that runs itself. Every morning at eight, it checks the news, and if there is genuinely new transfer information, it sends me a short Telegram message. If there is nothing new, it sends nothing at all.

That last part is the whole point. A daily digest that nags is worse than none. I set the contract in one sentence: “Only field new information. If there’s no new info, don’t bother me.” A roundup must never re-report yesterday’s rumours, and it must be completely silent when nothing is genuinely new. No filler, no status, no delivery at all.

Getting that right took three failed attempts. The fix that finally worked is a hard split between a deterministic script and the language model, and it is a pattern you can reuse for any “only when something new” digest.

The real problem

The obvious way to build this is to let the agent own the whole loop: check the news, decide what is new, write the briefing, deliver it. That is how it failed, three distinct ways.

The first was a verification wedge. I gave the agent a step to verify the state file, and it wedged on it, writing a helper script it could not run and sitting on a “blocked, no execution surface” status instead of delivering or going silent. Telling it not to write scripts was not enough. The mere presence of a verification step kept baiting it into writing a parser.

The second was a content-stub wedge. With the verification step removed, the agent had nothing genuinely new to report, so instead of going silent it narrated its own state management. “JSON parses cleanly. Verified.” The audit log was perfect. The delivered message was a stub.

The third was last-message burial. The agent produced a perfect briefing, then kept talking, narrating verification for three more turns. Because delivery grabs the last message, all that trailing chatter became the delivered message. The real briefing was buried.

What was built

A deterministic collector script owns all the mechanics. It checks the window, fetches the sources, normalises the headlines, dedupes against what has already been reported, and updates the state file at collection time. It decides whether anything is new, and if not, it prints a bare NO_NEW token and exits.

The language model formats only. It reads the collector’s shortlist, obeys the NO_NEW signal, and never touches state, dedupe, or the silence decision. It has one job: turn the shortlist into a readable briefing, or reply with the bare word SILENT and stop.

The state file is the single source of truth, holding every item already delivered. Anything in there is old, and never re-reported. The collector updates it atomically, writing to a temp file and swapping it into place, so a crash mid-write cannot corrupt it. The model never writes it. The transfer window runs 15 June to 1 September. The script checks the date itself, and outside the window it prints NO_NEW and exits without touching the network. No external pause, no wasted tokens on the idle path.

Next, the fetch. The script pulls from a set of sources, best-effort. If every source fails, it still exits cleanly, printing NO_NEW on stdout and a failure detail on stderr, so the model never sees a broken feed as news.

Then normalise. Headlines are leads, not facts. Two headlines for the same player and story must dedupe to one item, so the script resolves each headline to a player before comparing, and dedupes on player, direction, and status, not the raw headline. That defeats the rephrase trap, where an old rumour gets dressed up as new.

Then dedupe against state. Anything already in the state file is old. A rumour for a player already confirmed out is old. Escalation from rumour to confirmed is genuinely new; the reverse is stale.

Then update the state file at collection time. This is the safe direction for a “don’t bother me unless new” digest. If a run dies after collection, the items will not resurface tomorrow. That is a feature, not a bug.

Finally, the silence contract. The collector decides silence. The model obeys it. If the shortlist is empty or says NO_NEW, the model replies with the bare word SILENT and stops. Not [SILENT], not a status line, not a recap. Bare SILENT suppresses delivery entirely.

The output format

When there is news, the format matters as much as the silence. I was emphatic about this: every item names its player, with a one-line rundown. A bare headline list is useless to me. I want the name and a quick sentence on every player named in a rumour, every time.

The briefing groups items under four headings, in order: Inbound and linked, Outgoing and departures, Flags and corrections, and Context. Each item is the player’s name, their age, a one-line descriptor, and the transfer substance, with the source and how old the story is. A player appears once, never repeated across sections. The model enriches each named player with a quick search for age, position, and current club, confirming facts rather than inventing them. If a rundown search fails, it still shows the item with what it knows and marks the details unconfirmed. The whole thing should read like a sharp colleague in your ear, not a report.

The silence contract is the product

The silence contract is not a detail. It is the core feature. A digest that re-reports yesterday’s rumours is noise, and noise gets ignored, and then the real news gets missed. When the briefing fires, you know it is because something is actually new.

That is why the split matters. A language model, left to decide, will find a reason to say something. It will narrate, or hedge, or bury the real answer under process detail. A deterministic script has no such instinct. It either has new items or it does not, and it says so in one token. The model formats what the script decided.

Cost

The whole thing runs on free infrastructure: a Python script, a cron job, and a Telegram message. No API credits, no subscription, no per-run cost. The operational burden is a one-time setup and occasional maintenance.

Honest limits

First, it is only as good as its sources. Per-site scraping is the fragile layer. Some outlets block scripted requests outright, and one reliable local paper sits behind a bot wall that flaps between working and failing. The collector treats every source as best-effort, so a blocked source degrades to silence rather than to garbage, but the briefing can miss a story that only one blocked outlet covered.

Second, the ideal rumour surface is X, and it is not wired in yet. Until that is configured, the briefing relies on the aggregators the collector fetches. That is a real gap.

Third, it is window-bound. The script self-checks the transfer window, so it is quiet for most of the year by design. That is correct behaviour, not a bug.

← All transmissions