Insights — Implementation Plan
Build in phase order. Each phase is independently deployable and verifiable before moving on.
Phase 1 — Database & Types
Goal: New schema and type definitions in place; migration runs clean.
Tasks
- Add
ChannelInsightmodel topackages/db/prisma/schema.prisma(full spec in architecture.md) - Add reverse relation
insights ChannelInsight[]to theConnectedChannelmodel - Run
pnpm --filter @leadmetrics/db db:migrate— createschannel_insighttable with all indexes - Add
InsightAgentRoletype union topackages/queue/src/types.ts(8 roles) - Extend
AgentRoleto includeInsightAgentRole - Add
InsightJobDatainterface topackages/queue/src/types.ts
Verify
pnpm --filter @leadmetrics/db db:migratecompletes with no errorspnpm tsc --noEmitacross the monorepo passes
Phase 2 — API Routes
Goal: REST endpoints are live and returning correct responses.
Tasks
- Create
apps/api/src/routes/insights.routes.tswith 5 endpoints:POST /generate— validate channel ownership, createChannelInsight { status: "pending" }, enqueue job, return{ insightId }GET /— list insights for tenant with pagination and optionalchannelTypequery filterGET /:insightId— fetch single insight (tenant-scoped, 404 if not found or not owned)GET /channel/:connectedChannelId— fetch the most recent insight for a channelDELETE /:insightId— delete insight record (tenant-scoped)
- Register the route plugin under
/tenant/v1prefix in the Fastify app (alongside existingchannelsroutes) - Add channel type → agent role mapping helper (used by
POST /generateand the first-connect trigger to pick the correct queue) - Wire the first-connect auto-trigger in
apps/api/src/routers/channels.ts:- After a channel connect succeeds and
isConnectedis set totrue, check if aChannelInsightrecord already exists for theconnectedChannelId - If none exists and the channel type is in
CHANNEL_INSIGHT_AGENT, create aChannelInsight { status: "pending" }and enqueue the job withperiod: "last_6_months" - If the channel type is not supported, skip silently — no error thrown
- After a channel connect succeeds and
Channel Type → Agent Role Mapping
const CHANNEL_INSIGHT_AGENT: Record<string, InsightAgentRole> = {
"Google Search Console": "gsc-insights",
"Google Analytics": "ga-insights",
"Google Ads": "google-ads-insights",
"Meta Ads": "meta-ads-insights",
"Facebook": "facebook-insights",
"Instagram": "instagram-insights",
"LinkedIn": "linkedin-insights",
"Google Business Profile": "gbp-insights",
};Channels not in this map return a 400 Bad Request: Insights not supported for this channel type.
Verify
- API integration tests for all 5 endpoints (auth, ownership checks, create, list, get, get-by-channel, delete)
POST /generatefor an unsupported channel type returns 400POST /generatefor a disconnected channel returns 400- Connecting a supported channel for the first time automatically creates a
ChannelInsightrecord withstatus: "pending" - Connecting an unsupported channel type does not create a
ChannelInsightrecord and does not error - Reconnecting an already-connected channel (where a prior
ChannelInsightexists) does not enqueue a duplicate job
Phase 3 — Agent Workers
Goal: All 8 insight workers running and producing valid, structured output.
Start with GSC Insights as the reference implementation, then replicate the pattern.
Reference Worker (GSC)
- Create
packages/agents/src/workers/insights/folder - Build
gsc-insights.worker.tsfollowing the standard worker pattern — same rules as all other agents:- Load
agentConfig.systemPromptfromagent_configtable — throw if null, no hardcoded fallback - Load
clientContext.contentfromclient_contexttable — inject in full, same as content agents - Set
ChannelInsight.status = "generating" - Decrypt
tokenInfovia@leadmetrics/crypto, refresh OAuth token if needed - Fetch metrics via
GoogleSearchConsoleProvider - Build prompt:
systemPrompt+ client context block + metrics block (see architecture.md for the full assembly pattern) - Call Claude via
@leadmetrics/adapter-claude-local - Parse and validate JSON output (
sections+summary) - Write to
ChannelInsight { status: "done", insights, summary, costUsd, … } - Emit
publishAgentEvent("insight:completed") - On error:
status: "failed",errorMessage
- Load
- Add
agent_configseed row forgsc-insightsinpackages/db/src/seed.tswithcategory: "insights"— system prompt is the source of truth, editable via manage portal - Register
"insights"category in the three agents listing UIs (one-time change, do this alongside the first seed row):apps/dashboard/src/app/(dashboard)/settings/agents/AgentsPageClient.tsx— addinsights: "Insights"toCATEGORY_LABELand"insights"toCATEGORY_ORDERapps/dm/src/app/(dm)/agents/AgentsClient.tsx— same two additionsapps/manage/src/app/(manage)/agents/page.tsx— same two additions
- Register the worker in
apps/servers/agents/src/index.ts - Test end-to-end on a real GSC-connected tenant
Remaining Workers
Repeat the above 5 steps for each remaining worker:
| Worker file | Agent role | Provider class |
|---|---|---|
ga-insights.worker.ts | ga-insights | GoogleAnalyticsProvider |
google-ads-insights.worker.ts | google-ads-insights | GoogleAdsProvider |
meta-ads-insights.worker.ts | meta-ads-insights | MetaProvider |
facebook-insights.worker.ts | facebook-insights | FacebookProvider |
instagram-insights.worker.ts | instagram-insights | InstagramProvider |
linkedin-insights.worker.ts | linkedin-insights | LinkedInProvider |
gbp-insights.worker.ts | gbp-insights | GoogleBusinessProfileProvider |
Verify
For each worker: queue a job manually, confirm ChannelInsight.status transitions pending → generating → done, insights JSON matches the expected shape, and summary is non-empty markdown.
Phase 4 — Scheduled Trigger
Goal: Insights auto-refresh weekly without manual intervention.
Tasks
- Add a BullMQ repeatable job registration in
apps/servers/agents/src/index.ts - Cron expression:
0 3 * * 1(Monday 03:00 UTC) - Cron job handler:
- Fetch all tenants with
subscription.status = "active" - For each tenant, fetch all
ConnectedChannelwhereisConnected: trueandtypeis in the supported insight channel set - Enqueue one
InsightJobDataper channel withperiod: "last_6_months"
- Fetch all tenants with
- Log a summary of enqueued jobs (tenant count, channel count) at INFO level
Verify
- Advance the cron in the dev environment manually; confirm the expected number of jobs appear in the BullMQ queues
Phase 5 — Dashboard UI
Goal: Insights are visible, tappable, and update in real time.
Build order: API hooks → list page → detail page → channel tab → real-time.
- Add
/insightsto the sidebar navigation (after/channels, icon:Sparklesfrom lucide-react) - Create
apps/dashboard/src/app/(dashboard)/insights/page.tsx— Insights list (Screen I1) - Create
apps/dashboard/src/app/(dashboard)/insights/[insightId]/page.tsx— Insight detail (Screen I2) - Add “Insights” tab to
apps/dashboard/src/app/(dashboard)/channels/[id]/page.tsx(Screen I3) - Wire “Generate Insights” / “Regenerate” →
POST /tenant/v1/insights/generate - Show generating skeleton while
status === "pending" || status === "generating"— this covers the auto-triggered first-connect case where the tenant lands on the Insights tab immediately after connecting (WebSocket-driven or poll every 5s) - Render
insightsJSON as structured section cards (Strengths / Weaknesses / Opportunities / Recommendations) - Render
summaryas markdown below the cards
Full screen specs: ui.md
Verify
Playwright E2E:
- Connect GSC, navigate to
/insights, click “Generate Insights”, wait for the insight cards to appear - Verify all 4 section cards render with at least one item each
- Verify the markdown summary renders below the cards
Phase 6 — Prompt Tuning
Goal: Output quality is high enough to ship.
Because all 8 system prompts live in the agent_config table, they are editable at runtime via the manage portal — no code deploys needed for iteration. This is the same workflow used for tuning all other agents.
- Run each agent on 3–5 real connected channels across different business types and industries
- Review output for: relevance, specificity (are metric values cited?), actionability, and absence of hallucinated claims
- Edit the
systemPromptfor any underperforming agent directly in the manage portal → the next job run picks up the new prompt immediately - Re-run affected workers and confirm improvement
- Once prompts are stable, copy the final versions back into
packages/db/src/seed.tsso they survive a fresh database seed - Internal review sign-off on at least 2 sample insights per channel type before public release
Phase 7 — Accepted Learnings
Goal: Humans can accept individual insight items, persist them to the channel_insights
RAG dataset, and have agents automatically use them in future runs.
Full spec: accepted-learnings.md
Tasks
- Add
ChannelInsightAcceptancemodel topackages/db/prisma/schema.prismaand run migration - Add
channel_insightstoSTANDARD_DATASETSinpackages/feature-knowledge/src/knowledge.types.ts— include it in the automatic dataset provisioning on tenant creation - Add
POST /:insightId/accept,DELETE /:insightId/accept/:acceptanceId, andGET /:insightId/acceptancesendpoints toinsights.routes.ts - Update all 8 insight workers to pre-load prior accepted items from
channel_insightsbefore building the Claude prompt (step 7a in the worker flow) - Update
strategy-writerandreport-writerdataset lists to includechannel_insights - Add accept/undo UI controls to each insight item card in Screens I2 and I3
- Add “N learnings saved” count to Screen I1 insight cards
Verify
- Accepting an item creates
ChannelInsightAcceptanceand arag_filesentry withsource: 'channel_insight_acceptance' - The chunk appears in Qdrant with the correct
tenantIdfilter - Re-running an insight agent includes the
PRIOR ACCEPTED INSIGHTSblock in the prompt - Undoing an acceptance removes the Qdrant vectors
- Accepting the same item twice returns the existing record without duplicating RAG entries
Build Order Summary
Phase 1 (DB + Types)
↓
Phase 2 (API Routes)
↓
Phase 3 (Workers — GSC first, then remaining 7)
↓
Phase 4 (Cron) Phase 5 (UI)
↓ ↓
Phase 6 (Prompt Tuning)
↓
Phase 7 (Accepted Learnings)Phases 4 and 5 can begin in parallel once Phase 3’s first worker (GSC) is verified end-to-end.
Phase 7 requires Phase 5 (UI) and the channel_insights RAG dataset (Phase 1 additions) to be in place.