📋 Overview
This n8n workflow automates competitive analysis by:
- AI-powered competitor discovery using Claude Sonnet 4
- Multi-platform ad scraping (Google Ads, Facebook/Meta, LinkedIn, TikTok)
- Deep AI analysis of messaging, keywords, and strategies
- Automated scoring of opportunities and threats
- Executive HTML reports delivered via email
Time Savings: ~4-6 hours per week of manual competitive research
ROI: 10-50x through automated insights and faster market response
✨ Key Features
🔍 Intelligent Competitor Discovery
- Claude AI identifies direct competitors based on your business profile
- Focuses on single most relevant competitor per analysis
- Industry-specific competitive intelligence
📊 Multi-Platform Ad Monitoring
- Google Ads Transparency - Search and display campaigns
- Facebook/Meta Ad Library - Social advertising strategies
- LinkedIn Ads - B2B professional targeting
- TikTok Ads - Short-form video campaigns
🧠 AI-Powered Analysis
- Extracts ad copy, keywords, and messaging themes
- Identifies competitive advantages and market gaps
- Provides actionable recommendations
- Calculates opportunity and threat scores
📈 Strategic Scoring System
- Opportunity Score (0-100) - Market gaps and growth potential
- Threat Score (0-100) - Competitive pressure assessment
- Priority Levels - High/Medium/Low action items
- Confidence Ratings - AI analysis reliability scores
📧 Professional Reporting
- Beautifully formatted HTML email reports
- Executive summary with key findings
- Detailed competitive insights
- Strategic recommendations
🛠️ Prerequisites
Required Services & API Keys
- n8n Instance (self-hosted or cloud)
- Version 1.0+ recommended
- Webhook/form trigger support enabled
- Anthropic API Key (Claude)
- Model: Claude Sonnet 4 (claude-sonnet-4-20250514)
- Cost: ~$10-15/month for weekly reports
- Apify Account (Ad Scraping)
- Free tier: 5 actors, limited runs
- Paid tier recommended: $49/month for reliable scraping
- Required Actors:
- Google Ads Transparency Scraper (shashankms2580/google-ads-transparency-scraper)
- Facebook Ads Scraper (anchor/facebook-ads-scraper)
- LinkedIn Ads Scraper (anchor/linkedin-ads-scraper)
- TikTok Ads Scraper (worldwidestore/tiktok-ads-scraper)
- Gmail Account (Report Delivery)
- OAuth2 authentication required
- Setup in n8n credentials manager
📦 Installation & Setup
Step 1: Import Workflow
- Download
Competitor_Ad_Tracking_System_SANITIZED.json - Open your n8n instance
- Click Import → From File
- Select the downloaded JSON file
- Workflow will appear in your workflows list
Step 2: Configure Credentials
IMPORTANT: Never paste API keys directly into code nodes. Always use n8n’s credential manager.
A. Anthropic API Credential
- In n8n, go to Settings → Credentials
- Click Add Credential → Anthropic API
- Enter your API key from https://console.anthropic.com/
- Save with name “Anthropic account”
B. Apify API Credential
- Go to Settings → Credentials
- Click Add Credential → Apify API
- Enter your Apify API token from https://console.apify.com/account/integrations
- Save with name “Apify account”
C. Gmail OAuth2 Credential
- Go to Settings → Credentials
- Click Add Credential → Gmail OAuth2 API
- Follow n8n’s OAuth2 setup wizard
- Authorize Gmail access
- Save with name “Gmail account”
Step 3: Attach Credentials to Nodes
Open the workflow and attach credentials to these nodes:
- 🔍 AI Competitor Discovery → Attach “Anthropic account”
- 🧠 AI Ad Copy & Keywords Analysis → Attach “Anthropic account”
- Message a model → Attach “Anthropic account”
- Google Ads → Attach “Apify account”
- Facebook Ads Scraper → Attach “Apify account”
- LinkedIn Ads Scraper → Attach “Apify account”
- TikTok Ads Scraper → Attach “Apify account”
- 📧 Send Weekly Intelligence Report → Attach “Gmail account”
Step 4: Configure Email Recipient
- Open the 📧 Send Weekly Intelligence Report node
- Change
sendToparameter fromYOUR_EMAIL@company.comto your actual email - Save the node
Step 5: Activate Workflow
- Click the Activate toggle in the top-right
- Copy the webhook URL from the On form submission node
- You’re ready to run your first analysis!
🚀 Usage
Web Form Method (Recommended)
- Navigate to your workflow’s webhook URL
- Fill out the competitive intelligence form:
- Business Name: Your company name
- Business Domain: Your website (e.g., yourcompany.com)
- Industry Keywords: Comma-separated (e.g., “SaaS, marketing automation, analytics”)
- Click Submit
- Wait 2-5 minutes for analysis to complete
- Check your email for the comprehensive report
Manual Execution
- Open the workflow in n8n
- Click Execute Workflow button
- Manually input business details when prompted
- View results in execution log
📊 Understanding the Report
Your weekly report includes:
1. Executive Summary
High-level overview of competitive landscape and key findings
2. Competitor Profile
- Name and domain
- Focus area and positioning
- Scan date and metadata
3. Advertising Intelligence
- Ad Copies: Actual ads from competitors
- Keywords: Target keywords identified
- Messaging Themes: Brand messaging patterns
- Targeting Insights: Audience demographics
4. Competitive Analysis
- Advantages: What competitors do well
- Opportunity Gaps: Market openings you can exploit
- Threat Level: High/Medium/Low competitive pressure
5. Strategic Scoring
- Opportunity Score: Market potential (0-100)
- Threat Score: Competitive risk (0-100)
- Priority Level: Action urgency
- Confidence Score: AI analysis reliability
6. Recommended Actions
Specific, prioritized steps to improve competitive position
🔧 Configuration Options
Competitor Discovery Settings
Edit the 🔍 AI Competitor Discovery node to adjust:
{ "model": "claude-sonnet-4-20250514", "max_tokens": 1000, "temperature": 0.3 // Lower = more focused, Higher = more creative}
To find multiple competitors:
- Change system prompt to request “top 3 competitors” instead of “top direct competitor”
- Adjust
max_tokensto 2000+ for longer responses
Ad Scraping Limits
Adjust data collection in Apify actor nodes:
Google Ads node:
{ "maxResults": 20, // Increase for more ads (costs more) "region": "US" // Change for different regions}
Apply similar adjustments to Facebook, LinkedIn, TikTok nodes
Analysis Depth
Edit 🧠 AI Ad Copy & Keywords Analysis node:
{ "max_tokens": 3000, // More tokens = deeper analysis "temperature": 0.2 // Lower for consistency, higher for creativity}
Report Frequency
Currently form-triggered. To schedule weekly reports:
- Replace On form submission node with Schedule Trigger
- Set cron expression:
0 9 * * 1(Every Monday at 9 AM) - Add a Set node to define business variables
💰 Cost Breakdown
Estimated Monthly Costs (Weekly Reports)
Service | Usage | Cost |
|---|---|---|
Anthropic Claude API | ~15K tokens/report × 4 weeks | $10-15 |
Apify Actors | 4 actors × 4 runs/month | $49-99 |
n8n Cloud (optional) | Workflow hosting | $20-50 |
Gmail | Email delivery | Free |
Total | $79-164/month |
Cost Optimization Tips
- Use Apify Free Tier for testing (5 actors, limited runs)
- Reduce ad scraping frequency to bi-weekly
- Self-host n8n to eliminate hosting costs
- Limit Apify maxResults to reduce actor runtime
- Use Claude Haiku for faster, cheaper analysis (trade-off: less detailed)
🐛 Troubleshooting
Common Issues
1. “Paired Item” Errors in Parse AI Analysis
Solution: Already fixed in this workflow using $itemIndex approach
2. Apify Actors Timeout
Cause: Scrapers take too long or hit rate limits Solution:
- Increase timeout in Apify node settings
- Reduce
maxResultsparameter - Enable Apify proxy in actor settings
3. Claude API Returns Empty Response
Cause: Invalid JSON formatting or API quota exceeded Solution:
- Check Anthropic console for API errors
- Verify credential is correctly attached
- Ensure you have API credits remaining
4. Gmail Not Sending Reports
Cause: OAuth2 token expired or insufficient permissions Solution:
- Reconnect Gmail OAuth2 credential
- Ensure “Send email” permission is granted
- Check Gmail quota limits (500 emails/day)
5. No Competitors Found
Cause: Claude couldn’t identify competitors from input Solution:
- Provide more specific industry keywords
- Include detailed business description
- Check Claude API response in execution log
6. Workflow Runs But No Email Received
Check:
- Email address is correct in Send Email node
- Gmail credential is properly attached
- Check spam/junk folder
- Review execution log for errors
🔐 Security Best Practices
Credential Management
- ✅ DO: Store all API keys in n8n’s credential manager
- ✅ DO: Use environment variables for sensitive data
- ❌ DON’T: Paste API keys directly into Code nodes
- ❌ DON’T: Share workflows with credentials attached
Data Privacy
- Competitor data is processed in-memory only
- No data storage except in execution logs (configurable)
- Email reports contain sensitive competitive intelligence
- Use encrypted email or secure delivery method
- Limit recipient list to authorized personnel
Rotating Credentials
If credentials are compromised:
- Immediately revoke API keys in respective consoles
- Generate new keys
- Update credentials in n8n
- Review execution logs for unauthorized access
📈 Advanced Usage
Integration with Google Sheets
Add a Google Sheets node after Aggregate All Results:
{ "operation": "append", "sheetId": "YOUR_SHEET_ID", "range": "A:Z"}
Map these fields:
scan_date,competitor_name,competitor_domainopportunity_score,threat_score,priority_levelkeywords_identified,messaging_themes
Slack Notifications
Replace or supplement Gmail with Slack node:
{ "resource": "message", "operation": "post", "channel": "#competitive-intel", "text": "New competitor report ready!"}
Database Storage (PostgreSQL/MySQL)
Add database node for historical tracking:
INSERT INTO competitor_tracking (
scan_date, competitor_name, opportunity_score,
threat_score, keywords, raw_analysis
) VALUES (?, ?, ?, ?, ?, ?)
🤝 Contributing
Improvements and feature requests are welcome!
To contribute:
- Fork this workflow
- Make your enhancements
- Test thoroughly
- Share sanitized version (no credentials!)
- Document changes in README
Feature ideas:
- Multi-competitor batch analysis
- Historical trend tracking
- Automated A/B test recommendations
- Competitive pricing intelligence
- Social media sentiment analysis
📄 License
MIT License - Free to use, modify, and distribute
🆘 Support
Issues & Questions:
- Open an issue on GitHub
- Check n8n community forum: https://community.n8n.io/
- Review Anthropic documentation: https://docs.anthropic.com/
- Consult Apify actor documentation
Professional Support:
- For custom implementations or enterprise support
- Email: [your-support-email]
💡 Use Cases
Marketing Teams
- Monitor competitor campaign launches
- Track messaging trends across platforms
- Identify underserved audience segments
- Optimize ad spend based on competitive intelligence
Product Managers
- Understand competitive feature positioning
- Track competitor product launches
- Identify market gaps for new features
- Monitor competitive pricing strategies
Agencies
- Deliver competitive intelligence to clients
- Benchmark client performance against industry
- Generate monthly competitive reports
- Support strategic planning with data
E-commerce
- Track competitor promotional strategies
- Monitor seasonal campaign trends
- Identify winning ad copy patterns
- Optimize product positioning
🎯 Workflow Architecture
Form Trigger
↓
AI Competitor Discovery (Claude)
↓
Parse JSON Response
↓
Parse Competitor Data
↓
┌─────────────────────────────────────┐
│ Parallel Ad Scraping (Apify) │
│ ├─ Google Ads Transparency │
│ ├─ Facebook/Meta Ad Library │
│ ├─ LinkedIn Ads │
│ └─ TikTok Ads │
└─────────────────────────────────────┘
↓
Merge All Ad Data
↓
AI Analysis (Claude)
↓
Parse Analysis Results
↓
Competitive Opportunity Scoring
↓
Aggregate Results
↓
Generate Executive Report (Claude)
↓
Send Email Report (Gmail)Full JSON Code
{ "name": "Competitor Ad Tracking System (FIXED)", "nodes": [ { "parameters": { "method": "POST", "url": "https://api.anthropic.com/v1/messages", "authentication": "predefinedCredentialType", "nodeCredentialType": "anthropicApi", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "anthropic-version", "value": "2023-06-01" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 1000,\n \"temperature\": 0.3,\n \"system\": \"You are a competitive intelligence analyst. Given a business name, domain, and industry keywords, identify the top direct competitor. Return competitor as a separate JSON object on its own line (newline-delimited JSON format). Do NOT return a JSON array. Strictly stick to only 1 competitor. Each line should be a complete competitor object.\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Business: \\nDomain: \\nIndustry: \\n\\nFind top direct competitor and return as a separate JSON object:\\n{\\\"name\\\": \\\"CompetitorName\\\", \\\"domain\\\": \\\"competitor.com\\\", \\\"focus_area\\\": \\\"brief description\\\"}\"\n }\n ]\n}", "options": {} }, "id": "7200989b-4f1e-4f1e-b196-61054c9c9114", "name": "🔍 AI Competitor Discovery1", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [-1904, 1568] }, { "parameters": { "jsCode": "// Parse newline-delimited JSON from Claude\nconst response = $input.all()[0].json;\nlet content = '';\n\nif (response.content && Array.isArray(response.content)) {\n content = response.content[0]?.text || '';\n}\n\nif (!content) {\n throw new Error('No content found in Claude response');\n}\n\n// Split by newlines and parse each JSON object\nconst lines = content.trim().split('\\n').filter(line => line.trim());\nconst competitors = [];\n\nfor (const line of lines) {\n try {\n const competitor = JSON.parse(line.trim());\n competitors.push(competitor);\n } catch (e) {\n console.log('Failed to parse line:', line);\n }\n}\n\nif (competitors.length === 0) {\n throw new Error('No competitors parsed from response');\n}\n\nreturn competitors.map(comp => ({ json: comp }));" }, "id": "6362b1c2-7d54-4d78-959f-c08e9491e131", "name": "Parse Newline-Delimited JSON1", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1680, 1568] }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Do not mutate $json or its properties!\nlet businessName = 'YourBusiness';\ntry {\n businessName = $('Setup Business Variables').item.json['business_name'] || 'YourBusiness';\n} catch (e) {\n // If reading fails, fallback used\n}\n\n// Safely extract properties, avoiding mutation\nconst competitorName = typeof $json?.name === 'string' ? $json.name : 'Unknown Competitor';\nconst competitorDomain = typeof $json?.domain === 'string' ? $json.domain : 'unknown.com';\nconst focusArea = typeof $json?.focus_area === 'string' ? $json.focus_area : 'Unknown';\n\nreturn {\n json: {\n competitor_id: $itemIndex + 1,\n competitor_name: competitorName,\n competitor_domain: competitorDomain,\n focus_area: focusArea,\n business_name: businessName,\n scan_date: new Date().toISOString().split('T')[0]\n }\n};" }, "id": "b1604d5d-9b2a-4cea-900c-524f78d24f60", "name": "Parse Competitors Data1", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1456, 1568] }, { "parameters": { "mode": "combine", "combinationMode": "mergeByPosition", "options": {} }, "id": "ff75c4c0-b987-4529-8afe-17f8e16b2d71", "name": "Merge All Ad Data1", "type": "n8n-nodes-base.merge", "typeVersion": 2.1, "position": [-784, 1568] }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Parse Claude AI analysis and structure the data\nif (!item || !item.json) {\n throw new Error('No input item found.');\n}\n\nconst items_data = item.json;\n\n// Claude API returns: { content: [{ type: \"text\", text: \"...\" }] }\nlet response = '';\nif (items_data.content && Array.isArray(items_data.content)) {\n response = items_data.content[0]?.text || '';\n} else if (items_data.choices && Array.isArray(items_data.choices)) {\n response = items_data.choices[0]?.message?.content || '';\n}\n\nif (!response) {\n throw new Error('No response content found from Claude API.');\n}\n\nlet analysis;\ntry {\n analysis = JSON.parse(response);\n} catch (e) {\n const jsonMatch = response.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/);\n if (jsonMatch && jsonMatch[1]) {\n analysis = JSON.parse(jsonMatch[1].trim());\n } else {\n throw new Error(`Failed to parse AI analysis: ${e.message}`);\n }\n}\n\n// --- FIX START ---\n// Fetch all items from the source node and select the one matching the current index.\n// This bypasses the \"Paired Item\" error caused by broken lineage.\nconst sourceItems = $('Parse Competitors Data1').all();\nconst competitorData = (sourceItems[$itemIndex] && sourceItems[$itemIndex].json) ? sourceItems[$itemIndex].json : {};\n// --- FIX END ---\n\nconst enrichedAnalysis = {\n ...analysis,\n scan_date: new Date().toISOString().split('T')[0],\n scan_timestamp: new Date().toISOString(),\n business_name: competitorData.business_name || 'YourBusiness',\n competitor_id: competitorData.competitor_id || 1,\n competitor_domain: competitorData.competitor_domain || 'unknown.com',\n focus_area: competitorData.focus_area || 'Unknown',\n raw_google_ads: items_data.google_ads_data ? 'Available' : 'Not Available',\n raw_meta_ads: items_data.meta_ads_data ? 'Available' : 'Not Available',\n raw_linkedin_data: items_data.linkedin_intelligence ? 'Available' : 'Not Available'\n};\n\nreturn { json: enrichedAnalysis };" }, "id": "7fcea385-ea3a-47d8-93ed-2a1d9d02f4c6", "name": "Parse AI Analysis Results1", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-208, 1568] }, { "parameters": { "jsCode": "// Calculate competitive opportunity scores\nconst data = items[0].json;\n\nlet opportunityScore = 0;\nlet threatScore = 0;\n\nif (data.keywords_identified && Array.isArray(data.keywords_identified)) {\n opportunityScore += Math.min(data.keywords_identified.length * 2, 20);\n}\n\nif (data.opportunity_gaps && Array.isArray(data.opportunity_gaps)) {\n opportunityScore += Math.min(data.opportunity_gaps.length * 5, 25);\n}\n\nswitch (data.threat_level) {\n case 'high': threatScore = 80; break;\n case 'medium': threatScore = 50; break;\n case 'low': threatScore = 20; break;\n default: threatScore = 40;\n}\n\nif (data.competitive_advantages && Array.isArray(data.competitive_advantages)) {\n threatScore += Math.min(data.competitive_advantages.length * 3, 20);\n}\n\nconst confidence = parseInt(data.confidence_score) || 50;\nopportunityScore = Math.round(opportunityScore * (confidence / 100));\nthreatScore = Math.round(threatScore * (confidence / 100));\n\nlet priority = 'medium';\nif (opportunityScore > 30 || threatScore > 60) {\n priority = 'high';\n} else if (opportunityScore < 15 && threatScore < 30) {\n priority = 'low';\n}\n\nconst scoredData = {\n ...data,\n opportunity_score: Math.min(opportunityScore, 100),\n threat_score: Math.min(threatScore, 100),\n priority_level: priority,\n overall_impact: Math.round((opportunityScore + threatScore) / 2),\n keywords_count: data.keywords_identified ? data.keywords_identified.length : 0,\n opportunities_count: data.opportunity_gaps ? data.opportunity_gaps.length : 0,\n advantages_count: data.competitive_advantages ? data.competitive_advantages.length : 0,\n scoring_timestamp: new Date().toISOString()\n};\n\nreturn [{ json: scoredData }];" }, "id": "2d3669f5-fd7c-4e36-b5b1-70f3068a45e0", "name": "🎯 Competitive Opportunity Scoring1", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [16, 1568] }, { "parameters": { "aggregate": "aggregateAllItemData", "options": {} }, "id": "ccc150eb-b8ff-4bed-8a26-5901c47c22b8", "name": "Aggregate All Results1", "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [240, 1568] }, { "parameters": { "sendTo": "YOUR_EMAIL@company.com", "subject": "=🎯 Weekly Competitive Intelligence Report - ", "message": "=", "options": {} }, "id": "98c56140-873c-43fb-8fd7-ed370a308a7f", "name": "📧 Send Weekly Intelligence Report1", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [816, 1568] }, { "parameters": { "formTitle": "Competitor Intelligence Form", "formDescription": "Enter business details for competitive analysis", "formFields": { "values": [ { "fieldLabel": "Business Name", "requiredField": true }, { "fieldLabel": "Business Domain", "requiredField": true }, { "fieldLabel": "Industry Keywords", "fieldType": "textarea", "requiredField": true } ] }, "responseMode": "lastNode", "options": {} }, "id": "2c8028b2-cd0b-45ec-bc0a-617884cc26f4", "name": "On form submission1", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.3, "position": [-2128, 1568] }, { "parameters": { "operation": "Run actor and get dataset", "actorId": { "__rl": true, "value": "zB0rjv0Wf9gyguGSV", "mode": "list", "cachedResultName": "Google Ads Transparency Scraper (shashankms2580/google-ads-transparency-scraper)", "cachedResultUrl": "https://console.apify.com/actors/zB0rjv0Wf9gyguGSV/input" }, "customBody": "={\n \"deltaSinceLastRun\": true,\n \"mode\": \"FULL\",\n \"ocrEnabled\": false,\n \"preset\": \"competitive_analysis\",\n \"region\": \"US\",\n \"targets\": [\n \"\"\n ]\n}", "timeout": {} }, "type": "@apify/n8n-nodes-apify.apify", "typeVersion": 1, "position": [-1232, 1280], "id": "85c59a15-19f3-4022-930f-7a3e8ac1628d", "name": "Google Ads" }, { "parameters": { "jsCode": "// Get all ads returned by the scraper\nconst items = $input.all();\n\n// If no ads were found, return a default \"No Data\" message\nif (items.length === 0) {\n return [{\n json: {\n google_ads_data: \"No ads found or scraper returned no data.\",\n google_ads_count: 0\n }\n }];\n}\n\n// Combine all ad details into a single text summary for the AI\nlet consolidatedText = `Found ${items.length} ads.\\n\\n`;\n\nitems.forEach((item, index) => {\n const ad = item.json;\n \n // Skip if it's just an empty state record\n if (ad.status_message === 'empty_state_text' || ad.has_ads === false) {\n return;\n }\n\n consolidatedText += `Ad ${index + 1}:\\n`;\n if (ad.text_copy) consolidatedText += `Copy: ${ad.text_copy}\\n`;\n if (ad.images && ad.images.length > 0) consolidatedText += `Image URL: ${ad.images[0]}\\n`;\n if (ad.media_type) consolidatedText += `Type: ${ad.media_type}\\n`;\n consolidatedText += '---\\n';\n});\n\n// Return ONE item with the consolidated data\nreturn [{\n json: {\n // Keep the competitor name from the first item if available, or pass it through\n advertiser_name: items[0].json.advertiser_name,\n google_ads_data: consolidatedText,\n google_ads_count: items.length\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1008, 1280], "id": "33b7b6b4-19ad-4ed5-a658-10eb3b6044ea", "name": "Code in JavaScript" }, { "parameters": { "jsCode": "// Get all ads returned by the Meta/Facebook scraper\nconst items = $input.all();\n\n// If no data came back, return a default empty state\nif (items.length === 0) {\n return [{\n json: {\n meta_ads_data: \"No ads found or scraper returned no data.\",\n meta_ads_count: 0\n }\n }];\n}\n\n// Initialize the summary string\nlet consolidatedText = `Found ${items.length} ads.\\n\\n`;\n\nitems.forEach((item, index) => {\n const ad = item.json;\n \n // Basic filtering to skip empty/invalid records if necessary\n if (!ad.ad_archive_id && !ad.ad_body) {\n return;\n }\n\n consolidatedText += `Ad ${index + 1}:\\n`;\n \n // Meta scraper field names often differ. We check common variations.\n // 1. Ad Copy (Body text)\n const adBody = ad.ad_body || ad.ad_creative_body || ad.body || (ad.snapshot ? ad.snapshot.body : '') || 'No text';\n if (adBody) consolidatedText += `Copy: ${adBody}\\n`;\n \n // 2. Headline\n const headline = ad.ad_headline || ad.title || (ad.snapshot ? ad.snapshot.title : '');\n if (headline) consolidatedText += `Headline: ${headline}\\n`;\n\n // 3. CTA\n const cta = ad.ad_creative_link_caption || ad.call_to_action_type || (ad.snapshot ? ad.snapshot.cta_text : '');\n if (cta) consolidatedText += `CTA: ${cta}\\n`;\n\n // 4. Image/Video URL (for context)\n const mediaUrl = ad.image_url || ad.video_url || (ad.snapshot && ad.snapshot.images ? ad.snapshot.images[0] : '');\n if (mediaUrl) consolidatedText += `Media URL: ${mediaUrl}\\n`;\n \n // 5. Active Dates\n if (ad.start_date) consolidatedText += `Started: ${ad.start_date}\\n`;\n\n consolidatedText += '---\\n';\n});\n\n// Return ONE item with the consolidated data\nreturn [{\n json: {\n // Try to grab the page name from the first valid ad\n advertiser_name: items[0].json.page_name || items[0].json.ad_creative_link_title || \"Unknown\",\n meta_ads_data: consolidatedText,\n meta_ads_count: items.length\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1008, 1472], "id": "950a93ba-c536-487e-ba99-6703189ee99a", "name": "Code in JavaScript1" }, { "parameters": { "jsCode": "// Get all ads returned by the LinkedIn scraper\nconst items = $input.all();\n\n// Safely retrieve the competitor name from the Parse Competitors Data1 node, \n// which is the source of truth for the competitor's identity in this execution branch.\nconst competitorName = $node['Parse Competitors Data1'].json.competitor_name || \"Unknown\";\n\n// If no data came back\nif (items.length === 0) {\n return [{\n json: {\n linkedin_intelligence: \"No ads found or scraper returned no data.\",\n linkedin_ads_count: 0,\n company_name: competitorName // Use the safely retrieved name\n }\n }];\n}\n\nlet consolidatedText = `Found ${items.length} LinkedIn ads for ${competitorName}.\\n\\n`;\n\nitems.forEach((item, index) => {\n const ad = item.json;\n\n consolidatedText += `Ad ${index + 1}:\\n`;\n\n // 1. Main Text\n const mainText = ad.text || ad.description || ad.commentary || '';\n if (mainText) consolidatedText += `Text: ${mainText}\\n`;\n\n // 2. Headline (often appears in the link preview)\n const headline = ad.headline || ad.title || '';\n if (headline) consolidatedText += `Headline: ${headline}\\n`;\n\n // 3. Landing Page / URL\n const url = ad.landing_page_url || ad.destination_url || ad.reference_link || '';\n if (url) consolidatedText += `Link: ${url}\\n`;\n\n // 4. Format (Image, Video, Carousel)\n const type = ad.type || ad.content_type || ad.format || '';\n if (type) consolidatedText += `Format: ${type}\\n`;\n\n consolidatedText += '---\\n';\n});\n\n// Return ONE item with the consolidated data\nreturn [{\n json: {\n linkedin_intelligence: consolidatedText,\n linkedin_ads_count: items.length,\n company_name: competitorName // Use the safely retrieved name\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1008, 1664], "id": "2830b8a7-84eb-473b-8ad4-19587e6ea0ce", "name": "Code in JavaScript2" }, { "parameters": { "modelId": { "__rl": true, "value": "claude-opus-4-5-20251101", "mode": "list", "cachedResultName": "claude-opus-4-5-20251101" }, "messages": { "values": [ { "content": "=Analyze this competitor data for :\n\nGoogle Ads Data: \nMeta Ads Data: \n\nLinkedIn Intelligence: \n\nProvide analysis in JSON format:\n{\n \"competitor_name\": \"name\",\n \"ad_copies\": [\"copy1\", \"copy2\"],\n \"keywords_identified\": [\"keyword1\", \"keyword2\"],\n \"messaging_themes\": [\"theme1\", \"theme2\"],\n \"targeting_insights\": \"insights about their target audience\",\n \"competitive_advantages\": [\"advantage1\", \"advantage2\"],\n \"opportunity_gaps\": [\"gap1\", \"gap2\"],\n \"recommended_actions\": [\"action1\", \"action2\"],\n \"threat_level\": \"low/medium/high\",\n \"confidence_score\": \"0-100\"\n}`\n }\n ]\n })\n}" }, { "content": "You are an expert digital marketing analyst specializing in competitive intelligence. Analyze ad content and extract actionable insights about competitors' strategies, keywords, messaging, and opportunities.", "role": "assistant" } ] }, "options": { "maxTokens": 2000, "temperature": 0.2 } }, "type": "@n8n/n8n-nodes-langchain.anthropic", "typeVersion": 1, "position": [-560, 1568], "id": "ca1c7dc3-f5e7-4dad-917c-8c04ad0df235", "name": "🧠 AI Ad Copy & Keywords Analysis" }, { "parameters": { "modelId": { "__rl": true, "value": "claude-opus-4-5-20251101", "mode": "list", "cachedResultName": "claude-opus-4-5-20251101" }, "messages": { "values": [ { "content": "=" }, { "content": "You are a strategic business analyst. Create executive-level competitive intelligence reports with actionable insights and recommendations.", "role": "assistant" } ] }, "options": { "maxTokens": 3000, "temperature": 0.3 } }, "type": "@n8n/n8n-nodes-langchain.anthropic", "typeVersion": 1, "position": [464, 1568], "id": "c9601a52-8a6f-4d06-906c-277da001d2d9", "name": "Message a model" }, { "parameters": { "jsCode": "// Get all ads returned by the TikTok scraper\nconst items = $input.all();\n\n// Safely retrieve the competitor name from the Parse Competitors Data1 node\nconst competitorName = $node['Parse Competitors Data1'].json.competitor_name || \"Unknown\";\n\n// If no data came back\nif (items.length === 0) {\n return [{\n json: {\n tiktok_ads_data: \"No ads found or scraper returned no data.\",\n tiktok_ads_count: 0,\n company_name: competitorName\n }\n }];\n}\n\nlet consolidatedText = `Found ${items.length} TikTok ads for ${competitorName}.\\n\\n`;\n\nitems.forEach((item, index) => {\n const ad = item.json;\n\n consolidatedText += `Ad ${index + 1}:\\n`;\n\n // 1. Main Text\n const mainText = ad.text || ad.caption || ad.description || '';\n if (mainText) consolidatedText += `Text: ${mainText}\\n`;\n\n // 2. Video URL\n const videoUrl = ad.video_url || ad.url || '';\n if (videoUrl) consolidatedText += `Video: ${videoUrl}\\n`;\n\n // 3. Hashtags\n const hashtags = ad.hashtags || ad.tags || [];\n if (hashtags && hashtags.length > 0) consolidatedText += `Hashtags: ${hashtags.join(', ')}\\n`;\n\n consolidatedText += '---\\n';\n});\n\n// Return ONE item with the consolidated data\nreturn [{\n json: {\n tiktok_ads_data: consolidatedText,\n tiktok_ads_count: items.length,\n company_name: competitorName\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1008, 1856], "id": "3d7e61d9-0b44-4d0a-a055-10061569a0cb", "name": "Code in JavaScript3" }, { "parameters": { "operation": "Run actor and get dataset", "actorId": { "__rl": true, "value": "JJghSZmShuco4j9gJ", "mode": "list", "cachedResultName": "Facebook Ads Scraper (apify/facebook-ads-scraper)", "cachedResultUrl": "https://console.apify.com/actors/JJghSZmShuco4j9gJ/input" }, "customBody": "={\n \"activeStatus\": \"active\",\n \"isDetailsPerAd\": false,\n \"onlyTotal\": false,\n \"resultsLimit\": 99999,\n \"startUrls\": [\n {\n \"url\": \"https://www.facebook.com/ads/library/?active_status=all&ad_type=all&q=\"\n }\n ]\n}", "timeout": {} }, "type": "@apify/n8n-nodes-apify.apify", "typeVersion": 1, "position": [-1232, 1472], "id": "b3d3d604-0a43-4037-b535-cb30c1aff46d", "name": "Facebook Ads Scraper" }, { "parameters": { "operation": "Run actor and get dataset", "actorId": { "__rl": true, "value": "ojJfB9pfBE9aARO3A", "mode": "list", "cachedResultName": "LinkedIn Ads Scraper (ivanvs/linkedin-ads-scraper)", "cachedResultUrl": "https://console.apify.com/actors/ojJfB9pfBE9aARO3A/input" }, "customBody": "={\n \"maxRecords\": 10,\n \"urls\": [\n {\n \"url\": \"https://www.linkedin.com/ad-library/search?accountOwner=\"\n }\n ]\n}", "timeout": {} }, "type": "@apify/n8n-nodes-apify.apify", "typeVersion": 1, "position": [-1232, 1664], "id": "29c51e1b-fb89-4b50-b68a-8101f9694bab", "name": "LinkedIn Ads Scraper" }, { "parameters": { "operation": "Run actor and get dataset", "actorSource": "store", "actorId": { "__rl": true, "value": "AovGexsTSmlalAFzp", "mode": "list", "cachedResultName": "Tiktok Ads Scraper (silva95gustavo/tiktok-ads-scraper)", "cachedResultUrl": "https://console.apify.com/actors/AovGexsTSmlalAFzp/input" }, "customBody": "={\n \"skipDetails\": false,\n \"startUrls\": [\n {\n \"url\": \"https://library.tiktok.com/ads?region=all&start_time=1735689600000&end_time=1745967600000&adv_name=&adv_biz_ids=6891503886842987266&query_type=2&sort_type=last_shown_date,desc\"\n }\n ]\n}", "timeout": {} }, "type": "@apify/n8n-nodes-apify.apify", "typeVersion": 1, "position": [-1232, 1856], "id": "5e14ff9e-c092-4c19-acde-fb8897cefbd1", "name": "TikTok Ads Scraper" } ], "pinData": {}, "connections": { "🔍 AI Competitor Discovery1": { "main": [[{"node": "Parse Newline-Delimited JSON1", "type": "main", "index": 0}]] }, "Parse Newline-Delimited JSON1": { "main": [[{"node": "Parse Competitors Data1", "type": "main", "index": 0}]] }, "Parse Competitors Data1": { "main": [[ {"node": "Google Ads", "type": "main", "index": 0}, {"node": "LinkedIn Ads Scraper", "type": "main", "index": 0}, {"node": "TikTok Ads Scraper", "type": "main", "index": 0}, {"node": "Facebook Ads Scraper", "type": "main", "index": 0} ]] }, "Merge All Ad Data1": { "main": [[{"node": "🧠 AI Ad Copy & Keywords Analysis", "type": "main", "index": 0}]] }, "Parse AI Analysis Results1": { "main": [[{"node": "🎯 Competitive Opportunity Scoring1", "type": "main", "index": 0}]] }, "🎯 Competitive Opportunity Scoring1": { "main": [[{"node": "Aggregate All Results1", "type": "main", "index": 0}]] }, "Aggregate All Results1": { "main": [[{"node": "Message a model", "type": "main", "index": 0}]] }, "On form submission1": { "main": [[{"node": "🔍 AI Competitor Discovery1", "type": "main", "index": 0}]] }, "Google Ads": { "main": [[{"node": "Code in JavaScript", "type": "main", "index": 0}]] }, "Code in JavaScript": { "main": [[{"node": "Merge All Ad Data1", "type": "main", "index": 0}]] }, "Code in JavaScript1": { "main": [[{"node": "Merge All Ad Data1", "type": "main", "index": 0}]] }, "Code in JavaScript2": { "main": [[{"node": "Merge All Ad Data1", "type": "main", "index": 1}]] }, "🧠 AI Ad Copy & Keywords Analysis": { "main": [[{"node": "Parse AI Analysis Results1", "type": "main", "index": 0}]] }, "Message a model": { "main": [[{"node": "📧 Send Weekly Intelligence Report1", "type": "main", "index": 0}]] }, "Code in JavaScript3": { "main": [[{"node": "Merge All Ad Data1", "type": "main", "index": 1}]] }, "Facebook Ads Scraper": { "main": [[{"node": "Code in JavaScript1", "type": "main", "index": 0}]] }, "LinkedIn Ads Scraper": { "main": [[{"node": "Code in JavaScript2", "type": "main", "index": 0}]] }, "TikTok Ads Scraper": { "main": [[{"node": "Code in JavaScript3", "type": "main", "index": 0}]] } }, "active": false, "settings": { "executionOrder": "v1" }, "meta": { "templateCredsSetupCompleted": false }, "tags": [] }