📋 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

  1. n8n Instance (self-hosted or cloud)

    • Version 1.0+ recommended

    • Webhook/form trigger support enabled

  2. Anthropic API Key (Claude)

    • Model: Claude Sonnet 4 (claude-sonnet-4-20250514)

    • Cost: ~$10-15/month for weekly reports

  3. 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)

  4. Gmail Account (Report Delivery)

    • OAuth2 authentication required

    • Setup in n8n credentials manager

📦 Installation & Setup

Step 1: Import Workflow

  1. Download Competitor_Ad_Tracking_System_SANITIZED.json

  2. Open your n8n instance

  3. Click ImportFrom File

  4. Select the downloaded JSON file

  5. 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

  1. In n8n, go to SettingsCredentials

  2. Click Add CredentialAnthropic API

  3. Enter your API key from https://console.anthropic.com/

  4. Save with name “Anthropic account”

B. Apify API Credential

  1. Go to SettingsCredentials

  2. Click Add CredentialApify API

  3. Enter your Apify API token from https://console.apify.com/account/integrations

  4. Save with name “Apify account”

C. Gmail OAuth2 Credential

  1. Go to SettingsCredentials

  2. Click Add CredentialGmail OAuth2 API

  3. Follow n8n’s OAuth2 setup wizard

  4. Authorize Gmail access

  5. Save with name “Gmail account”

Step 3: Attach Credentials to Nodes

Open the workflow and attach credentials to these nodes:

  1. 🔍 AI Competitor Discovery → Attach “Anthropic account”

  2. 🧠 AI Ad Copy & Keywords Analysis → Attach “Anthropic account”

  3. Message a model → Attach “Anthropic account”

  4. Google Ads → Attach “Apify account”

  5. Facebook Ads Scraper → Attach “Apify account”

  6. LinkedIn Ads Scraper → Attach “Apify account”

  7. TikTok Ads Scraper → Attach “Apify account”

  8. 📧 Send Weekly Intelligence Report → Attach “Gmail account”

Step 4: Configure Email Recipient

  1. Open the 📧 Send Weekly Intelligence Report node

  2. Change sendTo parameter from [email protected] to your actual email

  3. Save the node

Step 5: Activate Workflow

  1. Click the Activate toggle in the top-right

  2. Copy the webhook URL from the On form submission node

  3. You’re ready to run your first analysis!

🚀 Usage

  1. Navigate to your workflow’s webhook URL

  2. 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”)

  3. Click Submit

  4. Wait 2-5 minutes for analysis to complete

  5. Check your email for the comprehensive report

Manual Execution

  1. Open the workflow in n8n

  2. Click Execute Workflow button

  3. Manually input business details when prompted

  4. 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

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_tokens to 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:

  1. Replace On form submission node with Schedule Trigger

  2. Set cron expression: 0 9 * * 1 (Every Monday at 9 AM)

  3. 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

  1. Use Apify Free Tier for testing (5 actors, limited runs)

  2. Reduce ad scraping frequency to bi-weekly

  3. Self-host n8n to eliminate hosting costs

  4. Limit Apify maxResults to reduce actor runtime

  5. 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 maxResults parameter

  • 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:

  1. Immediately revoke API keys in respective consoles

  2. Generate new keys

  3. Update credentials in n8n

  4. 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_domain

  • opportunity_score, threat_score, priority_level

  • keywords_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:

  1. Fork this workflow

  2. Make your enhancements

  3. Test thoroughly

  4. Share sanitized version (no credentials!)

  5. 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:

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: {{ $json['Business Name'] }}\\nDomain: {{ $json['Business Domain'] }}\\nIndustry: {{ $json['Industry Keywords'] }}\\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": "[email protected]", "subject": "=🎯 Weekly Competitive Intelligence Report - {{ new Date().toDateString() }}", "message": "={{ $json.content[0].text }}", "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 \"{{ $json.competitor_domain }}\"\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 {{ $json.advertiser_name }}:\n\nGoogle Ads Data: {{ $json.google_ads_data }}\nMeta Ads Data: \n\nLinkedIn Intelligence: {{ $json.linkedin_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": "={{ 'Create an executive summary report based on this competitive analysis data:\\n\\n' + JSON.stringify($json, null, 2) + '\\n\\nGenerate a professional HTML report including:\\n1. Executive Summary\\n2. Key Findings\\n3. Competitive Threats (High/Medium/Low)\\n4. Market Opportunities\\n5. Strategic Recommendations\\n6. Next Actions\\n\\nFormat as a complete HTML document. Use minimal, concise CSS styles to save space. Focus on the content of the Executive Summary and findings.' }}" }, { "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={{ $json.competitor_name }}\"\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={{ $json.competitor_name }}\"\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={{ $json.competitor_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": [] }