How to Build an Ad Creative Testing Pipeline
Automate the process of testing, scoring, and scaling ad creatives.
Jay Banlasan
The AI Systems Guy
An automated ad creative testing pipeline takes the guesswork out of knowing which ads to scale and which to kill. I run every new creative through this system. It watches performance, scores ads against benchmarks, and flags winners within 72 hours instead of waiting a week for someone to check.
The pipeline has three stages: launch, evaluate, act. Each one runs automatically.
What You Need Before Starting
- Meta Ads API access with a System User token
- Python 3.8+ with
requestsinstalled - A SQLite database for tracking creative performance
- Slack for notifications (optional but recommended)
Step 1: Define Your Testing Framework
Set benchmarks before you launch anything:
BENCHMARKS = {
"min_spend_to_judge": 30.00,
"min_impressions": 1000,
"ctr_good": 1.5,
"ctr_great": 2.5,
"cpc_max": 3.00,
"cpa_target": 25.00,
"protection_hours": 72,
}
The 72-hour protection window is critical. New ads need time to exit the learning phase. Killing too early wastes the money you already spent.
Step 2: Track Creative Performance
import sqlite3
from datetime import datetime, timedelta
def track_creative(db_path, ad_id, ad_name, spend, impressions, clicks, conversions, date):
conn = sqlite3.connect(db_path)
conn.execute("""CREATE TABLE IF NOT EXISTS creative_tests (
id INTEGER PRIMARY KEY, ad_id TEXT, ad_name TEXT, spend REAL,
impressions INTEGER, clicks INTEGER, conversions INTEGER,
date TEXT, launched_at TEXT, status TEXT DEFAULT 'testing',
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""")
conn.execute("""INSERT INTO creative_tests
(ad_id, ad_name, spend, impressions, clicks, conversions, date)
VALUES (?,?,?,?,?,?,?)""",
(ad_id, ad_name, spend, impressions, clicks, conversions, date))
conn.commit()
conn.close()
Step 3: Build the Scoring Engine
def score_creative(db_path, ad_id):
conn = sqlite3.connect(db_path)
stats = conn.execute("""
SELECT SUM(spend), SUM(impressions), SUM(clicks), SUM(conversions),
MIN(date) as first_date
FROM creative_tests WHERE ad_id = ?
""", (ad_id,)).fetchone()
conn.close()
total_spend, total_impressions, total_clicks, total_conversions, first_date = stats
if not total_impressions or total_impressions < BENCHMARKS["min_impressions"]:
return {"status": "insufficient_data", "action": "wait"}
ctr = (total_clicks / total_impressions) * 100
cpc = total_spend / total_clicks if total_clicks > 0 else 999
cpa = total_spend / total_conversions if total_conversions > 0 else 999
days_active = (datetime.now() - datetime.strptime(first_date, "%Y-%m-%d")).days
if days_active < 3:
return {"status": "protected", "action": "wait", "ctr": ctr, "cpc": cpc}
if total_spend >= BENCHMARKS["min_spend_to_judge"] and total_conversions == 0:
return {"status": "loser", "action": "pause", "ctr": ctr, "cpc": cpc, "spend": total_spend}
if ctr >= BENCHMARKS["ctr_great"] and cpa <= BENCHMARKS["cpa_target"]:
return {"status": "winner", "action": "scale", "ctr": ctr, "cpa": cpa}
if ctr >= BENCHMARKS["ctr_good"] and cpa <= BENCHMARKS["cpa_target"] * 1.2:
return {"status": "promising", "action": "continue", "ctr": ctr, "cpa": cpa}
return {"status": "underperforming", "action": "review", "ctr": ctr, "cpa": cpa}
Step 4: Automate the Actions
import requests
import os
def execute_action(ad_id, action, score_data):
token = os.getenv("META_ACCESS_TOKEN")
if action == "pause":
requests.post(f"https://graph.facebook.com/v19.0/{ad_id}",
params={"access_token": token, "status": "PAUSED"})
notify(f"PAUSED ad {ad_id}: ${score_data.get('spend', 0):.2f} spent, 0 conversions")
elif action == "scale":
notify(f"WINNER: Ad {ad_id} - CTR: {score_data['ctr']:.1f}%, CPA: ${score_data['cpa']:.2f}. Ready to scale.")
elif action == "review":
notify(f"REVIEW: Ad {ad_id} underperforming. CTR: {score_data.get('ctr', 0):.1f}%")
def notify(message):
token = os.getenv("SLACK_BOT_TOKEN")
requests.post("https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {token}"},
json={"channel": os.getenv("SLACK_CHANNEL"), "text": message})
Step 5: Run the Pipeline Daily
def run_pipeline(db_path, ad_ids):
for ad_id in ad_ids:
score = score_creative(db_path, ad_id)
if score["action"] != "wait":
execute_action(ad_id, score["action"], score)
print(f"Ad {ad_id}: {score['status']} -> {score['action']}")
if __name__ == "__main__":
active_ads = get_active_ad_ids() # Your function to get current ad IDs
run_pipeline("creative_tests.db", active_ads)
Schedule it to run after your daily data pull. The pipeline scores everything and acts on clear signals while flagging edge cases for your review.
What to Build Next
Add a weekly creative report that shows win/loss rates by format type (image vs video, hook type, framework). Use that data to inform your next batch of creative concepts.
Related Reading
- AI for Creative Strategy and Testing - the strategy behind creative testing
- The Measurement Framework That Actually Works - measuring what matters
- The Feedback Loop That Powers Everything - closing the loop on creative performance
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment