An agentic TypeScript pipeline. Math lives in a pure module; Claude or Groq writes a strict-JSON readout; Telegram, Slack and Email get the report at 9 AM. Built to demonstrate production patterns for LLM apps โ tool-use, prompt caching, zod validation, graceful fallback, idempotent fan-out.
Off-the-shelf solutions in Zapier / Make / n8n hand raw data to the LLM and ask it to summarize. The model casually re-derives totals and percentages โ and gets them wrong often enough to destroy trust. Once the owner spots one wrong number, the whole report becomes noise.
A small TypeScript pipeline computes every figure in a pure metrics.ts module. The LLM receives finished metrics through a strict JSON schema and is explicitly forbidden from re-deriving anything. The numbers are auditable, the prose stays human-readable, and the system fails gracefully if the LLM call breaks.
Seven typed steps. One direction. Deterministic where it matters; AI where it doesn't.
Sheets / CSV via adapter
Pure, deterministic math
Claude tool-use / Groq json
Strict schema or fallback
en-US, emojis, branding
Promise.all ยท TG/Slack/Email
Idempotent audit JSON
export async function runDailyReport(flags) {
const cfg = loadConfig();
const orders = await fetchOrders(cfg);
const metrics = computeMetrics(orders, flags.date);
if (!flags.force && await hasReportFor(cfg.SNAPSHOT_PATH, metrics.date)) return;
let summary;
try {
assertLLMReady(cfg);
summary = await writeSummary(cfg, metrics); // Claude tool-use or Groq json
} catch (err) {
summary = fallbackSummary(metrics); // never silently break
}
const text = formatReport(metrics, summary, cfg.BRAND_NAME);
if (flags.dry) return console.log(text);
const [tg, slack, email] = await Promise.all([ // fan-out
enabled.telegram ? sendTelegram(cfg, text) : null,
enabled.slack ? sendSlack(cfg, text) : null,
enabled.email ? sendEmail(cfg, subject, text) : null,
]);
if (![tg, slack, email].some(r => r?.ok)) throw new Error("all channels failed");
await saveSnapshot(cfg.SNAPSHOT_PATH, { date: metrics.date, metrics, summary, ... });
}
All KPI math in a single pure metrics.ts. LLM receives finished figures and is forbidden from re-deriving anything in the system prompt.
Claude path uses tools + tool_choice + input_schema; Groq path uses response_format: json_object. One zod schema validates both.
System prompt marked cache_control: ephemeral on Anthropic โ typical re-run reuses the prefix, cutting input tokens significantly.
LLM down or returns invalid JSON? fallbackSummary emits a templated readout from the same metrics so the pipeline never silently breaks.
Telegram + Slack + Email in parallel via Promise.all. Pipeline succeeds if at least one channel delivers; per-channel error logged.
Snapshot store keyed by report date. Re-run for the same day no-ops; --force overrides. Audit trail in data/reports.json.
Single OrdersSource interface. Adding Stripe / Shopify / Postgres = ~30 LOC, no downstream changes.
zod parses process.env once at boot. Invalid configuration fails fast with a clear message โ no silent misbehavior.
Free hosted scheduling via .github/workflows/daily.yml. Swap to node-cron on a VPS for sub-minute timing โ same entry point.
Promise.all.tsc --noEmit clean.tsx execution. Ships in 794 lines of code across 9 modules.~800 lines of TypeScript across 9 small modules. Clone, run npm run report -- --dry, and watch the full pipeline end-to-end on the bundled demo dataset.