Skip to Content
InsightsInsights — Implementation Plan

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

  1. Add ChannelInsight model to packages/db/prisma/schema.prisma (full spec in architecture.md)
  2. Add reverse relation insights ChannelInsight[] to the ConnectedChannel model
  3. Run pnpm --filter @leadmetrics/db db:migrate — creates channel_insight table with all indexes
  4. Add InsightAgentRole type union to packages/queue/src/types.ts (8 roles)
  5. Extend AgentRole to include InsightAgentRole
  6. Add InsightJobData interface to packages/queue/src/types.ts

Verify

  • pnpm --filter @leadmetrics/db db:migrate completes with no errors
  • pnpm tsc --noEmit across the monorepo passes

Phase 2 — API Routes

Goal: REST endpoints are live and returning correct responses.

Tasks

  1. Create apps/api/src/routes/insights.routes.ts with 5 endpoints:
    • POST /generate — validate channel ownership, create ChannelInsight { status: "pending" }, enqueue job, return { insightId }
    • GET / — list insights for tenant with pagination and optional channelType query filter
    • GET /:insightId — fetch single insight (tenant-scoped, 404 if not found or not owned)
    • GET /channel/:connectedChannelId — fetch the most recent insight for a channel
    • DELETE /:insightId — delete insight record (tenant-scoped)
  2. Register the route plugin under /tenant/v1 prefix in the Fastify app (alongside existing channels routes)
  3. Add channel type → agent role mapping helper (used by POST /generate and the first-connect trigger to pick the correct queue)
  4. Wire the first-connect auto-trigger in apps/api/src/routers/channels.ts:
    • After a channel connect succeeds and isConnected is set to true, check if a ChannelInsight record already exists for the connectedChannelId
    • If none exists and the channel type is in CHANNEL_INSIGHT_AGENT, create a ChannelInsight { status: "pending" } and enqueue the job with period: "last_6_months"
    • If the channel type is not supported, skip silently — no error thrown

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 /generate for an unsupported channel type returns 400
  • POST /generate for a disconnected channel returns 400
  • Connecting a supported channel for the first time automatically creates a ChannelInsight record with status: "pending"
  • Connecting an unsupported channel type does not create a ChannelInsight record and does not error
  • Reconnecting an already-connected channel (where a prior ChannelInsight exists) 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)

  1. Create packages/agents/src/workers/insights/ folder
  2. Build gsc-insights.worker.ts following the standard worker pattern — same rules as all other agents:
    • Load agentConfig.systemPrompt from agent_config table — throw if null, no hardcoded fallback
    • Load clientContext.content from client_context table — inject in full, same as content agents
    • Set ChannelInsight.status = "generating"
    • Decrypt tokenInfo via @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
  3. Add agent_config seed row for gsc-insights in packages/db/src/seed.ts with category: "insights" — system prompt is the source of truth, editable via manage portal
  4. 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 — add insights: "Insights" to CATEGORY_LABEL and "insights" to CATEGORY_ORDER
    • apps/dm/src/app/(dm)/agents/AgentsClient.tsx — same two additions
    • apps/manage/src/app/(manage)/agents/page.tsx — same two additions
  5. Register the worker in apps/servers/agents/src/index.ts
  6. Test end-to-end on a real GSC-connected tenant

Remaining Workers

Repeat the above 5 steps for each remaining worker:

Worker fileAgent roleProvider class
ga-insights.worker.tsga-insightsGoogleAnalyticsProvider
google-ads-insights.worker.tsgoogle-ads-insightsGoogleAdsProvider
meta-ads-insights.worker.tsmeta-ads-insightsMetaProvider
facebook-insights.worker.tsfacebook-insightsFacebookProvider
instagram-insights.worker.tsinstagram-insightsInstagramProvider
linkedin-insights.worker.tslinkedin-insightsLinkedInProvider
gbp-insights.worker.tsgbp-insightsGoogleBusinessProfileProvider

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

  1. Add a BullMQ repeatable job registration in apps/servers/agents/src/index.ts
  2. Cron expression: 0 3 * * 1 (Monday 03:00 UTC)
  3. Cron job handler:
    • Fetch all tenants with subscription.status = "active"
    • For each tenant, fetch all ConnectedChannel where isConnected: true and type is in the supported insight channel set
    • Enqueue one InsightJobData per channel with period: "last_6_months"
  4. 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.

  1. Add /insights to the sidebar navigation (after /channels, icon: Sparkles from lucide-react)
  2. Create apps/dashboard/src/app/(dashboard)/insights/page.tsx — Insights list (Screen I1)
  3. Create apps/dashboard/src/app/(dashboard)/insights/[insightId]/page.tsx — Insight detail (Screen I2)
  4. Add “Insights” tab to apps/dashboard/src/app/(dashboard)/channels/[id]/page.tsx (Screen I3)
  5. Wire “Generate Insights” / “Regenerate” → POST /tenant/v1/insights/generate
  6. 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)
  7. Render insights JSON as structured section cards (Strengths / Weaknesses / Opportunities / Recommendations)
  8. Render summary as 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.

  1. Run each agent on 3–5 real connected channels across different business types and industries
  2. Review output for: relevance, specificity (are metric values cited?), actionability, and absence of hallucinated claims
  3. Edit the systemPrompt for any underperforming agent directly in the manage portal → the next job run picks up the new prompt immediately
  4. Re-run affected workers and confirm improvement
  5. Once prompts are stable, copy the final versions back into packages/db/src/seed.ts so they survive a fresh database seed
  6. 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

  1. Add ChannelInsightAcceptance model to packages/db/prisma/schema.prisma and run migration
  2. Add channel_insights to STANDARD_DATASETS in packages/feature-knowledge/src/knowledge.types.ts — include it in the automatic dataset provisioning on tenant creation
  3. Add POST /:insightId/accept, DELETE /:insightId/accept/:acceptanceId, and GET /:insightId/acceptances endpoints to insights.routes.ts
  4. Update all 8 insight workers to pre-load prior accepted items from channel_insights before building the Claude prompt (step 7a in the worker flow)
  5. Update strategy-writer and report-writer dataset lists to include channel_insights
  6. Add accept/undo UI controls to each insight item card in Screens I2 and I3
  7. Add “N learnings saved” count to Screen I1 insight cards

Verify

  • Accepting an item creates ChannelInsightAcceptance and a rag_files entry with source: 'channel_insight_acceptance'
  • The chunk appears in Qdrant with the correct tenantId filter
  • Re-running an insight agent includes the PRIOR ACCEPTED INSIGHTS block 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.

© 2026 Leadmetrics — Internal use only