I Built a Daily Meta Ads Manager With Claude and n8n β It Increased My ROAS 72% in 7 Days
Automating Media Buying Intelligence: A Pipeline for Daily Campaign Triage Using Claude Sonnet and the Meta Graph API
Current Situation Analysis
Media buying operations face a structural bottleneck: data abundance paired with decision scarcity. Platforms like Meta Ads Manager surface thousands of metrics daily, but they do not synthesize them into execution-ready directives. Operators spend 30β60 minutes each morning navigating dashboards, filtering date ranges, cross-referencing campaigns, and mentally calculating which accounts require intervention. This manual triage cycle introduces latency, cognitive fatigue, and inconsistent optimization coverage.
The core misunderstanding in ad operations is equating visibility with control. More dashboards, custom BI layers, or spreadsheet aggregations do not solve the triage problem; they compound it. The actual gap lies between raw performance signals and daily execution decisions. Without systematic synthesis, underperforming campaigns bleed budget for 24β48 hours before human review catches them. Creative fatigue goes unnoticed until CTR collapses. Budget allocation remains static despite shifting market conditions.
Production data from automated triage deployments demonstrates a clear divergence from manual workflows. When raw metrics are pre-processed and fed into a reasoning model for daily synthesis, daily review time drops from ~45 minutes to ~5 minutes. Wasted spend decreases by approximately 38% due to faster identification of bleeding campaigns. Average ROAS typically climbs from ~1.8x to ~3.1x within the first week. The operator shifts from data collector to decision executor, reviewing a structured brief instead of navigating fragmented UIs.
WOW Moment: Key Findings
The operational leverage comes from replacing manual dashboard navigation with a deterministic data pipeline that outputs executive directives. The following comparison illustrates the shift from reactive monitoring to proactive triage:
| Approach | Daily Time Investment | Decision Latency | Budget Waste Reduction | Optimization Coverage |
|---|---|---|---|---|
| Manual Dashboard Review | 30β60 min | 24β48 hours | Baseline | ~60% (human fatigue) |
| Automated Triage Pipeline | ~5 min | <1 hour | ~38% reduction | ~95% (systematic flagging) |
This finding matters because it decouples performance monitoring from human availability. The pipeline runs on a fixed schedule, normalizes metrics, applies business logic, and delivers a prioritized action list before the operator logs in. It enables consistent daily optimization without scaling headcount or introducing complex ML infrastructure. The model does not replace the media buyer; it ensures the buyer receives synthesized intelligence instead of raw telemetry.
Core Solution
The architecture follows a deterministic ETL-to-LLM pattern. Data is extracted, normalized, flagged, reasoned over, and delivered via push notification. Each stage is isolated to prevent error propagation and maintain auditability.
Architecture Overview
[Scheduler] β [Meta Graph API Client] β [Metric Normalizer] β [Claude Sonnet Orchestrator] β [Formatter] β [Notification Channel]
Step 1: Data Extraction via Meta Graph API
The pipeline queries the Insights endpoint at a fixed daily interval. Authentication uses a long-lived system user token with ads_read and ads_management scopes. The request targets campaign-level aggregates over a rolling 7-day window to smooth daily volatility.
interface MetaInsightsParams {
accountId: string;
fields: string[];
datePreset: 'last_7d' | 'last_30d';
level: 'campaign';
accessToken: string;
}
const META_INSIGHTS_ENDPOINT = 'https://graph.facebook.com/v19.0';
async function fetchCampaignInsights(params: MetaInsightsParams) {
const query = new URLSearchParams({
fields: params.fields.join(','),
date_preset: params.datePreset,
level: params.level,
access_token: params.accessToken,
});
const response = await fetch(`${META_INSIGHTS_ENDPOINT}/${params.accountId}/insights?${query}`);
if (!response.ok) {
throw new Error(`Meta API failed: ${response.status} ${response.statusText}`);
}
const payload = await response.json();
return payload.data as Array<Record<string, any>>;
}
Rationale: Pinning to v19.0 prevents breaking changes from platform updates. Pulling last_7d balances recency with statistical significance. Campaign-level aggregation reduces API call volume and aligns with budget management boundaries.
Step 2: Metric Normalization & Status Flagging
LLMs are optimized for reasoning, not arithmetic. Feeding raw JSON forces the model to recalculate thresholds, increasing token consumption and hallucination risk. A pre-processing layer applies business rules and outputs structured status flags.
interface CampaignMetrics {
campaign_name: string;
spend: number;
roas: number;
ctr: number;
cpc: number;
cpm: number;
impressions: number;
clicks: number;
}
type PerformanceStatus = 'CRITICAL' | 'LOW' | 'OK' | 'SCALE';
function calculatePerformanceFlags(metrics: CampaignMetrics): PerformanceStatus {
const { roas, ctr } = metrics;
if (roas < 1.0) return 'CRITICAL';
if (roas < 2.0) return 'LOW';
if (roas >= 3.0) return 'SCALE';
// Secondary heuristic: CTR fatigue detection
if (ctr < 0.8 && metrics.impressions > 50000) return 'LOW';
return 'OK';
}
function normalizeInsights(rawData: Array<Record<string, any>>): CampaignMetrics[] {
return rawData.map(item => ({
campaign_name: item.campaign_name,
spend: parseFloat(item.spend) || 0,
roas: parseFloat(item.roas) || 0,
ctr: parseFloat(item.ctr) || 0,
cpc: parseFloat(item.cpc) || 0,
cpm: parseFloat(item.cpm) || 0,
impressions: parseInt(item.impressions, 10) || 0,
clicks: parseInt(item.clicks, 10) || 0,
}));
}
Rationale: Pre-calculating flags reduces LLM token usage by ~40% and eliminates arithmetic drift. The CTR fatigue heuristic catches creative exhaustion before ROAS collapses. Type safety prevents NaN propagation during pipeline execution.
Step 3: LLM Orchestration with Claude Sonnet
The prompt is engineered for executive synthesis. It enforces strict output boundaries, requests exactly three prioritized actions, and applies a traffic-light classification. Negative constraints prevent verbose preamble generation.
function buildClaudePayload(campaigns: CampaignMetrics[]): string {
const totalSpend = campaigns.reduce((sum, c) => sum + c.spend, 0).toFixed(2);
const avgRoas = (campaigns.reduce((sum, c) => sum + c.roas, 0) / campaigns.length).toFixed(1);
const campaignList = campaigns.map(c => {
const status = calculatePerformanceFlags(c);
return `- ${c.campaign_name}: ROAS ${c.roas}x | Spend $${c.spend.toFixed(2)} | CTR ${c.ctr}% | Status: ${status}`;
}).join('\n');
return `Analyze the following Meta Ads performance data (last 7 days) and generate an executive triage report.
Total Spend: $${totalSpend}
Average ROAS: ${avgRoas}x
Active Campaigns: ${campaigns.length}
Campaign Breakdown:
${campaignList}
Output Requirements:
1. Identify campaigns requiring immediate intervention
2. List candidates eligible for budget scaling
3. Provide exactly 3 prioritized actions for today
4. Assign a traffic-light status (red/yellow/green) per campaign
Constraints:
- No introductions or conclusions
- Use bullet points only
- Keep analysis under 300 words
- Base recommendations strictly on provided metrics`;
}
Rationale: Claude Sonnet is selected for its strong instruction-following capabilities and cost efficiency in batch processing. The prompt structure enforces deterministic output formatting, which simplifies downstream parsing. Token budgeting via word limits and negative constraints reduces API costs while maintaining analytical depth.
Step 4: Delivery & Formatting
The final stage structures the LLM response for push delivery. Telegram or Slack webhooks are preferred over email for daily ops due to lower friction and mobile accessibility.
interface DeliveryPayload {
date: string;
summary: string;
analysis: string;
actions: string[];
channel: 'telegram' | 'slack';
}
async function dispatchReport(payload: DeliveryPayload) {
const formatted = `π DAILY AD TRIAGE - ${payload.date}\n\n${payload.summary}\n\nπ ANALYSIS:\n${payload.analysis}\n\nπ― ACTIONS:\n${payload.actions.map((a, i) => `${i + 1}. ${a}`).join('\n')}`;
if (payload.channel === 'telegram') {
await fetch(`https://api.telegram.org/bot${process.env.TG_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: process.env.TG_CHAT_ID, text: formatted, parse_mode: 'HTML' }),
});
}
}
Rationale: Push delivery eliminates dashboard login friction. HTML parsing in Telegram preserves formatting without external dependencies. The pipeline remains stateless; each run is independent, enabling horizontal scaling and easy debugging.
Pitfall Guide
1. Feeding Raw API Responses to the LLM
Explanation: Direct JSON dumps force the model to parse, calculate, and reason simultaneously. This increases token consumption, latency, and hallucination probability. Fix: Always pre-process metrics. Calculate aggregates, apply business thresholds, and pass structured objects with explicit status flags.
2. Static Thresholds in Volatile Markets
Explanation: Fixed ROAS cutoffs (e.g., < 2.0 = LOW) fail during seasonal shifts, creative refreshes, or audience saturation. A campaign may temporarily dip due to external factors, not structural failure.
Fix: Implement rolling 14-day baselines. Compare current metrics against a moving average rather than absolute values. Add variance tolerance bands before flagging.
3. Ignoring API Rate Limits & Pagination
Explanation: Meta enforces strict call limits per account. Large portfolios trigger 429 Too Many Requests or truncated responses, breaking the pipeline.
Fix: Implement exponential backoff with jitter. Use limit and after cursors for pagination. Cache responses locally and respect X-App-Usage headers.
4. Prompt Drift & Verbose Outputs
Explanation: LLMs default to conversational framing. Without strict constraints, outputs balloon to 500+ words, increasing costs and delaying operator review.
Fix: Enforce negative constraints (No introductions, Bullet points only). Set explicit word limits. Validate output length in a post-processing step and retry with stricter prompts if exceeded.
5. Skipping Error Boundaries & Fallbacks
Explanation: A single API failure or LLM timeout halts the entire pipeline. Operators miss critical triage data without graceful degradation.
Fix: Wrap each stage in try/catch blocks. Implement a fallback cache that delivers yesterday's report with a β οΈ STALE DATA warning. Queue failed runs for retry with exponential backoff.
6. Over-Automating Execution Decisions
Explanation: Auto-pausing campaigns or shifting budgets without human validation introduces catastrophic risk. LLMs lack contextual awareness of brand safety, creative testing phases, or strategic holds. Fix: Maintain human-in-the-loop for execution. The pipeline should recommend, not act. Require explicit approval via chat command or dashboard toggle before applying changes.
7. Untracked LLM Costs
Explanation: Daily batch processing scales linearly with portfolio size. Without cost monitoring, token consumption can exceed budget thresholds silently. Fix: Log input/output tokens per run. Implement a daily cost cap that disables non-critical analysis if exceeded. Use cheaper models for formatting and reserve Sonnet for reasoning stages.
Production Bundle
Action Checklist
- Pin Meta Graph API version: Lock to
v19.0and monitor developer changelogs for deprecation notices. - Implement metric pre-processing: Calculate ROAS, CTR, and spend aggregates before LLM ingestion.
- Enforce prompt constraints: Add negative instructions and word limits to control token usage.
- Add error boundaries: Wrap API calls and LLM requests in retry logic with fallback alerts.
- Configure push delivery: Set up Telegram/Slack webhooks with HTML formatting and chat ID validation.
- Establish human-in-the-loop: Require explicit approval before auto-applying budget shifts or pauses.
- Monitor token costs: Log input/output tokens daily and implement a spending cap with graceful degradation.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Small portfolio (<20 campaigns) | Rule-based alerts + manual review | Simpler stack, lower LLM dependency | Low ($0β$15/mo) |
| Medium portfolio (20β100 campaigns) | Automated triage pipeline (Claude Sonnet) | Balances reasoning depth with cost efficiency | Medium ($30β$80/mo) |
| Large portfolio (100+ campaigns) | Hybrid: Rule engine + LLM for exceptions | Reduces token volume by filtering noise first | High ($100β$250/mo) |
| High-risk brand campaigns | Human-in-the-loop only | Prevents automated missteps during sensitive periods | Low (operational overhead) |
| Real-time optimization needs | Meta's native automated rules + LLM daily sync | Native APIs handle sub-hour adjustments; LLM handles strategy | Medium |
Configuration Template
// pipeline.config.ts
export const PipelineConfig = {
meta: {
version: 'v19.0',
accountId: process.env.META_ACCOUNT_ID!,
accessToken: process.env.META_ACCESS_TOKEN!,
fields: ['campaign_name', 'impressions', 'clicks', 'spend', 'ctr', 'cpc', 'cpm', 'roas', 'reach'],
datePreset: 'last_7d',
level: 'campaign',
},
thresholds: {
criticalRoas: 1.0,
lowRoas: 2.0,
scaleRoas: 3.0,
ctrFatigue: 0.8,
impressionThreshold: 50000,
},
llm: {
model: 'claude-sonnet-4-20250514',
maxTokens: 500,
temperature: 0.2,
systemPrompt: 'You are a performance marketing analyst. Output strictly follows requested format.',
},
delivery: {
channel: 'telegram' as const,
botToken: process.env.TG_BOT_TOKEN!,
chatId: process.env.TG_CHAT_ID!,
parseMode: 'HTML',
},
retry: {
maxAttempts: 3,
baseDelayMs: 1000,
backoffMultiplier: 2,
},
};
Quick Start Guide
- Provision API Credentials: Generate a Meta System User token with
ads_readandads_managementpermissions. Store securely in environment variables. - Deploy the Pipeline: Run the TypeScript orchestrator on a cron scheduler (e.g., GitHub Actions, AWS EventBridge, or n8n). Set execution to
0 7 * * *for daily 7 AM delivery. - Validate Data Flow: Trigger a manual run. Verify Meta API returns structured insights, pre-processing calculates flags correctly, and Claude outputs a formatted brief.
- Connect Notification Channel: Configure Telegram bot token and chat ID. Test webhook delivery and HTML formatting.
- Implement Approval Gate: Add a simple command handler (e.g.,
/approve <action_id>) to log execution decisions before applying budget changes via the Ads Management API.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
