[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Scheduled Report Generator

Build a Scheduled Report Generator

Build a scheduled report generator with configurable templates, data aggregation, multi-format output, email delivery, caching, and scheduling for automated business intelligence.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Peter leads ops at a 25-person company. Every Monday, someone spends 3 hours compiling the weekly report: querying the database, calculating metrics, formatting in a spreadsheet, and emailing to stakeholders. Monthly reports take a full day. The CEO wants daily KPI summaries but there's no bandwidth. Reports use stale data because they're compiled hours after the query. Different stakeholders want different metrics — marketing wants acquisition data, finance wants revenue, product wants engagement. They need automated reporting: configurable templates, scheduled generation, multi-format output (PDF, CSV, email), and personalized per-recipient.

Step 1: Build the Report Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface ReportTemplate { id: string; name: string; description: string; queries: Array<{ name: string; sql: string; params?: any[] }>; format: "pdf" | "csv" | "html" | "json"; sections: Array<{ title: string; type: "table" | "chart" | "metric" | "text"; query: string; config: Record<string, any> }>; recipients: Array<{ email: string; format?: string }>; schedule: string; enabled: boolean; }
interface ReportRun { id: string; templateId: string; status: "running" | "completed" | "failed"; data: Record<string, any>; outputUrl: string | null; generatedAt: string; duration: number; }

// Generate report from template
export async function generateReport(templateId: string): Promise<ReportRun> {
  const { rows: [tmpl] } = await pool.query("SELECT * FROM report_templates WHERE id = $1", [templateId]);
  if (!tmpl) throw new Error("Template not found");
  const template: ReportTemplate = { ...tmpl, queries: JSON.parse(tmpl.queries), sections: JSON.parse(tmpl.sections), recipients: JSON.parse(tmpl.recipients) };
  const runId = `report-${randomBytes(6).toString("hex")}`;
  const start = Date.now();

  // Execute all queries
  const data: Record<string, any[]> = {};
  for (const query of template.queries) {
    const { rows } = await pool.query(query.sql, query.params);
    data[query.name] = rows;
  }

  // Build report sections
  let output = "";
  for (const section of template.sections) {
    const sectionData = data[section.query] || [];
    switch (section.type) {
      case "metric": {
        const value = sectionData[0]?.[section.config.field] || 0;
        const prev = sectionData[0]?.[section.config.previousField];
        const change = prev ? ((value - prev) / prev * 100).toFixed(1) : null;
        output += `## ${section.title}\n**${formatNumber(value)}**${change ? ` (${parseFloat(change) >= 0 ? "+" : ""}${change}%)` : ""}\n\n`;
        break;
      }
      case "table": {
        if (sectionData.length === 0) { output += `## ${section.title}\nNo data\n\n`; break; }
        const cols = section.config.columns || Object.keys(sectionData[0]);
        output += `## ${section.title}\n| ${cols.join(" | ")} |\n| ${cols.map(() => "---").join(" | ")} |\n`;
        for (const row of sectionData.slice(0, section.config.limit || 50)) {
          output += `| ${cols.map((c: string) => formatCell(row[c])).join(" | ")} |\n`;
        }
        output += "\n";
        break;
      }
      case "text": { output += `## ${section.title}\n${section.config.content}\n\n`; break; }
    }
  }

  const duration = Date.now() - start;
  await pool.query(`INSERT INTO report_runs (id, template_id, status, data, output, duration, generated_at) VALUES ($1, $2, 'completed', $3, $4, $5, NOW())`, [runId, templateId, JSON.stringify(data), output, duration]);

  // Deliver to recipients
  for (const recipient of template.recipients) {
    await redis.rpush("notification:queue", JSON.stringify({ type: "report_delivery", email: recipient.email, reportId: runId, subject: `${template.name}${new Date().toLocaleDateString()}`, body: output }));
  }

  return { id: runId, templateId, status: "completed", data, outputUrl: null, generatedAt: new Date().toISOString(), duration };
}

function formatNumber(val: any): string {
  if (typeof val === "number") return val >= 1000 ? `${(val / 1000).toFixed(1)}K` : val.toFixed(val % 1 === 0 ? 0 : 2);
  return String(val);
}

function formatCell(val: any): string {
  if (val === null || val === undefined) return "-";
  if (val instanceof Date) return val.toISOString().slice(0, 10);
  if (typeof val === "number") return formatNumber(val);
  return String(val).slice(0, 100);
}

// Get report history
export async function getReportHistory(templateId: string, limit: number = 20): Promise<ReportRun[]> {
  const { rows } = await pool.query("SELECT * FROM report_runs WHERE template_id = $1 ORDER BY generated_at DESC LIMIT $2", [templateId, limit]);
  return rows;
}

Results

  • Weekly report: 3 hours → 0 — template runs every Monday 8 AM; metrics calculated, formatted, emailed to 5 stakeholders; no manual work
  • Daily KPI summary — CEO gets revenue, new users, churn rate at 8 AM daily; data from last 24 hours; trend comparison vs previous period
  • Personalized per-recipient — marketing gets acquisition report; finance gets revenue report; same schedule, different templates; each gets what they need
  • Multi-format output — PDF for executives, CSV for analysts, HTML email for quick scan; generated from same data; format per-recipient configurable
  • Report history — see every report generated; compare this week to last week; spot trends; no more "what were last month's numbers?"