Reports — Architecture
Data Model
Reports from the agent source are Activity rows (deliverableType=monthly_report, status=done). DM-uploaded and AI-generated reports live in the Report table (packages/db/prisma/schema.prisma).
Report table — key fields
| Field | Type | Notes |
|---|---|---|
id | String | CUID |
tenantId | String | FK to Tenant |
label | String | Display name |
period | String | e.g. “March 2026” |
startDate / endDate | DateTime | Reporting window |
source | String | "dm_upload" | "ai_generated" | "scheduled" |
frequency | String? | "weekly" | "monthly" — populated when source="scheduled" |
status | String | "generating" | "ready" | "failed" |
contentType | String | "markdown" | "pdf" |
content | String? | Markdown body (ai_generated / markdown uploads) |
fileUrl | String? | DigitalOcean Spaces URL (pdf uploads) |
spacesKey / spacesbucket | String? | DO Spaces coordinates |
userPrompt | String? | Original natural-language prompt (ai_generated only) |
requestedByName / requestedById | String? | Who triggered AI generation |
uploadedByName / uploadedById | String? | Who uploaded (dm_upload only) |
AI Generation Flow (ai_generated source)
User submits prompt + date range
│
▼
POST /api/reports/generate ← Dashboard API route
POST /dm/v1/reports/generate ← DM portal proxy → Fastify API
│
▼
Create Report(status="generating") in DB
│
▼
enqueueCustomReportWriter()
→ BullMQ queue: agent__custom-report-writer
│
▼
custom-report-writer.worker.ts
1. Load client context (context-file-writer output)
2. Load recent channel insights
3. Load goals (targets + actuals)
4. RAG search for relevant knowledge base content
5. Call Claude (claude-sonnet-4-6, 300s timeout)
6. Save markdown → Report.content, set status="ready"
7. Enqueue email notification to requesterQueue type
CustomReportWriterJobData in packages/queue/src/types.ts
Enqueue helper
enqueueCustomReportWriter() exported from @leadmetrics/queue
Worker registration
apps/servers/agents/src/index.ts
Monthly Report Agent Flow (agent source)
The activity planner creates a monthly_report activity as part of the standard deliverable plan. When the activity’s status reaches done, it appears on the Reports list pages automatically — no separate Report row is created; the Activity record is queried directly.
API Endpoints
| Method | Path | App | Notes |
|---|---|---|---|
| GET | /dm/v1/reports?tenantId= | Fastify API | Merged list of all sources, sorted by createdAt desc |
| GET | /dm/v1/reports/:id?tenantId= | Fastify API | Single report detail |
| POST | /dm/v1/reports | Fastify API | Upload markdown or PDF (dm_upload source) |
| POST | /dm/v1/reports/generate | Fastify API | Start AI generation (ai_generated source) |
| POST | /api/reports/generate | Dashboard Next.js route | Dashboard-side AI generation trigger (uses JWT session cookie) |
Dashboard list and detail pages query Prisma directly (server components) — they do not go through the Fastify API.
File Locations
Dashboard
| File | Purpose |
|---|---|
apps/dashboard/src/app/(dashboard)/reports/page.tsx | Server component — queries DB, passes data to client |
apps/dashboard/src/app/(dashboard)/reports/ReportsClient.tsx | List UI |
apps/dashboard/src/app/(dashboard)/reports/detail/[id]/page.tsx | Server component — fetches single report |
apps/dashboard/src/app/(dashboard)/reports/detail/[id]/ReportDetailClient.tsx | Detail UI + PDF download |
apps/dashboard/src/components/reports/GenerateReportModal.tsx | Prompt + date range modal |
apps/dashboard/src/app/api/reports/generate/route.ts | Next.js API route for AI generation |
DM Portal
| File | Purpose |
|---|---|
apps/dm/src/app/(dm)/reports/page.tsx | Server component |
apps/dm/src/app/(dm)/reports/DmReportsClient.tsx | List UI |
apps/dm/src/app/(dm)/reports/[id]/page.tsx | Server component |
apps/dm/src/app/(dm)/reports/[id]/ReportDetailClient.tsx | Detail UI + PDF download |
apps/dm/src/components/reports/GenerateReportModal.tsx | AI generation modal |
apps/dm/src/components/reports/UploadReportModal.tsx | PDF/Markdown upload modal |
apps/dm/src/app/api/dm/reports/route.ts | Proxy → Fastify (GET list, POST upload) |
apps/dm/src/app/api/dm/reports/generate/route.ts | Proxy → Fastify (POST generate) |
Backend
| File | Purpose |
|---|---|
packages/agents/src/workers/custom-report-writer.worker.ts | BullMQ worker — Claude call + DB save |
apps/api/src/routers/dm/reports.ts | Fastify router — all /dm/v1/reports routes |
packages/queue/src/types.ts | CustomReportWriterJobData type |
packages/queue/src/queues.ts | enqueueCustomReportWriter() helper |
Scheduled Performance Reports (source="scheduled")
Weekly and monthly reports generated automatically for all active tenants by the reporting server.
Schedule
| Frequency | Trigger | Period covered |
|---|---|---|
| Weekly | Friday 18:00–18:30 tenant local time | Monday – Friday of the current week |
| Monthly | Last day of month 18:00–18:30 tenant local time | 1st – last day of the month |
Flow
Reporting server cron (every 30 min)
→ checks Friday / last-day-of-month + 18:00 local window
→ skip if tenant has no channel insights at all
→ skip if report for this period already exists (dedupeKey check)
→ create Report(source="scheduled", frequency, status="generating")
→ enqueuePerformanceReportWriter()
↓
Agents server — performance-report-writer.worker.ts
→ db.agentConfig.findUnique({ role: "performance-report-writer" })
→ adapter, model, systemPrompt (editable from Manage portal)
→ load data in parallel:
- ClientContext.content
- ChannelInsight (current period, last 12, status=done)
- Goal (last 10)
- Previous Report of same frequency (for comparison; falls back to
most recent channel insights if no prior report exists)
→ createSkillsDir(tenantId, "performance-report-writer") → skills
→ getAdapter(adapter).execute(prompt, config, skillsDir)
→ save Report.content, status="ready"
→ create Notification(tenantId, type="performance_report_ready")
→ enqueueNotification email → all TenantMembers + all DM usersGuard
Tenants with zero ChannelInsight records (no integrations connected) are skipped entirely. The report will not be generated until at least one insight worker has run.
DedupeKey format
| Frequency | Key |
|---|---|
| Weekly | perf_weekly__{tenantId}__{YYYY-Www} |
| Monthly | perf_monthly__{tenantId}__{YYYY-MM} |
AgentConfig
role: "performance-report-writer" — seeded in packages/db/src/seed.ts. Adapter, model, and system prompt are all editable from the Manage portal → Agents → Performance Report Writer.
Report structure (16 sections)
Cover block · Executive Summary (KPI table + highlight cards) · Goal Progress · Organic Traffic · Organic Conversions · AI Search Visibility · Google Ads · Meta Ads · Google Business Profile · SEO Performance · Content Performance · Social Media · Email Marketing · Key Wins · Issues & Risks · Next Period Priorities
Notification recipients
| Channel | Recipients |
|---|---|
| In-app bell | All TenantMembers via Notification DB record (dashboard + DM portal) |
All TenantMembers (dashboard users) + all users with role: super_admin or reviewer (DM team) |
Email template slug: performance_report_ready — lightweight HTML with View Full Report CTA.
File locations
| File | Purpose |
|---|---|
apps/servers/reporting/src/jobs/performance-report.ts | Cron job — creates Report records + enqueues |
apps/servers/reporting/src/scheduler.ts | Adds weekly + monthly cron ticks |
packages/agents/src/workers/performance-report-writer.worker.ts | Worker — adapter/skills/AgentConfig pattern |
packages/queue/src/types.ts | PerformanceReportWriterJobData |
packages/queue/src/queues.ts | enqueuePerformanceReportWriter() |
Agent Chat Integration
The generateReport tool (packages/ai-chat/src/tools/analytics/generate-report.ts) lets users request reports from the AI Chat at /chat. The LangGraph analytics agent calls it when the user asks for a report, analysis, or performance summary. The tool creates the Report record, enqueues the BullMQ job, and returns a viewUrl link in the chat response.