0% 5h quota used · resets in 2h live · 08/09/2026 12:05 CEST

de:netzbetreiber-replies

Triage new replies from German grid operators and extract data from their answers, then send the queued follow-ups and reminders via the guarded sender.

Runs 98
last 4h ago
Failed 7
no current streak
Median run $1.58
5m · de ceiling $100 per run
Spent in total $295
4,795 turns of agent work

Manifest

de/tasks.yaml
Address
de:netzbetreiber-replies — also de:netzbetreiber-replies
Runner
claude-code
Policy
autonomous writes only inside its own workspace, no outward action
Workspace
countries/de/
Schedule
30 7,13,19 * * * · next 08/09/2026 13:30
Model
sonnet
Budget
250 turns · 75 minutes
Triggerable
yes — free text can reach it
Notifies
every run — 🟢 on success, 🔴 on failure
Description (manifest)
Triage new replies from German grid operators and extract data from their answers, then send the queued follow-ups and reminders via the guarded sender.

Cost per run

oldest left · ceiling $100
de per-run ceiling $100 25/08/2026 19:36: $2.6926/08/2026 07:32: $1.1126/08/2026 13:34: $2.2426/08/2026 19:32: $1.0827/08/2026 07:31: $0.9227/08/2026 13:35: $2.7027/08/2026 19:35: $2.2928/08/2026 08:04: $0.8828/08/2026 13:35: $1.9828/08/2026 19:32: $0.7929/08/2026 07:52: $0.3929/08/2026 13:31: $0.6429/08/2026 19:32: $0.5830/08/2026 07:32: $0.4230/08/2026 13:31: $0.3530/08/2026 19:32: $0.4331/08/2026 08:08: $1.1931/08/2026 13:36: $1.9431/08/2026 19:35: $1.4501/09/2026 08:05: $1.2201/09/2026 13:32: $0.8901/09/2026 19:39: $2.7602/09/2026 08:09: $2.2902/09/2026 13:46: $5.9002/09/2026 19:58: $19.7303/09/2026 08:16: $1.8203/09/2026 13:44: $4.8203/09/2026 19:39: $2.2904/09/2026 08:04: $1.5804/09/2026 13:34: $0.8704/09/2026 19:37: $1.6805/09/2026 07:58: $0.7205/09/2026 13:32: $0.4905/09/2026 19:32: $0.4306/09/2026 08:08: $0.4306/09/2026 13:32: $0.4906/09/2026 19:32: $0.5207/09/2026 08:05: $0.8107/09/2026 19:34: $1.4308/09/2026 07:53: $0.42 25/0827/0828/0829/0831/0801/0902/0904/0905/0906/09
WhenWhat it reportedTurnsTookCost
08/09/2026 07:53This was a quiet pass: 0 new mail, 0 pending documents, nothing to triage. `fetch_browser.mjs` hit its standing `ew-rohmund.de` function-400 failure again (12th consecuti…232m$0.42ok
07/09/2026 19:34This run processed 4 messages: 3 full §14a extractions (Stadtwerke Konstanz, Talwerk GmbH/Stadtwerke Kaiserslautern, LokalWerke GmbH — all high-confidence, complete recor…304m$1.43ok
07/09/2026 08:05This run processed 1 new message: Stadtwerke Norderstedt (SNB985965721965) replied with a link to their own Preisblatt, which I fetched and fully extracted — Modul 1 (97,…242m$0.81ok
06/09/2026 19:32Journal entry recorded. Final report: **netzbetreiber-replies run (2026-09-06 17:30) — quiet pass, no mail to triage.** - Fetch: 0 new inbox messages; browser fetch att…202m$0.52ok
06/09/2026 13:32Quiet pass — no new mail arrived and nothing was queued to triage. `fetch_mail.mjs` found 0 new messages, `pending_docs`/`pending_mail` were both empty, so `triage.jsonl`…192m$0.49ok
06/09/2026 08:08Quiet pass: 0 new messages, 0 pending docs/mail to triage, so no §14a extractions this run. Finish steps ran clean — export (510 operators, 479 complete), sheet push (510…192m$0.43ok
05/09/2026 19:32Run complete: no new mail arrived (`fetch_mail.mjs` and `pending_mail.mjs` both empty), so no triage/extraction work this pass — the fourth consecutive quiet run. The bro…182m$0.43ok
05/09/2026 13:32This was a quiet pass: no new mail arrived, nothing was pending to triage, and both `findings.csv`/the Google Sheet were re-pushed unchanged (510 operators, 479 complete)…202m$0.49ok
05/09/2026 07:58This run was quiet: no new mail arrived, no pending documents, and no messages needed triage — `triage.jsonl`/`findings.jsonl` are unchanged (still 510 operators, 479 com…222m$0.72ok
04/09/2026 19:37Journal entry written. Final report: Triaged both new messages this pass: one from swa Netze GmbH's retail sibling (Stadtwerke Augsburg Energie) claiming "§14a not yet i…537m$1.68ok
04/09/2026 13:34Run complete. This pass triaged 1 new reply (Stadtwerke Germersheim GmbH, SNB982049301273), which pointed to their own Preisblatt PDF; fetched and fully extracted all thr…314m$0.87ok
04/09/2026 08:04This scheduled pass processed a light mailbox — 1 new message and 1 pending document — and pushed the findings/outreach sheets clean. **Triage:** 1 message from Syna Hoc…526m$1.58ok
03/09/2026 19:39This run's work is complete. Final report: Processed 5 new inbox messages plus 2 leftover documents from a prior browser fetch. Got 2 full high-confidence §14a extractio…459m$2.29ok
03/09/2026 13:44This run is complete. Final report: Processed all 12 pending reply messages plus, after inadvertently triggering a broader website-hunt batch (a `--help` probe on `hunt_…6814m$4.82ok
03/09/2026 08:16Both background bash tasks I started earlier (b21ftt6lr, bpn0vdgsq) are just idle wait-loops on a now-dead PID — they'll exit on their own shortly and need no action. **…4116m$1.82ok
02/09/2026 19:58Cleaned up. The run is complete. Here's my final report: Triaged all 8 new messages (4 full §14a extractions, 2 auto-acks, 2 sent to human queue for host-rule/empty-body…14228m$19.73ok
02/09/2026 13:46This run triaged 14 replies: 9 full §14a extractions (SW Unna, Stadtwerke Wedel — recovering a lost 14.08 first-round reply that had never been triaged, BASF InfraService…7716m$5.90ok
02/09/2026 08:09Good, the journal entry is complete and correct. Now the final report to the human channel. This run triaged all 12 pending inbox messages: 8 auto-acks, 1 out-of-office…456m$2.29ok
01/09/2026 19:39This pass processed 34 new messages (25 triaged from the worklist, 9 deferred to next run): 22 auto-acks, 1 full §14a extraction (SWE Netz Ettlingen — duplicate-confirmin…538m$2.76ok
01/09/2026 13:32This run processed 3 replies: two plain ticket-confirmation auto-acks (Schleswig-Holstein Netz, NordNetz — no data) and one full §14a extraction for wesernetz Bremen (SNB…282m$0.89ok
01/09/2026 08:05This run's netzbetreiber-replies pass processed 2 new messages, both from Gemeindewerke Nüdlingen delivering a complete §14a Preisblatt (Modul 1: 119,05 EUR/a; Modul 2: 2…323m$1.22ok
31/08/2026 19:35This run processed 4 replies: 2 full §14a extractions (Stadtwerke Windsbach and Stadtwerke Zeven, bringing findings to 441 operators/425 complete), 1 reroute drafted (nvb…375m$1.45ok
31/08/2026 13:36This run's netzbetreiber-replies pass is complete: 9 messages triaged (3 full §14a extractions — Stromnetz Weiden, Stadtwerke Munster-Bispingen, EG Wittmund; 2 answer_par…426m$1.94ok
31/08/2026 08:08This run triaged 3 replies, all `answer_pdf`, and extracted full §14a Modul 1/2/3 data for all three (Stadtwerke Sondershausen Netz, Stadtwerke Bad Brückenau, Stadtwerke…274m$1.19ok
30/08/2026 19:32This run found an empty mailbox: `fetch_mail.mjs` retrieved 0 new messages and `pending_mail.mjs`/`pending_docs.mjs` had nothing queued, so no triage, extraction, or draf…172m$0.43ok
30/08/2026 13:31This pass had an empty mailbox: 0 new messages, 0 pending docs, nothing to triage or extract. Findings (437 operators, 421 complete) and the Outreach sheet were re-pushed…171m$0.35ok
30/08/2026 07:32This pass had an empty mailbox — 0 new messages fetched, 0 pending documents or mail, so no triage, extractions, or drafts. Finish steps ran clean: findings unchanged at…172m$0.42ok
29/08/2026 19:32This run processed 1 message: an auto-ack from Stadtwerke Herne's Netzcenter acknowledging the reroute sent last pass — no §14a data, classified `auto_ack`/action `none`,…242m$0.58ok
29/08/2026 13:31This run's mailbox had 1 new reply: Stadtwerke Herne AG's Kundenservice redirected our §14a inquiry to their Netzcenter ([email protected]), so I drafted and…221m$0.64ok
29/08/2026 07:52This run's inbox was empty (0 new messages, 0 pending) — nothing to triage or extract. Ran the finish pipeline regardless: findings unchanged at 437 operators (421 comple…161m$0.39ok
28/08/2026 19:32This run had a quiet inbox (1 new reply) but a clean full extraction: WeilerWärme eG (SNB910224319560) sent a Preisblatt link, and downloading/reading it yielded complete…242m$0.79ok
28/08/2026 13:35This run's triage work is complete: fetched 5 new messages, extracted full §14a data (Modul 1/2/3) for Stadtwerke Northeim, Werl, and Senftenberg, flagged a second TenneT…505m$1.98ok
28/08/2026 08:04Journal entry written. Here's the final report. This run fetched 1 new reply and fully triaged it: Stadtwerke Gaggenau (SNB925565312521) sent a direct Preisblatt link, f…301m$0.88ok
27/08/2026 19:35This run is complete. Final report: **netzbetreiber-replies, 2026-08-27 17:30 run:** Processed all 7 pending messages. 2 answer_complete (Bad Aibling and Quedlinburg — b…605m$2.29ok
27/08/2026 13:35Journal entry written. Final report: This run fetched and triaged **8 new replies** (0 unmatched). **7 answer_complete** with full §14a data extracted (all 3 modules) —…555m$2.70ok
27/08/2026 07:31This run's netzbetreiber-replies pass is complete: fetched and triaged the single new reply (Stadtwerke Stadtroda), extracting full §14a Modul 1-3 data from their linked…261m$0.92ok
26/08/2026 19:32Processed 2 replies: Stadtwerke Bietigheim-Bissingen gave a full §14a extraction (Modul 1/2/3, standard Q1+Q4 pattern) via their Preisblatt PDF, and Stadtwerke Ochtrup as…272m$1.08ok
26/08/2026 13:34Triaged all 6 pending replies: 3 full §14a extractions (EKS Schaffhausen, Stadtwerke Pfarrkirchen, Stadtwerke Backnang), 1 wrong_entity (TenneT, disputes §14a applicabili…424m$2.24ok
26/08/2026 07:32This run's inbox had 1 new message, fully triaged and extracted (Stadtwerke Schwarzenbach/Saale, complete §14a data — Modul 1: €113.36/a, Modul 2: 2.46 ct/kWh, Modul 3 wi…282m$1.11ok
25/08/2026 19:36Everything's clean, no stray files. Ready to report. **Run outcome (netzbetreiber-replies, 17:30 pass):** Triaged 6 new replies — 4 fully extracted (Stromnetz Weiden, St…596m$2.69ok
25/08/2026 13:46Everything checks out. The run is complete — final report follows. **Run outcome (11:30 scheduled run):** Triaged all 23 pending messages: 19 `answer_pdf` (full §14a Mod…10216m$7.51ok
25/08/2026 07:33Journal entry written. Final report to the user: This run's inbox had 3 new replies, all fully triaged and extracted with high-confidence §14a Modul 1/2/3 data: **Stadtw…323m$1.38ok
24/08/2026 19:37This run's triage and finish steps are complete. Here's the report: Processed 10 new replies (mailbox fully triaged): 5 auto-acks (no action needed) and 5 full §14a extr…587m$2.69ok
24/08/2026 15:20Journal entry written. Final report below. This run fetched 6 new mailbox messages (all matched to known operators) and fully triaged them: 3 were generic auto-acknowled…343m$1.88ok
24/08/2026 14:30Run complete. Inbox had just 1 new message (a generic auto-ack from Stadtwerke Radolfzell, no action needed) — mailbox fully triaged. Findings now stand at 378 operators…359m$0.68ok
24/08/2026 13:39Scheduled a fallback check in case the background task doesn't notify directly; otherwise I'll be notified automatically when the send completes.658m$3.55ok
24/08/2026 09:40Netzbetreiber-replies run complete: triaged all 9 new messages (4 auto-acks, 5 answers) and extracted full §14a Modul 1/2/3 data for 5 operators (Farchant, Neustadt a.d.…487m$2.45ok
23/08/2026 13:35I'll wait for the background send task to finish before wrapping up.405m$1.41ok
23/08/2026 07:30Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access10.5s$0error
22/08/2026 19:32Run complete. The inbox had zero new/pending messages this cycle, so there was nothing to triage or extract — `findings.jsonl`/`triage.jsonl` are unchanged. Ran the Finis…182m$0.57ok
22/08/2026 13:31No new mail arrived this run — mailbox was already fully triaged, so no messages to classify or extract. Ran the Finish sequence anyway: `export_findings.mjs` stayed at 3…201m$0.61ok
22/08/2026 07:33Run complete. Final report: Processed all 6 new mailbox messages: 5 routine auto-acknowledgements (Gronau, Kaltenkirchen, Stadtwerke Landau a.d.Isar, Herne, Lingen — all…413m$1.51ok
21/08/2026 19:48Journal entry recorded. Now the final report. **Netzbetreiber-replies run complete.** Processed all 16 new messages (3 auto-acks, 13 answers) — extracted full §14a Modul…7118m$5.05ok
21/08/2026 07:49I'll pause here and wait for the background `followups.mjs send --all` task to complete — the harness will notify me automatically when it finishes.22619m$13.83ok
20/08/2026 19:39Processed 25 of 46 new messages (21 carried to next run): 14 auto-acknowledgements and 11 complete §14a Preisblatt extractions, all high confidence, with zero bounces, re…949m$5.57ok
20/08/2026 08:02Run complete. Only 1 new message this cycle — Gemeindewerke Oberaudorf replied with a Preisblatt link, extracted cleanly (all 3 §14a modules, high confidence); mailbox fu…4132m$1.61ok
19/08/2026 19:42Send step (`followups.mjs send --all`) is running in the background — waiting for it to finish before writing the journal entry and final report.9212m$6.64ok
19/08/2026 07:46This will notify me when the send process exits. I'll wait for that notification now.8616m$5.65ok
18/08/2026 19:45**Run summary:** Fetched 44 new messages, processed all 25 the worklist cap allowed (19 remain for the next run) — 18 `answer_pdf` fully extracted, 2 `answer_partial` (op…13414m$10.69ok
18/08/2026 08:03All work is complete. Summary of the run: - **Fetched & triaged 20 messages** (all pending, no carryover): 18 `answer_pdf` with full §14a Modul 1-3 data extracted, 1 `an…17533m$15.10ok
Read the skill

Netzbetreiber reply triage & extraction

Campaign dir: campaigns/netzbetreiber/. You never compose-and-send email directly — drafts go to followups-proposed.jsonl, and the ONLY sending happens via the deterministic followups.mjs send in step 7, which applies send-time freshness guards.

1. Fetch

Run node campaigns/netzbetreiber/fetch_mail.mjs (appends new mail to inbox.jsonl, saves attachments under replies/).

Then run node campaigns/netzbetreiber/fetch_browser.mjs — it renders the links a plain GET could not turn into a document (the large WAF-fronted operators) and saves what it gets under replies/. It self-skips when no browser is configured or nothing is queued, so it is safe to run every pass.

2. Select work

Also run node campaigns/netzbetreiber/pending_docs.mjs — it lists documents already sitting in replies/ whose operator is still missing §14a fields, into /tmp/pending-docs.json. These are files a previous pass fetched: the browser path retrieves a document in one run and the message that named it was triaged in an earlier one, so nothing else would ever point at them. Read each and append a findings.jsonl record exactly as in step 4 (no new triage row — the message was already triaged). Usually this list is empty; when it is not, it is the cheapest data in the run.

Run node campaigns/netzbetreiber/pending_mail.mjs — it writes the worklist to /tmp/pending-mail.json (oldest first, capped at 25, text pre-truncated) and prints how many remain for the next run (include that in your summary). Work from that file ONLY — do not read inbox.jsonl yourself (it is several MB and reading it blows the time budget). Process every message in the worklist. Append to triage.jsonl/findings.jsonl after EACH message, never in one batch at the end — an interrupted run must keep its completed work.

Do ALL work synchronously inside this run. NEVER spawn background agents and end your turn "waiting" — this run terminates the moment you produce your final message, killing anything still running (2026-08-04: a run ended "waiting for the background triage agent" and zero messages got processed). Synchronous subagents are fine; background ones are not.

3. Classify each message

One of: answer_complete, answer_partial, answer_pdf (data in attachment/link), auto_ack (ticket/receipt confirmation), out_of_office, reroute ("write to X instead"), wrong_entity ("we are not the Netzbetreiber"), refusal, question_back (human asks who/why — never argue, route to human), do_not_contact (asks to stop contact), bounce (mailer-daemon), irrelevant (spam/tests).

Rules:

  • Bounces: the original message is usually attached — Read it to find the NB-SNB… reference or +SNB… recipient, set the row's mastr_nr.
  • Unmatched messages (mastr_nr: null): try to identify the operator from content/signature against seed.csv; if still unclear → irrelevant or note for a human.
  • Trust content over subject (an "Automatische Antwort" that also contains prices is an answer).

4. Extract §14a data (for answer_* classes)

Target fields: modul_1_price (EUR/year), modul_2_price (ct/kWh), off_peak_hours (time windows), modul_3_description (free text). Sources, in order of effort: the email text; PDF attachments (use Read on the saved file under replies/); linked Preisblatt URLs — for those run

node campaigns/netzbetreiber/fetch_reply_doc.mjs --mastr SNB…

which picks the URLs out of that operator's own messages, ranks them, follows a landing page one hop deeper, and prints the saved path under replies/ to Read. Never guess figures from a URL name. Do not hand-curl a link instead: the script is what checks the URL actually came from that operator's reply and sits on their own domain, and it un-wraps the links mail clients break across lines (Bayernwerk's PDF link was truncated at …/bayernwerk-netz/ for weeks).

It exits non-zero when it could not get a document, having written the URL to fetch-queue.jsonl for the browser path. That is not a failure to work around — classify the message answer_pdf, action human_queue, and say in detail that the document is queued. Do not try to fetch it another way.

Append one record per (message, operator) to findings.jsonl:

{"mastr_nr": "SNB…", "message_id": "…", "modul_1_price": "…" , "modul_2_price": "…",
 "off_peak_hours": "…", "modul_3_description": "…",
 "source": {"kind": "text|pdf|url", "detail": "<file or url>", "quote": "<verbatim passage per field, abridged ok>"},
 "confidence": "high|medium|low", "extracted_at": "<ISO date>"}

Missing fields stay null. NEVER invent values; a null with a note beats a guess (country hard rule 1). Numbers must appear in the source — quote them.

The modul_*_price fields are provenance, not data: keep the source's own wording, decimal comma, unit and sign — "-121,62 EUR/Jahr netto (-144,73 brutto), pauschale Entgeltreduzierung", never a pre-parsed "-121.62". The machine-readable figures belong in normalized below, and that is the only place unit/sign conversion happens. (2026-08-12: runs that wrote bare numbers here treated normalization as a copy and carried the Preisblatt's minus sign into normalized, which the contents API rejects.)

Additionally include a normalized object in every findings record:

"normalized": {"modul1_netto_eur_a": 127.60, "modul1_brutto_eur_a": 151.84,
 "modul2_netto_ct_kwh": 3.22, "modul2_brutto_ct_kwh": 3.83,
 "off_peak_windows": "00:00-06:00; 22:00-00:00", "off_peak_note": "nur Q1+Q4 (or null)",
 "modul3_summary": "<clean German, ≤200 chars>", "vat_derived": false,
 "review_note": "<ambiguity for Stefanie, or null>"}

Normalization conventions:

  • Magnitudes are POSITIVE — always, no exceptions. §14a only ever grants a reduction, never a surcharge, so these fields hold how big the reduction is. Preisblätter print Modul 1 as a deduction line ("-117,02 EUR/a") because it is subtracted from the normal Netzentgelt; that minus is bookkeeping, not part of the value. Drop it here even when you copied it verbatim into modul_1_price, and say so in review_note.
  • modul1 = the pauschale Reduzierung alone (EUR/a) — not Grundpreis/Arbeitspreis.
  • modul2 = the reduced Arbeitspreis in ct/kWh. A percentage-only answer ("60% Reduzierung") without an operator number → null + review_note.
  • When only netto or only brutto is given, derive the other with 19% USt (2 decimals) and set vat_derived: true. Unlabeled values: assume netto, note it in review_note.
  • off_peak_windows = the NT/Niedriglast windows only, canonical "HH:MM-HH:MM; HH:MM-HH:MM"; seasonal restrictions go to off_peak_note.
  • Numbers you cannot determine stay null with a review_note — never guess.

5. Record triage

Append one line per processed message to triage.jsonl:

{"message_id": "…", "mastr_nr": "SNB… or null", "classification": "…",
 "action": "extracted|followup_drafted|wait|reroute_drafted|human_queue|suppressed|none",
 "detail": "ticket ref / OOO return date / new contact / reason", "ts": "<ISO>",
 "wait_until": "YYYY-MM-DD or null"}

wait_until (out_of_office only): the stated return date, machine-readable — the reminder script defers silent operators until it has passed. Null when no date is stated.

6. Consequences per class

  • answer_partial: draft a short German follow-up asking ONLY the missing fields (thank them, quote reference) → append {mastr_nr, to, subject: "Re: <their subject>", body, in_reply_to: "<their message_id>"} to followups-proposed.jsonl. Do not send it here — step 7 flushes the whole queue at the end of the pass.
  • reroute: draft the same inquiry to the named address → also into followups-proposed.jsonl with "reroute": true (no in_reply_to — new thread).
  • do_not_contact: append the mastr_nr (one per line) to do_not_contact.txt.
  • bounce: nothing to send; it goes in the summary warning.
  • question_back / wrong_entity / unresolvable: list under "needs human" in the summary.

7. Finish

Run node campaigns/netzbetreiber/export_findings.mjs (regenerates findings.csv — one row per operator, latest non-null value per field), then node campaigns/netzbetreiber/push_sheet.mjs (updates the Findings/Outreach tabs in Stefanie's Google Sheet; skip without failing the run if it errors on missing credentials), then node campaigns/netzbetreiber/render_reminders.mjs (drafts reminders for operators silent ≥7 days / past their out-of-office return date — it self-skips unless the mailbox is fully triaged, so a cap-limited run simply defers reminders to the next one), and finally node campaigns/netzbetreiber/followups.mjs send --all — this SENDS all pending follow-ups and reminders right now, while the mailbox is provably fully triaged (maximum freshness). Send-time data guards supersede anything stale (operator replied meanwhile, data complete, do_not_contact). Report sent and superseded counts in your final message.

Fire it DETACHED and do not wait for it. The send takes ~30 minutes and is not your job to supervise — start it, confirm it started, move on:

setsid nohup node campaigns/netzbetreiber/followups.mjs send --all \
  > campaigns/netzbetreiber/send-last.log 2>&1 < /dev/null &

setsid is what makes this work. A plain &, or a background tool call, stays attached to this run and is killed the moment you produce your final message — 2026-08-24: a run did exactly that and was killed after 12 of 152 drafts, while the ledger recorded ok. Detached, it reparents to PID 1 and keeps sending long after the run ends. It holds a lock, so it cannot collide with the next pass.

Do not tail the log, do not poll it, do not schedule a check on yourself, and never end your turn "waiting" for it. Confirm it started, note how many drafts were queued, and report that. Its results land in followups-sent.jsonl; read that at the start of the NEXT run to report what the last send actually did.

Always run this step — it is not a judgment call. Country CLAUDE.md hard rule 2 names this script as its explicit exception; the rule forbids you writing mail yourself, not running the sender. If the mailbox is not fully triaged, the script self-skips and says so — that is the correct outcome. Report the skip; do not --force it and do not withhold the send on your own initiative. If you believe the send should not happen, say so in your final message and journal entry rather than silently deviating (2026-08-24: two runs made opposite calls here).

Final message (goes to Google Chat): this run's triage work — counts per classification, operators with complete data so far, what the previous pass's send did (from followups-sent.jsonl), one line on whether you fired the follow-up send and how many drafts were queued, and a WARNINGS block (bounces, do_not_contact, unmatched). Aggregated, not one line per mail. Journal entry per country CLAUDE.md.

The final message is the deliverable — it must be the results of THIS run's replies work. It is the only thing a human sees. Report what you did, with the numbers, in the past tense. The send is fire-and-forget: one line saying it was triggered is the whole of it, and its counts belong to the next run's report.

Never substitute process narration for results: no "I've dispatched…", no "I'll be notified when…", no "scheduled a fallback check", no describing tasks you started instead of work you finished. If the message has no triage numbers in it, this run reported nothing, whatever the ledger says. Same for the journal entry — write it before the final message, never skip it.