All projects 05 · LLM systems

Agentic Workflow Automation

A deterministic health pipeline for seven public market-data feeds, with an LLM triage agent invoked only when a check fails. On a clean day: zero LLM calls. Across six staged failures: 4 correct verdicts, 2 wrong-but-gated, 0 wrong-and-confident.

Status Shipped Year 2026 Stack Python 3.11 · Claude Sonnet 5 · pytest

Overview

Market-data teams watch many feeds, and most days nothing is wrong — which is exactly the problem. A monitor that pages a human for every holiday gap trains people to ignore it; one that silently swallows a decimal-shifted price is worse. The fashionable answer is 'put an agent on it'. The engineering answer, built here as feed-health-pipeline: detect anomalies mechanically, spend judgment — machine or human — only on real exceptions, and never let an uncertain judgment pass silently.

Seven deliberately heterogeneous public feeds (four formats, four cadences) flow through payload and schema guards, format-specific parsers and five deterministic checks — freshness, gaps, spikes, bounds, duplicates — with every threshold in config, not code. When all checks pass, the run costs zero LLM calls. Only when one fails does a bounded tool-use agent investigate that single incident: it can read the feed spec, recent observations and the previous snapshot, gets a hard six-turn limit, and must submit exactly one classified verdict with its full tool trace saved.

The verdict then hits a gate written in code, not vibes: confidence below 0.8, any consequential action, or any unknown classification routes to an append-only human review queue. The staged-failure showcase proves the design point — both wrong verdicts arrived with low confidence, were caught by the gate, and were corrected on the record with dated notes. Not a single incident was dropped, and nothing wrong was ever confident.

Triage scorecard4 / 6 correct
Wrong & confident0
Clean-day LLM calls0
Offline test suite~70 passed

Approach

01

Fetch

One HTTP request per source with snapshot rotation — the previous snapshot is kept deliberately, as the comparison material for the agent's provenance tool.

02

Parse & guard

A payload guard catches empty and HTML responses, a schema guard catches missing fields; seven format-specific parsers reduce everything to uniform (date, value) observations.

03

Check

Five deterministic checks — freshness, largest gap, spike, bounds, duplicate dates — every limit read from the per-feed spec in data/feeds.json. All pass → report written, zero LLM calls.

04

Triage

Exceptions only: a Claude tool-use loop with a capped toolbox, a hard 6-turn limit with forced verdict, retries with backoff, and strict validation that coerces unknown output to safe values.

05

Gate & review

Confidence < 0.8, consequential actions and unknowns go to an append-only review queue with dated resolution notes. No API key or persistent failure → straight to the queue. Nothing is silently dropped.

Watch the agent work

Every frame below is replayed from the committed run artifacts — the observations the checks saw, the tools the agent actually called, its verdict and confidence, and what the gate did with it. Pick a scenario.

Feed
Agentidle
Gateconfidence < 0.8 · consequential action · unknown → human
REAL RUN DATA — results/demo/<scenario>/triage/*.json · claude-sonnet-5

Six staged
failures, one
scorecard

A scenario builder deterministically tampers the committed baseline snapshots into six failure cases — byte-identical on every rebuild — and each was run against the live agent, with the full artifacts committed. Result: 4 correct, 2 wrong-but-gated, 0 wrong-and-confident. Both misses carried low confidence, landed in the review queue, and were corrected by a human with dated notes.

ScenarioTamperingAgent verdictOutcome
price_spike×10 decimal shift (EUR/NOK)data_error · 0.93 · refetchcorrect — auto-logged
btc_drawdown−30% price move (BTC/USD)data_error · 0.78 · use last goodcorrect — gated < 0.8
schema_changevalue column renamed (US debt)source_change · 0.85 · update_speccorrect — consequential, gated
html_payloadHTML bot wall (Eurostat)source_change · 0.60 · refetchcorrect — gated < 0.8
stale_feedlast 30 days removed (USD/EUR)benign · 0.62wrong — gated, corrected
gap10 business days cut (SEK/EUR)benign · 0.55wrong — gated, corrected

REAL RUN DATA — results/demo/<scenario>/triage/*.json · claude-sonnet-5

The judgment
pair

The standout result. The same spike check fired for two very different events: a ×10 decimal shift and a genuine −30% market move. A threshold cannot tell them apart — that is exactly the rare, unstructured judgment the agent exists for.

×10 shiftprevious snapshot held a different in-trend value for the same date → data error, 0.93, refetch −30% moveno provenance conflict → use last good value, 0.78 — under the gate, so a human still saw it

The agent distinguished them on provenance grounds — comparing against the rotated previous snapshot through its tools — not on headline size.

Nothing passes
the gate
silently

A verdict is auto-logged only when it is confident and its action is safe. Everything else routes to an append-only human review queue — entries are never deleted, and resolving one records a dated note, so wrong verdicts stay on the record next to their correction.

confidence < 0.8human review — regardless of how sure the model sounds consequential actionupdate_spec and escalate always need a human sign-off unknown / triage_failedstraight to the queue — including when there is no API key at all

Fail-safe by construction: the pipeline never requires the LLM to run, and never silently drops an incident.

It caught a
real one

On the first live run the freshness check flagged euro-area HICP as 189 days stale. Not a bug: Eurostat had genuinely frozen the dataflow after a methodology change. The feed was retired, replaced by the EA21 unemployment series, and every threshold was recalibrated against observed history — all documented in the repo.

Public endpoints changing and dying is treated as a feature of the demo, not a footnote: the guards exist precisely because “the fetch succeeded” does not mean “the data is fine.”

Seven real
feeds

Deliberately heterogeneous — four formats, four cadences, seven independent public endpoints — so the parsers, payload/schema guards and per-feed thresholds all earn their keep. Every threshold lives in config, not code: a wrong limit is a visible, fixable config change, not a bug.

FeedSourceFormatCadence
EUR/NOK spot rateNorges BankSDMX-CSV (semicolon)business daily
SEK/EUR daily fixingSveriges RiksbankJSON arraybusiness daily
US GDP, current US$World BankJSON (metadata + rows)annual
EA21 unemployment (SA)EurostatSDMX-CSV (comma)monthly
Total public debtUS Treasuryplain CSVbusiness daily
BTC/USD daily priceCoinGeckoJSON (prices array)daily 24/7
USD/EUR reference rateFrankfurter (ECB)JSON (rates object)business daily

Known
limitations,
documented

TUNED

Thresholds are hand-tuned

Calibrated on observed history rather than statistically fitted — a regime change needs a recalibration pass.

GATE

The gate trusts the model’s calibration

A wrong-and-confident verdict would pass it. None did in the showcase — but the limit is stated, not hidden.

POINT

No trend storage

Reports are point-in-time snapshots; only the review queue persists across runs.