0% found this document useful (0 votes)
33 views26 pages

Google Ads Performance Optimization - templates.thevibemarketer.com

Google Ads Optimization

Uploaded by

makeworkflows
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views26 pages

Google Ads Performance Optimization - templates.thevibemarketer.com

Google Ads Optimization

Uploaded by

makeworkflows
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

Access Granted ✓

Back to Templates

Advertising

n8n

Google Ads Performance Optimization


Continuously optimize Google Ads campaigns by automatically analyzing performance data, identifying
opportunities, and implementing data-driven improvements.

Save 5-8 hours per week 100x faster workflows

Template Benefits
AI-Powered Google Ads Save time and resources

Optimization System Increase efficiency


Improve marketing results
Scale your marketing efforts
Overview
This n8n automation template helps marketers continuously optimize their
Google Ads campaigns for maximum performance. By automating the entire
optimization process from data collection to bid adjustments, this workflow
ensures your ads deliver the best possible results without constant manual
monitoring and tweaking.

Use Case
Marketing teams need to consistently optimize their Google Ads campaigns, but
manually analyzing performance data, identifying opportunities, and
implementing changes is extremely time-consuming. This automation streamlines
the entire optimization process, allowing marketers to achieve better ad
performance with minimal effort.

Benefits
Save 8-10 hours per week on Google Ads management

Improve campaign performance through data-driven optimizations

Reduce wasted ad spend on underperforming keywords and placements

Identify new opportunities for growth

Scale your Google Ads management without additional resources

Prerequisites
n8n account (cloud or self-hosted)
Google Ads account with API access

Google Analytics account (preferably GA4)

Google Sheets for reporting

OpenAI API key (for AI-powered insights)

Step-by-Step Implementation
Step 1: Set Up Your n8n Workflow
Log in to your n8n account

Create a new workflow by clicking "Create new workflow"

Name your workflow "Google Ads Optimization System"

Step 2: Configure Google Ads API Connection


Add a "Google Ads" node

Authentication: OAuth2
Resource: Campaign
Operation: Get
Account ID: [Your Google Ads account ID]

Add a "Function" node to store account information

// Store Google Ads account information


const adsAccount = $json || {};

// Format account data


const accountInfo = {
accountId: adsAccount.id || '',
accountName: adsAccount.name || '',
currency: adsAccount.currency || 'USD',
timeZone: adsAccount.timeZone || 'America/New_York',
timestamp: new Date().toISOString()
};

return { accountInfo };

Step 3: Set Up Daily Performance Data Collection


Add a "Schedule" node for daily data collection

Frequency: Daily
Time: 06:00 AM

Add a "Google Ads" node for campaign performance

Resource: Report
Operation: Get
Report Type: CAMPAIGN_PERFORMANCE_REPORT

Date Range: YESTERDAY


Metrics: impressions, clicks, cost, conversions, conversion_value

Add a "Function" node to process campaign data

// Process campaign performance data


const reportData = $json.data || [];
const processedCampaigns = [];

// Process each campaign


reportData.forEach(campaign => {
processedCampaigns.push({
campaignId: campaign.campaign_id || '',
campaignName: campaign.campaign_name || '',
campaignStatus: campaign.campaign_status || '',
impressions: parseInt(campaign.impressions || 0),
clicks: parseInt(campaign.clicks || 0),
cost: parseFloat(campaign.cost || 0) / 1000000, // Convert micros to actual currency
conversions: parseFloat(campaign.conversions || 0),
conversionValue: parseFloat(campaign.conversion_value || 0),
ctr: parseFloat(campaign.ctr || 0) * 100, // Convert to percentage
cpc: parseFloat(campaign.average_cpc || 0) / 1000000, // Convert micros to actual
conversionRate: parseFloat(campaign.conversion_rate || 0) * 100, // Convert to percentage

roas: parseFloat(campaign.conversion_value || 0) / (parseFloat(campaign.cost || 1)


date: new Date().toISOString().split('T')[0]
});
});

return { campaigns: processedCampaigns };

Add a "Google Ads" node for keyword performance

Resource: Report
Operation: Get
Report Type: KEYWORDS_PERFORMANCE_REPORT
Date Range: YESTERDAY
Metrics: impressions, clicks, cost, conversions, conversion_value, quality_score

Add a "Function" node to process keyword data

// Process keyword performance data


const reportData = $json.data || [];
const processedKeywords = [];

// Process each keyword


reportData.forEach(keyword => {
processedKeywords.push({
keywordId: keyword.id || '',
keywordText: keyword.keyword_text || '',
keywordMatchType: keyword.keyword_match_type || '',
campaignId: keyword.campaign_id || '',
campaignName: keyword.campaign_name || '',
adGroupId: keyword.ad_group_id || '',
adGroupName: keyword.ad_group_name || '',
impressions: parseInt(keyword.impressions || 0),
clicks: parseInt(keyword.clicks || 0),
cost: parseFloat(keyword.cost || 0) / 1000000, // Convert micros to actual currency
conversions: parseFloat(keyword.conversions || 0),
conversionValue: parseFloat(keyword.conversion_value || 0),
ctr: parseFloat(keyword.ctr || 0) * 100, // Convert to percentage
cpc: parseFloat(keyword.average_cpc || 0) / 1000000, // Convert micros to actual currency

conversionRate: parseFloat(keyword.conversion_rate || 0) * 100, // Convert to percentage

qualityScore: parseInt(keyword.quality_score || 0),


date: new Date().toISOString().split('T')[0]
});
});

return { keywords: processedKeywords };

Add a "Google Ads" node for ad performance

Resource: Report
Operation: Get
Report Type: AD_PERFORMANCE_REPORT
Date Range: YESTERDAY
Metrics: impressions, clicks, cost, conversions, conversion_value

Add a "Function" node to process ad data

// Process ad performance data


const reportData = $json.data || [];
const processedAds = [];

// Process each ad
reportData.forEach(ad => {
processedAds.push({
adId: ad.ad_id || '',
adHeadline: ad.ad_headline || '',
adDescription: ad.ad_description || '',
campaignId: ad.campaign_id || '',
campaignName: ad.campaign_name || '',
adGroupId: ad.ad_group_id || '',
adGroupName: ad.ad_group_name || '',
impressions: parseInt(ad.impressions || 0),
clicks: parseInt(ad.clicks || 0),
cost: parseFloat(ad.cost || 0) / 1000000, // Convert micros to actual currency
conversions: parseFloat(ad.conversions || 0),
conversionValue: parseFloat(ad.conversion_value || 0),
ctr: parseFloat(ad.ctr || 0) * 100, // Convert to percentage
cpc: parseFloat(ad.average_cpc || 0) / 1000000, // Convert micros to actual currency
conversionRate: parseFloat(ad.conversion_rate || 0) * 100, // Convert to percentage
date: new Date().toISOString().split('T')[0]
});
});

return { ads: processedAds };

Step 4: Store Performance Data


Add a "Google Sheets" node for campaign data
Operation: Append
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Campaign Performance
Data: [Campaign data array]

Add a "Google Sheets" node for keyword data

Operation: Append
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Keyword Performance
Data: [Keyword data array]

Add a "Google Sheets" node for ad data

Operation: Append
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Ad Performance
Data: [Ad data array]

Step 5: Set Up Performance Analysis


Add a "Schedule" node for weekly analysis

Frequency: Weekly
Day: Monday
Time: 08:00 AM

Add a "Google Sheets" node to get historical data

Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Campaign Performance

Add a "Function" node to analyze campaign performance

// Analyze campaign performance


const campaignData = $json.data || [];
const campaignAnalysis = [];

// Skip header row


const campaigns = campaignData.slice(1);

// Group by campaign ID
const campaignGroups = {};
campaigns.forEach(campaign => {
const campaignId = campaign[0]; // Assuming campaign ID is in first column
if (!campaignGroups[campaignId]) {
campaignGroups[campaignId] = [];
}
campaignGroups[campaignId].push({
campaignId: campaign[0],
campaignName: campaign[1],
campaignStatus: campaign[2],
impressions: parseInt(campaign[3]),
clicks: parseInt(campaign[4]),
cost: parseFloat(campaign[5]),
conversions: parseFloat(campaign[6]),
conversionValue: parseFloat(campaign[7]),
ctr: parseFloat(campaign[8]),
cpc: parseFloat(campaign[9]),
conversionRate: parseFloat(campaign[10]),
roas: parseFloat(campaign[11]),
date: campaign[12]
});
});

// Analyze each campaign


Object.keys(campaignGroups).forEach(campaignId => {
const campaignRecords = campaignGroups[campaignId];

// Sort by date (newest first)


campaignRecords.sort((a, b) => new Date(b.date) - new Date(a.date));

// Get last 7 days and previous 7 days


const last7Days = campaignRecords.slice(0, 7);
const previous7Days = campaignRecords.slice(7, 14);

// Calculate totals for last 7 days


const last7DaysTotals = calculateTotals(last7Days);

// Calculate totals for previous 7 days


const previous7DaysTotals = calculateTotals(previous7Days);

// Calculate changes
const changes = calculateChanges(last7DaysTotals, previous7DaysTotals);

// Add to analysis
campaignAnalysis.push({
campaignId,
campaignName: campaignRecords[0].campaignName,
campaignStatus: campaignRecords[0].campaignStatus,
last7Days: last7DaysTotals,
previous7Days: previous7DaysTotals,
changes,
performanceScore: calculatePerformanceScore(changes),
recommendations: generateRecommendations(changes, last7DaysTotals)
});
});

// Helper functions
function calculateTotals(records) {
if (!records || records.length === 0) {
return {
impressions: 0,
clicks: 0,
cost: 0,
conversions: 0,
conversionValue: 0,
ctr: 0,
cpc: 0,
conversionRate: 0,
roas: 0
};
}
const totals = {
impressions: records.reduce((sum, record) => sum + record.impressions, 0),
clicks: records.reduce((sum, record) => sum + record.clicks, 0),
cost: records.reduce((sum, record) => sum + record.cost, 0),
conversions: records.reduce((sum, record) => sum + record.conversions, 0),
conversionValue: records.reduce((sum, record) => sum + record.conversionValue, 0)
};

// Calculate derived metrics


totals.ctr = totals.impressions > 0 ? (totals.clicks / totals.impressions) * 100 : 0;

totals.cpc = totals.clicks > 0 ? totals.cost / totals.clicks : 0;


totals.conversionRate = totals.clicks > 0 ? (totals.conversions / totals.clicks) * 100

totals.roas = totals.cost > 0 ? totals.conversionValue / totals.cost : 0;

return totals;
}

function calculateChanges(current, previous) {


if (!previous || Object.values(previous).every(val => val === 0)) {
return {
impressions: 100,
clicks: 100,
cost: 100,
conversions: 100,
conversionValue: 100,
ctr: 100,
cpc: 100,
conversionRate: 100,
roas: 100
};
}

return {
impressions: calculatePercentChange(current.impressions, previous.impressions),
clicks: calculatePercentChange(current.clicks, previous.clicks),
cost: calculatePercentChange(current.cost, previous.cost),
conversions: calculatePercentChange(current.conversions, previous.conversions),
conversionValue: calculatePercentChange(current.conversionValue, previous.conversionValue),
ctr: calculatePercentChange(current.ctr, previous.ctr),
cpc: calculatePercentChange(current.cpc, previous.cpc),
conversionRate: calculatePercentChange(current.conversionRate, previous.conversionRate),
roas: calculatePercentChange(current.roas, previous.roas)
};
}

function calculatePercentChange(current, previous) {


if (previous === 0) return current > 0 ? 100 : 0;
return ((current - previous) / previous) * 100;
}

function calculatePerformanceScore(changes) {
// Weight different metrics
const weights = {
conversions: 0.25,
conversionRate: 0.2,
roas: 0.2,
ctr: 0.15,
cpc: 0.1,
impressions: 0.05,
clicks: 0.05
};

// Calculate weighted score


let score = 0;
let totalWeight = 0;

Object.keys(weights).forEach(metric => {
// Skip if change is not a number
if (isNaN(changes[metric])) return;

// For CPC, lower is better, so invert the change


const change = metric === 'cpc' ? -changes[metric] : changes[metric];

score += change * weights[metric];


totalWeight += weights[metric];
});

// Normalize score
const normalizedScore = totalWeight > 0 ? score / totalWeight : 0;

// Convert to 0-100 scale


return Math.min(100, Math.max(0, normalizedScore + 50));
}

function generateRecommendations(changes, current) {


const recommendations = [];

// Check conversion rate


if (changes.conversionRate < -10) {
recommendations.push({
type: 'warning',
metric: 'conversionRate',
message: 'Conversion rate has decreased by ' + Math.abs(changes.conversionRate).toFixed(2)
action: 'Review landing pages and ad relevance'
});
}

// Check ROAS
if (changes.roas < -10) {
recommendations.push({
type: 'warning',
metric: 'roas',
message: 'ROAS has decreased by ' + Math.abs(changes.roas).toFixed(2) + '%',
action: 'Consider adjusting bids or targeting'
});
}

// Check CTR
if (changes.ctr < -10) {
recommendations.push({
type: 'warning',
metric: 'ctr',
message: 'CTR has decreased by ' + Math.abs(changes.ctr).toFixed(2) + '%',
action: 'Review ad copy and relevance'
});
}

// Check CPC
if (changes.cpc > 10) {
recommendations.push({
type: 'warning',
metric: 'cpc',

message: 'CPC has increased by ' + changes.cpc.toFixed(2) + '%',


action: 'Review bidding strategy and keyword competition'
});
}

// Check if campaign is performing well


if (changes.conversions > 10 && changes.roas > 0) {
recommendations.push({
type: 'opportunity',
metric: 'budget',
message: 'Campaign is performing well with increasing conversions and positive ROAS',

action: 'Consider increasing budget'


});
}

// Check if campaign has low conversion volume but good ROAS


if (current.conversions < 5 && current.roas > 3) {
recommendations.push({
type: 'opportunity',
metric: 'targeting',
message: 'Campaign has good ROAS but low conversion volume',
action: 'Consider expanding targeting to reach more potential customers'
});
}

return recommendations;
}

return { campaignAnalysis };

Add a "Google Sheets" node to get keyword data

Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Keyword Performance

Add a "Function" node to analyze keyword performance

// Analyze keyword performance


const keywordData = $json.data || [];
const keywordAnalysis = {
topPerforming: [],
underperforming: [],
opportunities: [],
wasted: []
};

// Skip header row


const keywords = keywordData.slice(1);

// Group by keyword ID
const keywordGroups = {};
keywords.forEach(keyword => {
const keywordId = keyword[0]; // Assuming keyword ID is in first column
if (!keywordGroups[keywordId]) {
keywordGroups[keywordId] = [];
}
keywordGroups[keywordId].push({
keywordId: keyword[0],
keywordText: keyword[1],
keywordMatchType: keyword[2],
campaignId: keyword[3],
campaignName: keyword[4],
adGroupId: keyword[5],
adGroupName: keyword[6],
impressions: parseInt(keyword[7]),
clicks: parseInt(keyword[8]),
cost: parseFloat(keyword[9]),
conversions: parseFloat(keyword[10]),
conversionValue: parseFloat(keyword[11]),
ctr: parseFloat(keyword[12]),
cpc: parseFloat(keyword[13]),
conversionRate: parseFloat(keyword[14]),
qualityScore: parseInt(keyword[15]),
date: keyword[16]
});
});

// Analyze each keyword


const analyzedKeywords = [];
Object.keys(keywordGroups).forEach(keywordId => {
const keywordRecords = keywordGroups[keywordId];

// Sort by date (newest first)


keywordRecords.sort((a, b) => new Date(b.date) - new Date(a.date));

// Get last 30 days


const last30Days = keywordRecords.slice(0, 30);

// Calculate totals
const totals = {
impressions: last30Days.reduce((sum, record) => sum + record.impressions, 0),
clicks: last30Days.reduce((sum, record) => sum + record.clicks, 0),
cost: last30Days.reduce((sum, record) => sum + record.cost, 0),
conversions: last30Days.reduce((sum, record) => sum + record.conversions, 0),
conversionValue: last30Days.reduce((sum, record) => sum + record.conversionValue,
};

// Calculate derived metrics


totals.ctr = totals.impressions > 0 ? (totals.clicks / totals.impressions) * 100 : 0;

totals.cpc = totals.clicks > 0 ? totals.cost / totals.clicks : 0;


totals.conversionRate = totals.clicks > 0 ? (totals.conversions / totals.clicks) * 100

totals.roas = totals.cost > 0 ? totals.conversionValue / totals.cost : 0;

// Add latest quality score


totals.qualityScore = keywordRecords[0].qualityScore;

// Add keyword details


const analyzedKeyword = {
keywordId,
keywordText: keywordRecords[0].keywordText,
keywordMatchType: keywordRecords[0].keywordMatchType,
campaignId: keywordRecords[0].campaignId,
campaignName: keywordRecords[0].campaignName,
adGroupId: keywordRecords[0].adGroupId,
adGroupName: keywordRecords[0].adGroupName,
metrics: totals
};
analyzedKeywords.push(analyzedKeyword);

// Categorize keywords
if (totals.conversions > 0 && totals.roas > 3) {
keywordAnalysis.topPerforming.push(analyzedKeyword);
} else if (totals.conversions > 0 && totals.roas < 1) {
keywordAnalysis.underperforming.push(analyzedKeyword);
} else if (totals.clicks > 20 && totals.conversions === 0) {
keywordAnalysis.wasted.push(analyzedKeyword);
} else if (totals.impressions > 1000 && totals.ctr > 2 && totals.conversions === 0)
keywordAnalysis.opportunities.push(analyzedKeyword);
}
});

// Sort categories
keywordAnalysis.topPerforming.sort((a, b) => b.metrics.roas - a.metrics.roas);
keywordAnalysis.underperforming.sort((a, b) => a.metrics.roas - b.metrics.roas);
keywordAnalysis.wasted.sort((a, b) => b.metrics.cost - a.metrics.cost);
keywordAnalysis.opportunities.sort((a, b) => b.metrics.ctr - a.metrics.ctr);

// Limit to top 10 in each category


keywordAnalysis.topPerforming = keywordAnalysis.topPerforming.slice(0, 10);
keywordAnalysis.underperforming = keywordAnalysis.underperforming.slice(0, 10);
keywordAnalysis.wasted = keywordAnalysis.wasted.slice(0, 10);
keywordAnalysis.opportunities = keywordAnalysis.opportunities.slice(0, 10);

return { keywordAnalysis, analyzedKeywords };

Add a "Google Sheets" node to get ad data

Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Ad Performance

Add a "Function" node to analyze ad performance

// Analyze ad performance
const adData = $json.data || [];
const adAnalysis = {
topPerforming: [],
underperforming: [],
opportunities: []
};

// Skip header row


const ads = adData.slice(1);

// Group by ad ID
const adGroups = {};
ads.forEach(ad => {
const adId = ad[0]; // Assuming ad ID is in first column
if (!adGroups[adId]) {
adGroups[adId] = [];
}
adGroups[adId].push({
adId: ad[0],
adHeadline: ad[1],
adDescription: ad[2],
campaignId: ad[3],
campaignName: ad[4],
adGroupId: ad[5],
adGroupName: ad[6],
impressions: parseInt(ad[7]),
clicks: parseInt(ad[8]),
cost: parseFloat(ad[9]),
conversions: parseFloat(ad[10]),
conversionValue: parseFloat(ad[11]),
ctr: parseFloat(ad[12]),
cpc: parseFloat(ad[13]),
conversionRate: parseFloat(ad[14]),
date: ad[15]
});
});

// Analyze each ad
const analyzedAds = [];
Object.keys(adGroups).forEach(adId => {
const adRecords = adGroups[adId];

// Sort by date (newest first)


adRecords.sort((a, b) => new Date(b.date) - new Date(a.date));

// Get last 30 days


const last30Days = adRecords.slice(0, 30);

// Calculate totals
const totals = {
impressions: last30Days.reduce((sum, record) => sum + record.impressions, 0),
clicks: last30Days.reduce((sum, record) => sum + record.clicks, 0),
cost: last30Days.reduce((sum, record) => sum + record.cost, 0),
conversions: last30Days.reduce((sum, record) => sum + record.conversions, 0),
conversionValue: last30Days.reduce((sum, record) => sum + record.conversionValue,
};

// Calculate derived metrics


totals.ctr = totals.impressions > 0 ? (totals.clicks / totals.impressions) * 100 : 0;

totals.cpc = totals.clicks > 0 ? totals.cost / totals.clicks : 0;


totals.conversionRate = totals.clicks > 0 ? (totals.conversions / totals.clicks) * 100

totals.roas = totals.cost > 0 ? totals.conversionValue / totals.cost : 0;

// Add ad details
const analyzedAd = {
adId,
adHeadline: adRecords[0].adHeadline,
adDescription: adRecords[0].adDescription,
campaignId: adRecords[0].campaignId,
campaignName: adRecords[0].campaignName,
adGroupId: adRecords[0].adGroupId,
adGroupName: adRecords[0].adGroupName,
metrics: totals
};

analyzedAds.push(analyzedAd);

// Categorize ads
if (totals.conversions > 0 && totals.conversionRate > 2) {
adAnalysis.topPerforming.push(analyzedAd);
} else if (totals.clicks > 20 && totals.conversions === 0) {
adAnalysis.underperforming.push(analyzedAd);
} else if (totals.impressions > 1000 && totals.ctr > 2 && totals.conversions === 0)
adAnalysis.opportunities.push(analyzedAd);
}
});

// Sort categories
adAnalysis.topPerforming.sort((a, b) => b.metrics.conversionRate - a.metrics.conversionRate);
adAnalysis.underperforming.sort((a, b) => b.metrics.clicks - a.metrics.clicks);
adAnalysis.opportunities.sort((a, b) => b.metrics.ctr - a.metrics.ctr);

// Limit to top 10 in each category


adAnalysis.topPerforming = adAnalysis.topPerforming.slice(0, 10);
adAnalysis.underperforming = adAnalysis.underperforming.slice(0, 10);
adAnalysis.opportunities = adAnalysis.opportunities.slice(0, 10);

return { adAnalysis, analyzedAds };

Step 6: Generate AI-Powered Insights


Add an "OpenAI" node for performance insights

Model: gpt-4
Operation: Complete Text
Prompt: Analyze the following Google Ads performance data and provide strategic insights:

CAMPAIGN ANALYSIS:
{{JSON.stringify($json.campaignAnalysis, null, 2)}}

KEYWORD ANALYSIS:
Top Performing Keywords: {{JSON.stringify($json.keywordAnalysis.topPerforming, null,

Underperforming Keywords: {{JSON.stringify($json.keywordAnalysis.underperforming,


Wasted Spend Keywords: {{JSON.stringify($json.keywordAnalysis.wasted, null, 2)}}
Opportunity Keywords: {{JSON.stringify($json.keywordAnalysis.opportunities, null,

AD ANALYSIS:
Top Performing Ads: {{JSON.stringify($json.adAnalysis.topPerforming, null, 2)}}
Underperforming Ads: {{JSON.stringify($json.adAnalysis.underperforming, null, 2)}}
Opportunity Ads: {{JSON.stringify($json.adAnalysis.opportunities, null, 2)}}

Please provide:
1. Executive summary of Google Ads performance
2. Top 3-5 actionable recommendations to improve performance
3. Specific bid adjustment suggestions for campaigns and keywords
4. Ad copy improvement suggestions based on top performing ads
5. New keyword suggestions based on performance patterns

Format the output as a structured analysis with clear sections and actionable insights.

Max Tokens: 1500


Temperature: 0.7

Add a "Function" node to extract recommendations

// Extract recommendations from AI insights


const aiInsights = $json.text || '';
const campaignAnalysis = $json.campaignAnalysis || [];
const keywordAnalysis = $json.keywordAnalysis || {};
const adAnalysis = $json.adAnalysis || {};

// Extract bid adjustment recommendations


const bidAdjustmentRegex = /bid adjustment[s]?.*?:([^]*?)(?=\n\n|\n#|\n\*\*|$)/i;
const bidAdjustmentMatch = aiInsights.match(bidAdjustmentRegex);
const bidAdjustmentText = bidAdjustmentMatch ? bidAdjustmentMatch[1].trim() : '';

// Parse bid adjustments


const bidAdjustments = [];
if (bidAdjustmentText) {
// Look for campaign and keyword adjustments
const lines = bidAdjustmentText.split('\n');
lines.forEach(line => {
// Look for campaign adjustments
const campaignMatch = line.match(/campaign[s]?.*?["'](.+?)["'].*?(increase|decrease).*?(\d+)%/i);
if (campaignMatch) {
const campaignName = campaignMatch[1];
const direction = campaignMatch[2].toLowerCase();
const percentage = parseInt(campaignMatch[3]);

// Find campaign in analysis


const campaign = campaignAnalysis.find(c => c.campaignName.includes(campaignName));
if (campaign) {
bidAdjustments.push({
type: 'campaign',
id: campaign.campaignId,
name: campaign.campaignName,
adjustment: direction === 'increase' ? percentage : -percentage,
reason: line.trim()
});
}
}

// Look for keyword adjustments


const keywordMatch = line.match(/keyword[s]?.*?["'](.+?)["'].*?(increase|decrease).*?(\d+)%/i);
if (keywordMatch) {
const keywordText = keywordMatch[1];
const direction = keywordMatch[2].toLowerCase();
const percentage = parseInt(keywordMatch[3]);

// Find keyword in analysis


const allKeywords = [
...keywordAnalysis.topPerforming,
...keywordAnalysis.underperforming,
...keywordAnalysis.opportunities
];

const keyword = allKeywords.find(k => k.keywordText.includes(keywordText));


if (keyword) {
bidAdjustments.push({
type: 'keyword',
id: keyword.keywordId,
text: keyword.keywordText,
adjustment: direction === 'increase' ? percentage : -percentage,
reason: line.trim()
});
}
}
});
}

// Extract ad copy suggestions


const adCopyRegex = /ad copy.*?:([^]*?)(?=\n\n|\n#|\n\*\*|$)/i;
const adCopyMatch = aiInsights.match(adCopyRegex);
const adCopyText = adCopyMatch ? adCopyMatch[1].trim() : '';

// Extract new keyword suggestions


const newKeywordRegex = /new keyword[s]?.*?:([^]*?)(?=\n\n|\n#|\n\*\*|$)/i;
const newKeywordMatch = aiInsights.match(newKeywordRegex);
const newKeywordText = newKeywordMatch ? newKeywordMatch[1].trim() : '';

// Format recommendations
const recommendations = {
bidAdjustments,
adCopySuggestions: adCopyText,
newKeywordSuggestions: newKeywordText,
fullInsights: aiInsights
};

return { recommendations };

Step 7: Create Optimization Actions


Add a "Function" node to generate optimization actions

// Generate optimization actions


const recommendations = $json.recommendations || {};
const campaignAnalysis = $json.campaignAnalysis || [];
const keywordAnalysis = $json.keywordAnalysis || {};
const adAnalysis = $json.adAnalysis || {};

// Initialize optimization actions


const optimizationActions = [];

// Add bid adjustment actions


if (recommendations.bidAdjustments && recommendations.bidAdjustments.length > 0) {
recommendations.bidAdjustments.forEach(adjustment => {
optimizationActions.push({
type: adjustment.type === 'campaign' ? 'campaign_bid_adjustment' : 'keyword_bid_adjustment',

id: adjustment.id,
name: adjustment.name || adjustment.text,
adjustment: adjustment.adjustment,
reason: adjustment.reason,
status: 'pending'
});
});
}

// Add keyword pause actions for wasted spend


if (keywordAnalysis.wasted && keywordAnalysis.wasted.length > 0) {
keywordAnalysis.wasted.forEach(keyword => {
// Only recommend pausing if significant spend with no conversions
if (keyword.metrics.cost > 50 && keyword.metrics.conversions === 0) {
optimizationActions.push({
type: 'keyword_pause',
id: keyword.keywordId,
text: keyword.keywordText,
campaign: keyword.campaignName,
adGroup: keyword.adGroupName,
metrics: {
cost: keyword.metrics.cost,
clicks: keyword.metrics.clicks,
conversions: keyword.metrics.conversions
},
reason: `Wasted spend: $${keyword.metrics.cost.toFixed(2)} spent with ${keyword.metrics.clicks}

status: 'pending'
});
}
});
}

// Add new ad suggestions based on top performing ads


if (adAnalysis.topPerforming && adAnalysis.topPerforming.length > 0) {
// Group top ads by ad group
const adGroupMap = {};
adAnalysis.topPerforming.forEach(ad => {
if (!adGroupMap[ad.adGroupId]) {
adGroupMap[ad.adGroupId] = {
adGroupId: ad.adGroupId,
adGroupName: ad.adGroupName,
campaignId: ad.campaignId,
campaignName: ad.campaignName,
ads: []
};
}
adGroupMap[ad.adGroupId].ads.push(ad);
});

// For each ad group with top performing ads, suggest creating similar ads
Object.values(adGroupMap).forEach(adGroup => {
if (adGroup.ads.length > 0) {
const topAd = adGroup.ads[0]; // Best performing ad

optimizationActions.push({
type: 'create_similar_ad',
adGroupId: adGroup.adGroupId,
adGroupName: adGroup.adGroupName,
campaignId: adGroup.campaignId,
campaignName: adGroup.campaignName,
baseAdId: topAd.adId,
baseAdHeadline: topAd.adHeadline,
baseAdDescription: topAd.adDescription,
metrics: {
conversionRate: topAd.metrics.conversionRate,
ctr: topAd.metrics.ctr,
conversions: topAd.metrics.conversions
},
reason: `Create similar ad based on top performer (${topAd.metrics.conversionRate.toFixed(2)}%
status: 'pending'
});
}
});
}

// Add budget adjustment actions for campaigns


campaignAnalysis.forEach(campaign => {
// Recommend budget increase for high performing campaigns
if (campaign.performanceScore > 70 && campaign.changes.conversions > 0 && campaign.changes.roas

optimizationActions.push({
type: 'campaign_budget_increase',
id: campaign.campaignId,
name: campaign.campaignName,
adjustment: 15, // Suggest 15% budget increase

metrics: {
performanceScore: campaign.performanceScore,
conversionChange: campaign.changes.conversions,
roasChange: campaign.changes.roas
},
reason: `High performing campaign (score: ${campaign.performanceScore.toFixed(0)})
status: 'pending'
});
}

// Recommend budget decrease for poor performing campaigns


if (campaign.performanceScore < 30 && campaign.changes.conversions < 0 && campaign.changes.roas

optimizationActions.push({
type: 'campaign_budget_decrease',
id: campaign.campaignId,
name: campaign.campaignName,
adjustment: -20, // Suggest 20% budget decrease
metrics: {
performanceScore: campaign.performanceScore,
conversionChange: campaign.changes.conversions,
roasChange: campaign.changes.roas
},
reason: `Poor performing campaign (score: ${campaign.performanceScore.toFixed(0)})
status: 'pending'
});
}
});

return { optimizationActions };

Add a "Google Sheets" node to store optimization actions

Operation: Append
Spreadsheet ID: [Your optimization tracking spreadsheet]
Sheet: Optimization Actions
Data: [Optimization actions array]

Step 8: Implement Bid Adjustments


Add a "Function" node to prepare bid adjustments

// Prepare bid adjustments


const optimizationActions = $json.optimizationActions || [];

// Filter for bid adjustment actions


const bidAdjustments = optimizationActions.filter(action =>
action.type === 'campaign_bid_adjustment' ||
action.type === 'keyword_bid_adjustment'
);

// Group by type
const campaignBidAdjustments = bidAdjustments.filter(action =>
action.type === 'campaign_bid_adjustment'
);

const keywordBidAdjustments = bidAdjustments.filter(action =>


action.type === 'keyword_bid_adjustment'
);

return {
campaignBidAdjustments,
keywordBidAdjustments
};

Add a "Google Ads" node for campaign bid adjustments

Resource: Campaign
Operation: Update
Campaign ID: {{$json.id}}
Bid Adjustment: {{$json.adjustment}}

Add a "Google Ads" node for keyword bid adjustments

Resource: Keyword
Operation: Update
Keyword ID: {{$json.id}}
Bid Adjustment: {{$json.adjustment}}

Step 9: Implement Keyword Pauses


Add a "Function" node to prepare keyword pauses

// Prepare keyword pauses


const optimizationActions = $json.optimizationActions || [];

// Filter for keyword pause actions


const keywordPauses = optimizationActions.filter(action =>
action.type === 'keyword_pause'
);

return { keywordPauses };

Add a "Google Ads" node for keyword pauses

Resource: Keyword
Operation: Update
Keyword ID: {{$json.id}}
Status: PAUSED

Step 10: Generate New Ad Suggestions


Add a "Function" node to prepare ad creation

// Prepare ad creation
const optimizationActions = $json.optimizationActions || [];
const recommendations = $json.recommendations || {};

// Filter for ad creation actions


const adCreations = optimizationActions.filter(action =>
action.type === 'create_similar_ad'
);

// Use AI suggestions to create variations


const adVariations = [];

adCreations.forEach(action => {
// Create variations based on the original ad
const baseHeadline = action.baseAdHeadline;
const baseDescription = action.baseAdDescription;

// Extract patterns from AI suggestions


const patterns = extractPatterns(recommendations.adCopySuggestions);

// Create variations
const variations = [];

// Headline variations
const headlineVariations = createVariations(baseHeadline, patterns.headlines);

// Description variations
const descriptionVariations = createVariations(baseDescription, patterns.descriptions);

// Create ad variations (up to 3)


for (let i = 0; i < Math.min(3, headlineVariations.length); i++) {
variations.push({
adGroupId: action.adGroupId,
adGroupName: action.adGroupName,
campaignId: action.campaignId,
campaignName: action.campaignName,
headline: headlineVariations[i],
description: descriptionVariations[i % descriptionVariations.length],
baseAdId: action.baseAdId
});
}

adVariations.push(...variations);
});

// Helper functions
function extractPatterns(suggestions) {
// Default patterns if no AI suggestions
const defaultPatterns = {
headlines: [
'Add numbers (e.g., "5 Ways to...")',
'Add urgency (e.g., "Limited Time...")',
'Add question (e.g., "Want to...?")'
],
descriptions: [
'Add social proof',
'Add specific benefit',
'Add call to action'
]
};

// If no suggestions, return defaults


if (!suggestions) return defaultPatterns;

// Extract headline patterns


const headlineRegex = /headline[s]?.*?:([^]*?)(?=\n\n|\ndescription|\n#|\n\*\*|$)/i;
const headlineMatch = suggestions.match(headlineRegex);
const headlinePatterns = headlineMatch ?
headlineMatch[1].trim().split('\n').filter(line => line.trim()) :
defaultPatterns.headlines;

// Extract description patterns


const descriptionRegex = /description[s]?.*?:([^]*?)(?=\n\n|\n#|\n\*\*|$)/i;
const descriptionMatch = suggestions.match(descriptionRegex);
const descriptionPatterns = descriptionMatch ?
descriptionMatch[1].trim().split('\n').filter(line => line.trim()) :
defaultPatterns.descriptions;

return {
headlines: headlinePatterns,
descriptions: descriptionPatterns
};
}

function createVariations(baseText, patterns) {


const variations = [baseText]; // Include original

// Apply each pattern to create a variation


patterns.forEach(pattern => {
let variation = baseText;

// Extract the pattern instruction


const patternText = pattern.replace(/^[-*•]?\s*/, '');

if (patternText.includes('numbers') || patternText.includes('stats')) {
variation = addNumbers(baseText);
} else if (patternText.includes('urgency') || patternText.includes('limited')) {
variation = addUrgency(baseText);
} else if (patternText.includes('question')) {
variation = addQuestion(baseText);
} else if (patternText.includes('social proof')) {
variation = addSocialProof(baseText);
} else if (patternText.includes('benefit')) {
variation = addBenefit(baseText);
} else if (patternText.includes('call to action') || patternText.includes('CTA'))
variation = addCallToAction(baseText);
}

// Only add if different from original and other variations


if (variation !== baseText && !variations.includes(variation)) {
variations.push(variation);
}
});

return variations;
}

function addNumbers(text) {
// Add numbers to headline if not already present
if (/\d+/.test(text)) return text; // Already has numbers

const number = Math.floor(Math.random() * 7) + 3; // Random number between 3-9

if (text.length > 20) {


// For longer text, add number at beginning
return `${number} Ways to ${text}`;
} else {
// For shorter text, add percentage
return `${text} - ${number * 10}% Better Results`;
}
}

function addUrgency(text) {
// Add urgency phrases
const urgencyPhrases = [
'Limited Time: ',
'Act Now: ',
'Today Only: ',
'Last Chance: '
];

const phrase = urgencyPhrases[Math.floor(Math.random() * urgencyPhrases.length)];


return phrase + text;
}

function addQuestion(text) {
// Convert statement to question
if (text.endsWith('?')) return text; // Already a question

if (text.startsWith('How') || text.startsWith('Why') || text.startsWith('What')) {


// Already starts with question word, just add question mark
return text.endsWith('.') ? text.slice(0, -1) + '?' : text + '?';
} else {
// Add question starter
const questionStarters = [
'Want to ',
'Need to ',
'Looking to ',
'Ready to '
];

const starter = questionStarters[Math.floor(Math.random() * questionStarters.length)];


return starter + text.charAt(0).toLowerCase() + text.slice(1) + '?';
}
}

function addSocialProof(text) {
// Add social proof phrases
const socialProofPhrases = [
'Trusted by 1000+ businesses',
'Join 10,000+ satisfied customers',
'5-star rated by customers',
'Industry-leading solution'
];

const phrase = socialProofPhrases[Math.floor(Math.random() * socialProofPhrases.length)];


return text + '. ' + phrase;
}

function addBenefit(text) {
// Add benefit phrases
const benefitPhrases = [
'Save time and money',
'Boost your results',
'Improve efficiency by 50%',
'Reduce costs while increasing quality'
];

const phrase = benefitPhrases[Math.floor(Math.random() * benefitPhrases.length)];


return text + '. ' + phrase;
}
function addCallToAction(text) {
// Add CTA phrases
const ctaPhrases = [
'Start today!',
'Get started now!',
'Learn more today.',
'Contact us for details.'
];

const phrase = ctaPhrases[Math.floor(Math.random() * ctaPhrases.length)];


return text + ' ' + phrase;
}

return { adVariations };

Add a "Google Ads" node for ad creation

Resource: Ad
Operation: Create
Ad Group ID: {{$json.adGroupId}}
Headline: {{$json.headline}}
Description: {{$json.description}}

Step 11: Generate Performance Report


Add a "Function" node to compile report data

// Compile report data


const campaignAnalysis = $json.campaignAnalysis || [];
const keywordAnalysis = $json.keywordAnalysis || {};
const adAnalysis = $json.adAnalysis || {};
const recommendations = $json.recommendations || {};
const optimizationActions = $json.optimizationActions || [];

// Format date range


const today = new Date();
const lastWeek = new Date(today);
lastWeek.setDate(lastWeek.getDate() - 7);

const dateRange = {
start: lastWeek.toISOString().split('T')[0],
end: today.toISOString().split('T')[0]
};

// Calculate overall metrics


const overallMetrics = {
impressions: 0,
clicks: 0,
cost: 0,
conversions: 0,
conversionValue: 0,
ctr: 0,
cpc: 0,
conversionRate: 0,
roas: 0
};
// Sum up campaign metrics
campaignAnalysis.forEach(campaign => {
const metrics = campaign.last7Days;
overallMetrics.impressions += metrics.impressions;
overallMetrics.clicks += metrics.clicks;
overallMetrics.cost += metrics.cost;
overallMetrics.conversions += metrics.conversions;
overallMetrics.conversionValue += metrics.conversionValue;
});

// Calculate derived metrics


overallMetrics.ctr = overallMetrics.impressions > 0 ?
(overallMetrics.clicks / overallMetrics.impressions) * 100 : 0;
overallMetrics.cpc = overallMetrics.clicks > 0 ?
overallMetrics.cost / overallMetrics.clicks : 0;
overallMetrics.conversionRate = overallMetrics.clicks > 0 ?
(overallMetrics.conversions / overallMetrics.clicks) * 100 : 0;
overallMetrics.roas = overallMetrics.cost > 0 ?
overallMetrics.conversionValue / overallMetrics.cost : 0;

// Format optimization actions summary


const optimizationSummary = {
total: optimizationActions.length,
byType: {}
};

optimizationActions.forEach(action => {
optimizationSummary.byType[action.type] = (optimizationSummary.byType[action.type] ||

});

// Format report data


const reportData = {
dateRange,
overallMetrics,
campaignPerformance: campaignAnalysis.map(campaign => ({
name: campaign.campaignName,
status: campaign.campaignStatus,
metrics: campaign.last7Days,
changes: campaign.changes,
performanceScore: campaign.performanceScore
})).sort((a, b) => b.performanceScore - a.performanceScore),
keywordInsights: {
topPerforming: keywordAnalysis.topPerforming.slice(0, 5),
underperforming: keywordAnalysis.underperforming.slice(0, 5),
wasted: keywordAnalysis.wasted.slice(0, 5)
},
adInsights: {
topPerforming: adAnalysis.topPerforming.slice(0, 5),
underperforming: adAnalysis.underperforming.slice(0, 5)
},
aiRecommendations: recommendations.fullInsights,
optimizationSummary,
generatedOn: today.toISOString()
};

return { reportData };

Add a "Google Sheets" node to create report


Operation: Create
Folder ID: [Your reports folder]
Name: Google Ads Performance Report - {{$now.format('YYYY-MM-DD')}}
Sheets:
- Summary
- Campaign Performance
- Keyword Insights
- Ad Insights
- Recommendations
- Optimization Actions

Add an "Email" node for report distribution

Operation: Send
To: [Stakeholder emails]
Subject: Weekly Google Ads Performance Report
Text: [Report summary]
Attachments: [Performance report]

Step 12: Schedule Recurring Optimization


Add a "Schedule" node for recurring optimization

Frequency: Weekly
Day: Wednesday
Time: 10:00 AM

Add a "Function" node to check optimization status

// Check optimization status


const optimizationActions = $json.optimizationActions || [];

// Count actions by status


const actionCounts = {
pending: 0,
completed: 0,
failed: 0,
total: optimizationActions.length
};

optimizationActions.forEach(action => {
actionCounts[action.status] = (actionCounts[action.status] || 0) + 1;
});

// Check if all actions are completed


const allCompleted = actionCounts.pending === 0;

// Format status message


const statusMessage = `
Google Ads Optimization Status:
- Total Actions: ${actionCounts.total}
- Completed: ${actionCounts.completed}
- Pending: ${actionCounts.pending}
- Failed: ${actionCounts.failed}

${allCompleted ? 'All optimizations have been completed.' : 'Some optimizations are still
`;

return {
optimizationStatus: {
allCompleted,
actionCounts,
statusMessage
}
};

Add a "Google Sheets" node to update optimization status

Operation: Update
Spreadsheet ID: [Your optimization tracking spreadsheet]
Sheet: Optimization Actions
Key Column: ID
Data: [Updated status]

Automation in Action
When you run this workflow, n8n will:

Collect daily performance data from your Google Ads account

Store and analyze campaign, keyword, and ad performance

Generate AI-powered insights and recommendations

Create and implement optimization actions

Adjust bids for campaigns and keywords

Pause underperforming keywords

Create new ad variations based on top performers

Generate comprehensive performance reports

Track optimization actions and their impact

Customization Options
Add conversion tracking integration with your CRM

Implement A/B testing for ad variations

Create industry-specific optimization rules

Add competitor analysis for keyword research

Implement budget forecasting and allocation

Create custom alerts for performance thresholds

Add integration with other marketing channels

Tips for Success


Start with a few campaigns before scaling to your entire account
Review AI recommendations before implementing automated changes
Set performance thresholds based on your specific business goals

Regularly update your optimization rules as your business evolves

Use the performance reports to identify long-term trends

Combine automated optimizations with strategic manual reviews

Test different bid adjustment percentages to find optimal settings

This template is part of the "Vibe Marketing" automation series created for
@boringmarketer. It demonstrates how n8n's workflow automation can replace
manual Google Ads optimization tasks, allowing marketers to achieve better ad
performance with minimal effort.

© 2025 The Vibe Marketer. All rights reserved.

You might also like