Skip to Content
CampaignsCampaigns — Data Model

Campaigns — Data Model

Related: API Routes | Workflow & Permissions | Prisma Schema


Model Index

#ModelPurposeCampaign Types
1Campaign (extended)Core campaign recordAll
2CampaignExternalMappingLinks to external platform campaign IDpaid_ads
3CampaignMetricsDaily performance snapshots per channelAll
4CampaignAudienceAudience segments (CRM, platform, custom list)All
5CampaignSequence + CampaignSequenceStepEmail drip sequence and stepsemail_marketing
6ReviewCampaignReview request drip configurationreview_generation
7Activity.campaignIdFK linking generated content to campaignAll
8AdGroupGoogle Ads ad groupspaid_ads (Google)
9CampaignKeywordBidding keywords with metricspaid_ads (Google)
10SearchTermReportRaw search queries from Google Adspaid_ads (Google)
11SearchTermClassificationAI + DM classification per search termpaid_ads (Google)
12NegativeKeywordNegative keywords at campaign / ad group levelpaid_ads (Google)
13MetaAdSetMeta ad sets with targeting + fatigue datapaid_ads (Meta)
14MetaAdIndividual Meta ad creativespaid_ads (Meta)
15LinkedInAdLinkedIn ad creativespaid_ads (LinkedIn)
16LinkedInDemographicBreakdownPer-dimension performance breakdownpaid_ads (LinkedIn)
17CampaignOptimizationRecommendationAI recommendations driving the Optimizations panelpaid_ads, seo_outreach, review_generation
18BacklinkHealthHTTP health status of acquired backlinksseo_outreach
19ReviewMetricsWeekly review velocity snapshots per platformreview_generation

1. Campaign (extended)

The existing Campaign model currently handles backlink outreach only. It is generalised to support all campaign types.

New / changed fields:

FieldTypeNotes
typeCampaignType enumpaid_ads | email_marketing | social_media | seo_outreach | review_generation
channelString[]e.g. ["google_ads", "meta_ads"]
goalCampaignGoal enumawareness | traffic | leads | sales | retention | reviews
statusCampaignStatus enumFull lifecycle — see Workflow
budgetDecimal?Optional; relevant for paid_ads campaigns
budgetCurrencyString?ISO currency code
startDateDateTime?Campaign start
endDateDateTime?Campaign end
deliverablePeriodIdString?Optional FK to DeliverablePeriod
briefText?AI-generated brief stored after campaign-brief-writer runs
autoPilotEnabledBooleanDefault false; paid_ads only — enables AI optimisation suggestions to be auto-queued (still requires double-approval)

Kept for backward compatibility: activityId, totalEmails, existing emails[] and backlinks[] relations.

Note on source: Add a source field (imported | created) to distinguish campaigns imported from a connected platform (Flow A) from campaigns created directly in Leadmetrics.


2. CampaignExternalMapping

Stores the link between a Leadmetrics Campaign record and the corresponding campaign(s) on external ad platforms. A single Leadmetrics campaign can map to one external campaign per platform (e.g. the same campaign tracks one Google Ads campaign and one Meta campaign).

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
connectedChannelIdStringFK → ConnectedChannel (identifies the platform and ad account)
externalCampaignIdStringThe platform’s own campaign ID (e.g. Google Ads campaign ID, Meta campaign ID)
externalCampaignNameStringName as it appears on the platform (synced on import)
platformStringgoogle_ads | meta_ads | linkedin_ads
lastSyncedAtDateTime?Timestamp of the last successful metrics pull
createdAtDateTime

Unique index on (campaignId, platform) — one mapping per platform per campaign.

This model is populated during Flow A (Import) and read during Flow B (Metrics Sync).


3. CampaignMetrics

Daily / weekly performance snapshots pulled from connected channel APIs.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
dateDateTimeSnapshot date
channelStringPlatform the metrics are from
impressionsInt?
clicksInt?
conversionsInt?
spendDecimal?Paid ads only
revenueDecimal?
ctrFloat?Click-through rate
cpcDecimal?Cost per click
roasFloat?Return on ad spend
openRateFloat?Email only
unsubscribesInt?Email only
reachInt?Social only
engagementInt?Social only

Unique index on (campaignId, date, channel) to prevent duplicate snapshots.

Metrics are populated by the google-ads-insights and meta-ads-insights workers during Flow B (Metrics Sync), using CampaignExternalMapping.externalCampaignId to scope the API call to the specific campaign rather than the whole ad account.


4. CampaignAudience

Audience segments attached to a campaign. Can be CRM-derived, platform-native, or custom.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
nameStringHuman-readable segment name
segmentTypeStringcrm_segment | platform_audience | custom_list
filtersJson?Age range, location, interests, lead tags, etc.
estimatedSizeInt?Estimated audience reach
platformAudienceIdString?External ID on Google / Meta / LinkedIn
createdAtDateTime

5. CampaignSequence + CampaignSequenceStep

Email drip sequences for email_marketing and review_generation campaigns.

CampaignSequence:

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
nameStringe.g. “Welcome Sequence”
triggerTypeStringon_subscribe | on_date | manual
statusStringdraft | active | paused | completed
createdAtDateTime

CampaignSequenceStep:

FieldTypeNotes
idStringCUID
sequenceIdStringFK → CampaignSequence
stepOrderInt1-based ordering
delayDaysIntDays after previous step (0 = immediate)
emailSubjectString
emailBodyText
statusStringdraft | approved | active
sentCountInt
openCountInt
clickCountInt

6. ReviewCampaign

Configuration for GBP review generation drip campaigns.

FieldTypeNotes
idStringCUID
tenantIdStringFK → Tenant
campaignIdStringFK → Campaign
channelStringemail | sms
sequenceJsonArray of { delayDays, messageTemplate } steps
contactListIdString?FK to leads / contacts query
statusStringdraft | active | paused | completed
startedAtDateTime?
completedAtDateTime?

7. Activity.campaignId (new FK)

Add an optional campaignId FK on Activity so that all content generated for a campaign (ad copy, emails, social posts) can be retrieved via the Campaign Content tab without joining through DeliverablePeriod.

campaignId String? campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)

8. AdGroup

Represents a Google Ads ad group within a campaign. Synced from the platform via GAQL.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
externalAdGroupIdStringGoogle Ads ad group ID
nameStringAd group name as on the platform
statusStringenabled | paused | removed (mirrored from platform)
defaultBidMicrosBigInt?Default CPC bid in micros (1,000,000 = $1.00)
lastSyncedAtDateTime?
createdAtDateTime

Unique index on (campaignId, externalAdGroupId).


9. CampaignKeyword

Keywords actively bidding within a campaign/ad group, synced from the platform. Distinct from the SEO-focused Keyword model.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
adGroupIdString?FK → AdGroup
externalKeywordIdStringGoogle Ads criterion ID
keywordStringKeyword text
matchTypeStringbroad | phrase | exact
statusStringenabled | paused | removed
bidMicrosBigInt?Keyword-level CPC bid override
qualityScoreInt?1–10 as reported by Google
impressionsInt?From last sync period
clicksInt?From last sync period
ctrFloat?
avgCpcDecimal?
conversionsFloat?
lastSyncedAtDateTime?
createdAtDateTime

Unique index on (campaignId, externalKeywordId).


10. SearchTermReport

Actual search queries that triggered ads, synced from search_term_view via GAQL. This is the raw data before classification.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
adGroupIdString?FK → AdGroup
searchTermStringThe actual query typed by the user
matchTypeStringHow it matched: broad | phrase | exact | broad_match_modifier
impressionsInt
clicksInt
costDecimalSpend for this term
conversionsFloat
ctrFloat
avgCpcDecimal
syncDateDateTimeDate range end of this sync batch
createdAtDateTime

Unique index on (campaignId, searchTerm, syncDate) to avoid duplicate rows per sync.


11. SearchTermClassification

Stores the AI-generated classification for each search term and tracks the human review decision. This is the HITL gate before pushing negative keywords to the platform.

FieldTypeNotes
idStringCUID
searchTermReportIdStringFK → SearchTermReport (unique — one classification per term)
campaignIdStringFK → Campaign (denormalised for query convenience)
aiClassificationStringAI suggestion: add_as_keyword | add_as_negative | watch | irrelevant
aiRationaleString?Brief reason from the AI
dmDecisionString?DM override: same enum values — null until reviewed
statusStringpending | dm_reviewed | pushed | skipped
pushedAtDateTime?When the negative keyword was pushed to Google Ads
createdAtDateTime

12. NegativeKeyword

Negative keywords at campaign or ad group level, both manually added and pushed from SearchTermClassification.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
adGroupIdString?FK → AdGroup (null = campaign-level negative)
keywordStringNegative keyword text
matchTypeStringbroad | phrase | exact
externalCriterionIdString?Google Ads criterion ID after push
sourceStringmanual | search_term_classification
statusStringpending_push | active | removed
createdAtDateTime

Unique index on (campaignId, keyword, matchType) to prevent duplicates.


13. MetaAdSet

Ad sets within a Meta Ads campaign. Each ad set has its own audience targeting, budget, and schedule. Synced from the Meta Marketing API.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
externalAdSetIdStringMeta’s ad set ID
nameStringAd set name
statusStringactive | paused | deleted | archived
dailyBudgetCentsInt?Daily budget in cents
lifetimeBudgetCentsInt?Lifetime budget in cents
targetingJson?Meta targeting spec (age, gender, geo, interests, custom audiences)
placementsJson?Placement config (automatic vs. manual placement list)
impressionsInt?From last sync period
clicksInt?
spendDecimal?
reachInt?Unique people reached
frequencyFloat?Avg times a person has seen the ads — key fatigue indicator
ctrFloat?
cpmDecimal?Cost per 1,000 impressions
cpcDecimal?
conversionsFloat?
roasFloat?
lastSyncedAtDateTime?
createdAtDateTime

Unique index on (campaignId, externalAdSetId).

Fatigue rule of thumb: frequency > 3.0 for cold (lookalike/interest) audiences or > 7.0 for warm (retargeting) audiences signals creative burnout — flag for refresh.


14. MetaAd

Individual ad creatives within a Meta ad set. Performance is tracked at this level to identify winning and fatigued creatives.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
adSetIdStringFK → MetaAdSet
externalAdIdStringMeta’s ad ID
nameString
statusStringactive | paused | deleted | archived
formatStringimage | video | carousel | collection | lead | instant_experience
creativePreviewUrlString?Thumbnail / preview image URL
headlineString?Primary text / headline used
impressionsInt?
clicksInt?
spendDecimal?
frequencyFloat?Ad-level frequency — more granular than ad set
ctrFloat?
cpcDecimal?
conversionsFloat?
roasFloat?
fatigueFlagBooleanSet to true by meta-ads-insights when frequency is high + CTR is declining
lastSyncedAtDateTime?
createdAtDateTime

Unique index on (campaignId, externalAdId).


15. LinkedInAd

Individual ad creatives within a LinkedIn Ads campaign. LinkedIn’s hierarchy is Campaign Group → Campaign → Ad. We track at ad level for creative performance.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
externalAdIdStringLinkedIn’s creative ID
nameString
statusStringactive | paused | archived | draft
formatStringsponsored_content | message_ad | text_ad | dynamic_ad | lead_gen_form
headlineString?
introTextString?Introductory text (Sponsored Content)
leadGenFormIdString?LinkedIn Lead Gen Form ID if applicable
impressionsInt?
clicksInt?
spendDecimal?
frequencyFloat?LinkedIn computes this — fatigue happens fast on small B2B audiences
ctrFloat?
cpcDecimal?
cpmDecimal?
conversionsFloat?
leadGenFormCompletionsInt?For Lead Gen Form ads only
leadGenFormCompletionRateFloat?Form completions / ad clicks
fatigueFlagBooleanSet by linkedin-ads-insights when frequency is high
lastSyncedAtDateTime?
createdAtDateTime

Unique index on (campaignId, externalAdId).


16. LinkedInDemographicBreakdown

LinkedIn’s unique strength is demographic targeting. This model stores performance broken down by audience dimension — used to identify which job functions, seniority levels, industries, and company sizes are converting.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
syncDateDateTimeDate range end of this sync batch
dimensionTypeStringjob_function | seniority | industry | company_size | job_title | company
dimensionValueStringe.g. "Engineering", "Director", "1-10 employees"
impressionsInt?
clicksInt?
spendDecimal?
conversionsFloat?
ctrFloat?
cpcDecimal?
createdAtDateTime

Unique index on (campaignId, syncDate, dimensionType, dimensionValue).

This data feeds the AI recommendation to tighten or expand LinkedIn audience segments (e.g. “Director and VP seniority converting at 3× the rate of Manager — recommend excluding Manager”).


17. CampaignOptimizationRecommendation

Stores every AI-generated optimisation recommendation for a campaign — whether from a scheduled weekly scan or a manual trigger. This is the single model that drives the Optimizations panel in the UI. Nothing is pushed to a platform until the recommendation reaches applied status via the HITL approval chain.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
platformStringmeta_ads | linkedin_ads | google_ads
typeStringSee recommendation types below
titleStringShort human-readable title, e.g. “Refresh fatigued ad — Summer Promo V1”
rationaleTextAI explanation of why this is recommended + supporting data
suggestedActionJsonStructured payload describing the change (pause ad set ID, new budget amount, etc.)
estimatedImpactString?AI estimate of expected improvement, e.g. “~15% CTR recovery”
priorityStringhigh | medium | low — set by the optimizer worker based on spend/impact
statusStringpending | dm_reviewing | dm_approved | client_review | client_approved | applied | skipped | expired
dmNoteString?DM comments when approving or skipping
clientNoteString?Client comments when approving or rejecting
appliedAtDateTime?When the change was pushed to the platform
expiresAtDateTime?Stale recommendations expire after 14 days if not actioned
sourceJobIdString?BullMQ job ID that generated this recommendation
createdAtDateTime

Recommendation types:

TypePlatformDescription
creative_refreshMeta, LinkedInFatigued ad — generate new variant
pause_adMeta, LinkedInPause a specific underperforming or fatigued ad
pause_ad_setMetaPause a whole ad set (all audience exhausted or ROAS < 0)
budget_shiftMetaMove daily budget from a low-ROAS ad set to a high-ROAS one
pause_placementMetaDisable a specific placement (e.g. Audience Network) within an ad set
audience_tightenLinkedInNarrow targeting to top-converting demographic segment
audience_excludeLinkedIn, MetaAdd an exclusion (converted leads, competitor employees)
bid_adjustGoogle, LinkedInChange CPC bid for keyword or campaign
refresh_contact_listMeta, LinkedInRe-upload updated CRM audience list for custom audience
lead_form_refreshLinkedInLead Gen Form copy is stale or completion rate dropped — refresh
audience_expandMetaCold audience exhausted — suggest new interest or lookalike
refresh_outreach_templateSEO OutreachLow reply rate on an outreach email step — rewrite copy
add_follow_up_stepSEO OutreachSequence ended without a reply — AI drafts an additional follow-up step
reprioritize_prospectsSEO OutreachDomain authority scores have shifted — reorder the target prospect list
flag_dead_backlinkSEO OutreachA previously acquired backlink has gone dead — re-engage the domain
suggest_disavowSEO OutreachSuspicious / spammy backlink pattern detected — add domain to disavow file
refresh_review_sequence_stepReview GenerationLow open or click rate on a drip step — rewrite subject line or body
re_engage_non_respondersReview GenerationContacts who completed the sequence without leaving a review — send a different angle
adjust_send_scheduleReview GenerationSend time analysis shows a higher-converting window — adjust drip schedule
shift_review_platformReview GenerationOne review platform is lagging behind — redirect the next outreach batch
flag_negative_review_patternReview GenerationMultiple recent reviews share the same negative theme — flag for tenant action

Unique constraint: one pending or dm_reviewing recommendation per (campaignId, platform, type, suggestedAction.targetId) to prevent duplicate recommendations for the same object.


18. BacklinkHealth

Tracks the live status of each backlink acquired through an seo_outreach campaign. A weekly background job performs HTTP HEAD checks on each URL and updates the status. Dead or nofollowed backlinks surface as flag_dead_backlink recommendations.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
domainStringRoot domain of the linking site (e.g. techcrunch.com)
urlStringFull URL of the page containing the link
anchorTextString?Anchor text used for the link
statusStringunchecked | live | dead | nofollow | redirected | blocked
httpStatusCodeInt?Last HTTP response code (200, 301, 404, etc.)
isDoFollowBoolean?Whether the link is dofollow — checked via rel attribute scan
domainAuthorityInt?DA/DR score at last check (sourced from third-party API or manual entry)
acquiredAtDateTime?When the backlink was first confirmed live
lastCheckedAtDateTime?Timestamp of the last health check
createdAtDateTime

Unique index on (campaignId, url).

Health check logic: A HEAD request to url is made. If HTTP status ≠ 2xx → dead. If redirect chain lands on a different domain → redirected. If the page is reachable but the link can no longer be found (requires full GET + HTML parse) → dead. If rel="nofollow" is present → nofollow. Status blocked is set when the domain returns 403/rate-limit consistently.


19. ReviewMetrics

Weekly snapshot of reviews received per platform for a review_generation campaign. Used to track review velocity, star rating trends, and negative theme patterns.

FieldTypeNotes
idStringCUID
campaignIdStringFK → Campaign
platformStringgoogle | trustpilot | g2 | yelp | tripadvisor | other
weekStartDateDateTimeMonday of the reporting week
newReviewCountIntReviews received during this week
avgRatingFloat?Average star rating for new reviews (1–5)
totalReviewCountInt?Cumulative total on the platform (synced from connected channel or manually entered)
negativeCountInt?Reviews with rating ≤ 2 this week
sentimentScoreFloat?AI-computed sentiment score across new review text (−1 negative → +1 positive)
keyThemesJson?Array of recurring themes extracted from review text by AI — e.g. ["slow response", "great onboarding"]
createdAtDateTime

Unique index on (campaignId, platform, weekStartDate).

Populated by the review-campaign-optimizer worker or manually. keyThemes and sentimentScore are AI-generated — they feed the flag_negative_review_pattern recommendation type.


Periodic optimization activities, thresholds, and the weekly scheduler are documented in the Periodic Optimization Automation section of workflow.md.

Provider API method extensions required to support these models are documented in api.md — Provider Extensions.

© 2026 Leadmetrics — Internal use only