Skip to Content
CampaignsCampaigns — Workflow & Permissions

Campaigns — Workflow & Permissions

Related: Data Model | API Routes | HITL


Status Lifecycle

draft dm_review ← DM team reviews brief, content, audience dm_approved ← DM approves; ready for client client_review ← Client sees campaign summary; reviews brief + creatives client_approved ← Client approves; campaign ready to launch active ← Campaign is live on connected channels ├──► paused ← Manually paused by DM (can resume → active) completed ← End date passed or manually marked done archived ← Soft-deleted; no further edits

Allowed transitions:

FromToWho can trigger
draftdm_reviewadmin, member
dm_reviewdm_approvedadmin, member
dm_reviewdraftadmin, member (send back for revisions)
dm_approvedclient_reviewadmin, member
dm_approveddm_reviewadmin, member (reopen for edits)
client_reviewclient_approvedreviewer (client), admin
client_reviewdm_reviewreviewer (client) — requests changes
client_approvedactiveadmin, member
activepausedadmin, member
pausedactiveadmin, member
activecompletedadmin, member (or automatic on endDate)
anyarchivedadmin only

Role Gates

Campaigns reuse the existing TenantMember role system.

Actionadminmemberreviewer (client)
Create campaign
Edit campaign (draft → dm_approved)
Submit for client review
View campaign detail✅ (client_review only)
Approve / reject (client review)
Launch (client_approved → active)
Pause / resume
Enable auto-pilot
Archive
View performance metrics✅ (own campaigns)
Trigger agent (brief, audience, content)

Auto-Pilot Mode

Auto-pilot is an opt-in feature for paid_ads, seo_outreach, and review_generation campaigns. When enabled, AI optimizer workers may propose optimisation batches automatically on a schedule (e.g. weekly).

Safeguards:

  • Only admin users can enable auto-pilot (autoPilotEnabled flag on Campaign).
  • Each batch of AI-proposed changes still enters the normal approval workflow: dm_review → dm_approved → client_review → client_approved.
  • No change is pushed to Google Ads / Meta APIs until client_approved.
  • A clear audit log entry is written for every auto-pilot suggestion, approval, and platform push.

Auto-pilot does not mean automatic execution. It means automatic suggestion generation on a schedule, not automatic platform changes.


Notifications

Status transitions should trigger notifications (to be confirmed as part of the notification system design — see Open Questions):

TransitionNotify
draft → dm_reviewAll DM team members
dm_approved → client_reviewClient users (reviewer role)
client_review → client_approvedDM team members who submitted for review
client_review → dm_review (rejected)DM team with client note
client_approved → activeClient users + DM team
active → pausedDM team + client users
completedDM team + client users

Periodic Optimization Automation

This describes the full lifecycle of a weekly automated optimization check for paid_ads campaigns on Meta and LinkedIn.

Scheduler Design

A BullMQ cron job runs once per week (e.g. Sunday 02:00 UTC) and enqueues one campaign-optimizer-runner job per active paid_ads, seo_outreach, or review_generation campaign. This runner does the threshold checks in code, then enqueues the AI workers for campaigns with hits.

Weekly BullMQ cron (Sunday 02:00 UTC) ├── Query: all active paid_ads campaigns with autoPilotEnabled = true │ OR seo_outreach campaigns (always eligible) │ OR review_generation campaigns (always eligible) └── For each campaign: └── Enqueue campaign-optimizer-runner Read stored data per campaign type: paid_ads → MetaAdSet, MetaAd, LinkedInAd, LinkedInDemographicBreakdown, CampaignMetrics seo_outreach → CampaignSequenceStep stats, BacklinkHealth review_gen → CampaignSequenceStep stats, ReviewMetrics Run threshold checks (in-process, no AI): ├── Meta: frequency > 3.0 cold? → creative_refresh candidate ├── Meta: any adset ROAS > 2× avg? → budget_shift candidate ├── Meta: placement CPM > 2× avg? → pause_placement candidate ├── Meta: campaign frequency > 4.0? → audience_expand candidate ├── LinkedIn: ad frequency > 2.0? → creative_refresh candidate ├── LinkedIn: demographic CTR outlier? → audience_tighten candidate ├── LinkedIn: CPC up > 20% WoW? → bid_adjust candidate ├── LinkedIn: form completion rate down > 15%? → lead_form_refresh ├── SEO: sequence step open rate < 20%? → refresh_outreach_template ├── SEO: final step sent 7+ days ago, 0 replies? → add_follow_up_step ├── SEO: BacklinkHealth status → dead/nofollow? → flag_dead_backlink ├── Review: sequence step open rate < 20%? → refresh_review_sequence_step ├── Review: review velocity drop > 30%? → re_engage_non_responders └── Review: negative review rate > 25%? → flag_negative_review_pattern For each campaign type with threshold hits: ├── Enqueue meta-ads-optimizer (if Meta hits) ├── Enqueue linkedin-ads-optimizer (if LinkedIn hits) ├── Enqueue seo-outreach-optimizer (if SEO hits) └── Enqueue review-campaign-optimizer (if Review Gen hits) Write CampaignOptimizationRecommendation records (status: pending) For creative_refresh / refresh_outreach_template / refresh_review_sequence_step type: └── Also enqueue the relevant writer worker Linked via Activity.campaignId

Per-Campaign-Type Thresholds and Activities

Meta Ads

Weekly (automated threshold scan):

ActivityWhat’s checkedThresholdRecommendation type
Creative fatigue detectionMetaAd.frequency AND week-over-week CTR trendfrequency > 3.0 (cold audience) / > 7.0 (warm/retargeting) + CTR decliningcreative_refresh
Fatigued ad pauseSame as above but severe (frequency > 5.0 cold / > 10.0 warm)Hard threshold — no CTR condition requiredpause_ad
Ad set budget rebalanceCompare MetaAdSet.roas across ad sets in same campaignAny ad set with ROAS > 2× campaign avg is budget-constrainedbudget_shift
Placement performance reviewPer-placement CPM vs campaign avgCPM > 2× avg OR CTR < 50% avg for a specific placementpause_placement
Frequency cap alertCampaign-level aggregate frequency> 4.0 cold / > 8.0 warmaudience_expand
Zero-conversion ad setAd sets spending with 0 conversions for 7 days0 conversions + spend > $50 equivalentpause_ad_set

Monthly (partially manual, partially automated):

ActivityHow
Custom audience refreshRe-upload updated CRM lead/customer list to Meta. Triggered manually from Audience tab → “Refresh Custom Audience” button. Platform: POST /act_{id}/customaudiences
Lookalike audience rebuildWhen source custom audience grows > 20%, rebuild lookalike. Flagged by optimizer as refresh_contact_list recommendation
Attribution window reviewManual DM check — verify 7-day click window still matches sales cycle length. No automation, surfaced as a reminder in Performance tab
Campaign Budget Optimization (CBO) checkReview if CBO is bottlenecking high-ROAS ad sets. Surfaced in budget_shift recommendation rationale

LinkedIn Ads

Weekly (automated threshold scan):

ActivityWhat’s checkedThresholdRecommendation type
Creative fatigue detectionLinkedInAd.frequency AND week-over-week CTR trendfrequency > 2.0 (B2B audiences are much smaller — fatigue hits faster)creative_refresh
Demographic opportunityLinkedInDemographicBreakdown CTR / conversion outliersAny dimension value with CTR > 2× campaign avg or disproportionate conversionsaudience_tighten
CPC spike monitoringWeek-over-week CPC changeCPC rises > 20% week-over-weekbid_adjust
Lead Gen Form dropLinkedInAd.leadGenFormCompletionRate week-over-week changeDrop > 15% week-over-weeklead_form_refresh
Zero-conversion adSpend with no conversions for 7 days0 conversions + spend > equivalent of $100pause_ad

Monthly (partially manual, partially automated):

ActivityHow
Audience exclusion refreshSync converted leads from CRM into a LinkedIn Matched Audience exclusion list. Triggered from Audience tab. Recommendation type: audience_exclude
Matched audience / contact list refreshRe-upload updated CRM list. Recommendation type: refresh_contact_list
Bid strategy reviewManual DM review — compare maximum delivery vs target cost performance. Surfaced as bid_adjust recommendation with strategy rationale
Ad format rotationWhen all active creatives are fatigued, optimizer suggests trying a different format (e.g. Sponsored Content → Message Ad → Lead Gen Form)
InMail sender profile checkLinkedIn Message Ad open rates are heavily influenced by the sender’s profile completeness. Manual check; noted in lead_form_refresh rationale if open rate is low

SEO Outreach

Weekly (automated threshold scan):

ActivityWhat’s checkedThresholdRecommendation type
Outreach email reply rateCampaignSequenceStep.clickCount / sentCount (used as a reply proxy for email-tracked outreach)Open rate < 20% or click rate < 3% for any active steprefresh_outreach_template
Sequence gap detectionActive outreach sequences where all steps are sent but 0 replies after the final stepFinal step sent > 7 days ago, 0 replies recordedadd_follow_up_step
Backlink health checkBacklinkHealth.status for all acquired backlinksAny status change to dead, nofollow, or redirectedflag_dead_backlink

Monthly (partially manual, partially automated):

ActivityHow
Domain authority rescoreFetch updated DA/DR for the target prospect list. If high-priority domains drop significantly, reorder the queue. Recommendation type: reprioritize_prospects
Disavow file reviewAI scans new backlinks pointing to the tenant’s domain (via Google Search Console integration or manual import) for spammy patterns. Recommendation type: suggest_disavow
Prospect list refreshAdd new link-building targets based on fresh competitor backlink gap analysis. DM-triggered from the campaign Audience tab
Bounce / deliverability cleanupIdentify hard-bounced outreach contacts from the email provider and remove from future steps

Note on “replies”: Email-based backlink outreach reply tracking requires either an email provider webhook (e.g. Mailchimp reply tracking) or manual DM entry. Until that is integrated, the clickCount on the “Check our website” CTA link in the email is used as a proxy for engagement.

Review Generation

Weekly (automated threshold scan):

ActivityWhat’s checkedThresholdRecommendation type
Drip step open rateCampaignSequenceStep.openCount / sentCountOpen rate < 20% for any active steprefresh_review_sequence_step
Drip step click rateCampaignSequenceStep.clickCount / openCountClick-to-open rate < 10% (suggests CTA or body is weak)refresh_review_sequence_step
Review velocity dropReviewMetrics.newReviewCount week-over-weekCount drops > 30% vs prior weekre_engage_non_responders
Negative review spikeReviewMetrics.negativeCount / newReviewCount> 25% of new reviews are ≤ 2 starsflag_negative_review_pattern
Platform imbalanceCompare ReviewMetrics.totalReviewCount across platforms for the same campaignOne platform has < 30% of the count of the leading platformshift_review_platform

Monthly (partially manual, partially automated):

ActivityHow
New customer auto-enrollContacts added to the CRM in the past month who haven’t yet been enrolled in the drip are identified and batch-enrolled. Triggered automatically if the ReviewCampaign has status: active and the contact list query is dynamic
Send time optimizationreview-campaign-optimizer analyzes click-to-review conversion rates by send hour/day of week across all steps and recommends the best send window. Recommendation type: adjust_send_schedule
Review platform expansionIf the campaign is only targeting Google Reviews but the tenant has a Trustpilot or G2 profile, optimizer surfaces this as a shift_review_platform recommendation
Sequence refreshIf average open rate across all steps drops below 15% for 4 consecutive weeks, optimizer recommends a full sequence rewrite. Enqueues review-campaign-writer for new draft

Recommendation HITL Flow

CampaignOptimizationRecommendation created (status: pending) DM notified (in-app + email) — "X new optimisation tasks for [Campaign Name]" DM opens Optimizations panel in Campaign Detail ├── Review recommendation (rationale, estimated impact, suggested action) ├── Skip → status: skipped / dmNote recorded │ Recommendation expires after 14 days if not actioned └── Approve → status: dm_approved Determine if client approval required: ├── Budget or bid change only AND amount < threshold ($X) → admin can apply directly │ └── status: client_approved (system actor) └── Creative change, pause, or audience change → client_review required Client notified → reviews in Campaign Detail (reviewer role) ├── Reject → status: dm_reviewing / clientNote recorded │ DM notified to revise └── Approve → status: client_approved Platform action queued (BullMQ job) Action pushed via Meta Marketing API / LinkedIn Ads API status: applied / appliedAt stamped Audit log entry written

Role Gates for Optimizations

Actionadminmemberreviewer (client)
View Optimizations panel✅ (own campaigns)
Skip a recommendation
DM-approve a recommendation
Apply directly (budget/bid only)
Client-approve a recommendation
Client-reject a recommendation
Trigger manual optimization scan

Notifications for Optimizations

EventNotify
New recommendations generated (weekly scan)DM team members for that tenant
Recommendation sent to client reviewClient (reviewer role)
Client rejects a recommendationDM team with client note
Recommendation applied to platformDM team + client
Recommendation expired (14 days old, still pending)DM team reminder

Audit Log

Every status transition, approval, rejection, and platform push must be written to the tenant audit log with:

  • entityType: "campaign"
  • entityId: campaign.id
  • action: e.g. campaign.status_changed, campaign.approved, campaign.optimisation_applied
  • actor: userId who performed the action
  • payload: { from, to, note? } for status changes

For optimization recommendations, additionally log:

  • entityType: "campaign_optimization_recommendation"
  • entityId: recommendation.id
  • Actions: recommendation.created, recommendation.dm_approved, recommendation.skipped, recommendation.client_approved, recommendation.client_rejected, recommendation.applied, recommendation.expired

© 2026 Leadmetrics — Internal use only