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 is 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.