๐Ÿ“Š PulseReport
Portfolio project ยท TypeScript ยท Agentic LLM systems

Daily sales KPIs by code, narrative by AI.

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.

TypeScript 5.6 Node 20 Anthropic SDK Groq zod GitHub Actions
P
pulse report
bot ยท 09:00
๐Ÿ“Š Daily Sales Pulse (Tue, Jun 9, 2026) Revenue at $1,606.76 drops 20.47% from yesterday but stays 24.42% above last week ๐Ÿ’ฐ Revenue: $1,606.76 (-20.47% vs prev day, +24.42% vs last week) ๐Ÿ›๏ธ Orders: 17 ๐Ÿงพ Avg order: $94.52 ๐Ÿ” Top product: Nimbus Sneakers ($493.19) Today's revenue is $1,606.76, a 20.47% decline versus the previous day, while still 24.42% higher than the same day last week. Highlights: โ€ข Orders fell to 17, pulling the average order value to $94.52 โ€ข Nimbus Sneakers generated $493.19, the largest single-product contribution โ€ข Weekly momentum is positive, with revenue up 24.42% compared to last week ๐Ÿ‘‰ Launch a limited-time discount on Nimbus Sneakers tomorrow to capitalize on its strong performance.
Problem

Existing AI-report templates hallucinate numbers

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.

Solution

Separate math and narrative

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.

Pipeline

Seven typed steps. One direction. Deterministic where it matters; AI where it doesn't.

1
fetchOrders

Sheets / CSV via adapter

2
computeMetrics

Pure, deterministic math

3
writeSummary

Claude tool-use / Groq json

4
zod validate

Strict schema or fallback

5
formatReport

en-US, emojis, branding

6
fan-out

Promise.all ยท TG/Slack/Email

7
saveSnapshot

Idempotent audit JSON

Orchestrator
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, ... });
}

Stack & Patterns

๐Ÿ”ข
Deterministic numbers

All KPI math in a single pure metrics.ts. LLM receives finished figures and is forbidden from re-deriving anything in the system prompt.

๐Ÿ› ๏ธ
Strict structured output

Claude path uses tools + tool_choice + input_schema; Groq path uses response_format: json_object. One zod schema validates both.

โšก
Prompt caching

System prompt marked cache_control: ephemeral on Anthropic โ€” typical re-run reuses the prefix, cutting input tokens significantly.

๐Ÿ›ก๏ธ
Graceful fallback

LLM down or returns invalid JSON? fallbackSummary emits a templated readout from the same metrics so the pipeline never silently breaks.

๐Ÿ“ก
Multi-channel fan-out

Telegram + Slack + Email in parallel via Promise.all. Pipeline succeeds if at least one channel delivers; per-channel error logged.

๐Ÿ”
Idempotent runs

Snapshot store keyed by report date. Re-run for the same day no-ops; --force overrides. Audit trail in data/reports.json.

๐Ÿ”Œ
Adapter pattern

Single OrdersSource interface. Adding Stripe / Shopify / Postgres = ~30 LOC, no downstream changes.

๐Ÿงช
Typed env

zod parses process.env once at boot. Invalid configuration fails fast with a clear message โ€” no silent misbehavior.

โฐ
GitHub Actions cron

Free hosted scheduling via .github/workflows/daily.yml. Swap to node-cron on a VPS for sub-minute timing โ€” same entry point.

What this project demonstrates

โœ“ Agentic pipeline design with a single strict LLM call instead of an open-ended tool-use loop โ€” predictable, debuggable, cheap.
โœ“ Production patterns for LLM apps: structured output, schema validation, prompt caching, graceful fallback.
โœ“ Multi-provider abstraction over Anthropic and Groq with a one-env-var switch.
โœ“ Clean adapter interface for plug-and-play data sources.
โœ“ Multi-channel delivery with partial-failure tolerance via Promise.all.
โœ“ Idempotency + audit-trail design for one-shot scheduled jobs.
โœ“ End-to-end type safety: env โ†’ AI response โ†’ file IO. tsc --noEmit clean.
โœ“ Zero-runtime-bundler setup โ€” direct tsx execution. Ships in 794 lines of code across 9 modules.

Read the code.

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