Skip to Content
Deliverable TrackingGoal Measurement — Implementation Spec

Goal Measurement — Implementation Spec

Problem

Goals created by the deliverable planner store a human-readable metric string and targetValue string (e.g., “organic sessions”, “+30%”). There is no automated measurement — progress is shown only as an activity completion percentage, not an actual business outcome.

This spec adds automated metric snapshots sourced from connected channels, so the Goals page can show real progress: baseline → current vs target.


Schema Changes

New enum: GoalMetricType

Machine-readable metric identifiers. Each value maps to a specific provider API call.

enum GoalMetricType { // Google Search Console GSC_CLICKS GSC_IMPRESSIONS GSC_AVG_POSITION // Google Analytics GA_SESSIONS GA_USERS GA_PAGEVIEWS GA_CONVERSIONS // Google Ads GOOGLE_ADS_CLICKS GOOGLE_ADS_IMPRESSIONS GOOGLE_ADS_CONVERSIONS GOOGLE_ADS_SPEND // Meta / Facebook / Instagram META_REACH META_IMPRESSIONS META_FOLLOWERS // LinkedIn LINKEDIN_IMPRESSIONS LINKEDIN_CLICKS LINKEDIN_FOLLOWERS }

Updated Goal model

New fields added alongside the existing metric (kept as human-readable display label):

FieldTypePurpose
metricTypeGoalMetricType?Machine-readable metric enum; set by strategy worker
targetNumericValueFloat?Machine-readable target (e.g. 15000 for “15,000 sessions”)
connectedChannelIdString?Which channel to pull metrics from
baselineValueFloat?Metric value at the time the plan was approved
snapshotsGoalSnapshot[]Weekly measurement history

New model: GoalSnapshot

One row per goal per weekly measurement run.

model GoalSnapshot { id String @id @default(cuid()) goalId String tenantId String connectedChannelId String? snapshotDate DateTime metricValue Float isBaseline Boolean @default(false) createdAt DateTime @default(now()) goal Goal @relation(...) tenant Tenant @relation(...) connectedChannel ConnectedChannel? @relation(...) @@index([goalId]) @@index([goalId, snapshotDate]) @@map("goal_snapshot") }

Queue

Job data

interface GoalTrackerJobData { goalId: string; tenantId: string; }

Queue name

agent__goal-tracker

Enqueue functions

  • enqueueGoalTracker({ goalId, tenantId }) — single goal snapshot
  • enqueueGoalTrackerBaseline({ planId, tenantId }) — enqueues one job per active goal on a plan

Worker

File: packages/agents/src/workers/jobs/goal-tracker.job.ts

Pattern: Same as gsc-keywords-snapshot.job.ts — no Claude call, no AgentConfig. Pure data fetch + DB write.

Per-job flow

  1. Load Goal — if no metricType or no connectedChannelId, skip silently
  2. Load ConnectedChannel — decrypt tokenInfo
  3. Refresh OAuth token if within 5 min of expiry (same helper as GSC snapshot worker)
  4. Compute date range: last 30 days (for trend), point-in-time yesterday for the value
  5. Call the appropriate provider method based on metricType
  6. Write GoalSnapshot { metricValue, snapshotDate: yesterday, isBaseline: false }
  7. If Goal.baselineValue is null → also set baselineValue and isBaseline: true on this snapshot

MetricType → Provider mapping

MetricTypeProviderMethodValue extracted
GSC_CLICKSGoogleSearchConsoleServicegetStats()sum of clicks
GSC_IMPRESSIONSGoogleSearchConsoleServicegetStats()sum of impressions
GSC_AVG_POSITIONGoogleSearchConsoleServicegetStats()avg of position
GA_SESSIONSGoogleAnalyticsServicegetOrganicTrafficMetrics()sessions
GA_USERSGoogleAnalyticsServicegetOrganicTrafficMetrics()users
GA_PAGEVIEWSGoogleAnalyticsServicegetOrganicTrafficMetrics()pageviews
GA_CONVERSIONSGoogleAnalyticsServicerunReport()conversions metric
GOOGLE_ADS_CLICKSGoogleAdsServicegetCampaignMetrics()clicks
GOOGLE_ADS_IMPRESSIONSGoogleAdsServicegetCampaignMetrics()impressions
GOOGLE_ADS_CONVERSIONSGoogleAdsServicegetCampaignMetrics()conversions
GOOGLE_ADS_SPENDGoogleAdsServicegetCampaignMetrics()spend
META_REACHMetaServicepage insightsreach
META_IMPRESSIONSMetaServicepage insightsimpressions
META_FOLLOWERSMetaServicepage insightsfollowers
LINKEDIN_IMPRESSIONSLinkedInServiceorg analyticsimpressions
LINKEDIN_CLICKSLinkedInServiceorg analyticsclicks
LINKEDIN_FOLLOWERSLinkedInServiceorg analyticsfollowerCount

Schedule

Weekly, Monday 03:30 UTC — 30 min after the insights job to avoid token refresh conflicts.

Trigger on plan approval

When a client approves a plan (POST /tenant/v1/deliverable-plan/:planId/approve), enqueueGoalTrackerBaseline() is called for that plan to capture the baseline value immediately.


Strategy Worker Changes

The deliverable planner prompt is updated to require metricType and targetNumericValue in the goals JSON output, alongside the existing human-readable fields.

Prompt addition (in buildPlannerPrompt)

"metricType": "one of: GSC_CLICKS | GSC_IMPRESSIONS | GSC_AVG_POSITION | GA_SESSIONS | GA_USERS | GA_PAGEVIEWS | GA_CONVERSIONS | GOOGLE_ADS_CLICKS | GOOGLE_ADS_IMPRESSIONS | GOOGLE_ADS_CONVERSIONS | GOOGLE_ADS_SPEND | META_REACH | META_IMPRESSIONS | META_FOLLOWERS | LINKEDIN_IMPRESSIONS | LINKEDIN_CLICKS | LINKEDIN_FOLLOWERS", "targetNumericValue": number — the absolute target value (e.g. 15000 for '15k sessions'),

PlannerOutput interface update

interface PlannerGoal { title: string; metric: string; // human-readable label (kept) metricType: string; // GoalMetricType enum value targetValue: string; // human-readable (kept) targetNumericValue: number; // machine-readable timeframeMonths: number; channel?: string | null; linkedDeliverableTypes: string[]; }

API Changes

New endpoint: GET /tenant/v1/goals/:goalId/progress

Returns current metric value, baseline, target, and time-series snapshots.

Response:

{ goal: { id, title, metric, metricType, targetValue, targetNumericValue, baselineValue, timeframeMonths, status }, progress: { currentValue: number | null, // latest snapshot value baselineValue: number | null, // value at plan approval changeAbsolute: number | null, // currentValue - baselineValue changePercent: number | null, // (change / baseline) × 100 targetPercent: number | null, // changePercent / target% (if targetNumericValue set) isAchieved: boolean }, snapshots: { snapshotDate: string; metricValue: number; isBaseline: boolean }[] }

New endpoint: GET /tenant/v1/goals

Lists all active goals for the tenant’s approved plan, with progress summary embedded.


UI Changes

Goals page (dashboard)

Each goal card shows two progress tracks:

  1. Activity progress (existing) — deliverables done / planned this period
  2. Metric progress (new) — real channel metric vs baseline vs target

New elements per card:

  • Progress bar: changePercent / targetPercent × 100%
  • Metric line: “14,200 sessions — up 25.1% from 11,350 baseline”
  • Target line: “Target: +30% (15,355 sessions) by month 6”
  • Status badge: On Track / Ahead / Behind / Achieved
  • Sparkline: weekly snapshot values (last 8 weeks)

DM portal parity

/dm/goals shows the same data read-only (no actions). Calls the same /tenant/v1/goals/:goalId/progress endpoint proxied through the DM proxy routes.


Progress Calculation

changeAbsolute = currentValue - baselineValue changePercent = (changeAbsolute / baselineValue) × 100 // For a "+30%" target expressed as targetNumericValue: // targetNumericValue = baselineValue × 1.30 (set by Claude at plan time) targetChangeNeeded = targetNumericValue - baselineValue progressToTarget = (changeAbsolute / targetChangeNeeded) × 100 // capped at 100 isAchieved = currentValue >= targetNumericValue

Status badge logic:

  • progressToTarget >= 100Achieved
  • progressToTarget >= 80On Track
  • progressToTarget >= 50Behind
  • progressToTarget < 50At Risk

Implementation Order

  1. Schema migration (GoalMetricType enum + GoalSnapshot model + Goal fields)
  2. packages/queueGoalTrackerJobData type + enqueueGoalTracker / enqueueGoalTrackerBaseline
  3. packages/agentsgoal-tracker.job.ts worker
  4. apps/servers/agents — register worker + weekly schedule
  5. apps/api — goals progress API endpoints
  6. apps/api/src/routers/tenant/main.ts — call enqueueGoalTrackerBaseline on plan approval
  7. packages/agents/src/workers/strategy.worker.ts — update prompt + parser
  8. Dashboard goals page — metric progress UI

© 2026 Leadmetrics — Internal use only