Google Ads Performance Optimization - templates.thevibemarketer.com
Google Ads Performance Optimization - templates.thevibemarketer.com
Back to Templates
Advertising
n8n
Template Benefits
AI-Powered Google Ads Save time and resources
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
Prerequisites
n8n account (cloud or self-hosted)
Google Ads account with API access
Step-by-Step Implementation
Step 1: Set Up Your n8n Workflow
Log in to your n8n account
Authentication: OAuth2
Resource: Campaign
Operation: Get
Account ID: [Your Google Ads account ID]
return { accountInfo };
Frequency: Daily
Time: 06:00 AM
Resource: Report
Operation: Get
Report Type: CAMPAIGN_PERFORMANCE_REPORT
Resource: Report
Operation: Get
Report Type: KEYWORDS_PERFORMANCE_REPORT
Date Range: YESTERDAY
Metrics: impressions, clicks, cost, conversions, conversion_value, quality_score
Resource: Report
Operation: Get
Report Type: AD_PERFORMANCE_REPORT
Date Range: YESTERDAY
Metrics: impressions, clicks, cost, conversions, conversion_value
// 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]
});
});
Operation: Append
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Keyword Performance
Data: [Keyword data array]
Operation: Append
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Ad Performance
Data: [Ad data array]
Frequency: Weekly
Day: Monday
Time: 08:00 AM
Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Campaign Performance
// 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]
});
});
// 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)
};
return totals;
}
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 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
};
Object.keys(weights).forEach(metric => {
// Skip if change is not a number
if (isNaN(changes[metric])) return;
// Normalize score
const normalizedScore = totalWeight > 0 ? score / totalWeight : 0;
// 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',
return recommendations;
}
return { campaignAnalysis };
Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Keyword Performance
// 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]
});
});
// 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,
};
// 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);
Operation: Read
Spreadsheet ID: [Your performance tracking spreadsheet]
Sheet: Ad Performance
// Analyze ad performance
const adData = $json.data || [];
const adAnalysis = {
topPerforming: [],
underperforming: [],
opportunities: []
};
// 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];
// 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,
};
// 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);
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,
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.
// Format recommendations
const recommendations = {
bidAdjustments,
adCopySuggestions: adCopyText,
newKeywordSuggestions: newKeywordText,
fullInsights: aiInsights
};
return { recommendations };
id: adjustment.id,
name: adjustment.name || adjustment.text,
adjustment: adjustment.adjustment,
reason: adjustment.reason,
status: 'pending'
});
});
}
status: 'pending'
});
}
});
}
// 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'
});
}
});
}
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'
});
}
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 };
Operation: Append
Spreadsheet ID: [Your optimization tracking spreadsheet]
Sheet: Optimization Actions
Data: [Optimization actions array]
// Group by type
const campaignBidAdjustments = bidAdjustments.filter(action =>
action.type === 'campaign_bid_adjustment'
);
return {
campaignBidAdjustments,
keywordBidAdjustments
};
Resource: Campaign
Operation: Update
Campaign ID: {{$json.id}}
Bid Adjustment: {{$json.adjustment}}
Resource: Keyword
Operation: Update
Keyword ID: {{$json.id}}
Bid Adjustment: {{$json.adjustment}}
return { keywordPauses };
Resource: Keyword
Operation: Update
Keyword ID: {{$json.id}}
Status: PAUSED
// Prepare ad creation
const optimizationActions = $json.optimizationActions || [];
const recommendations = $json.recommendations || {};
adCreations.forEach(action => {
// Create variations based on the original ad
const baseHeadline = action.baseAdHeadline;
const baseDescription = action.baseAdDescription;
// Create variations
const variations = [];
// Headline variations
const headlineVariations = createVariations(baseHeadline, patterns.headlines);
// Description variations
const descriptionVariations = createVariations(baseDescription, patterns.descriptions);
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'
]
};
return {
headlines: headlinePatterns,
descriptions: descriptionPatterns
};
}
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);
}
return variations;
}
function addNumbers(text) {
// Add numbers to headline if not already present
if (/\d+/.test(text)) return text; // Already has numbers
function addUrgency(text) {
// Add urgency phrases
const urgencyPhrases = [
'Limited Time: ',
'Act Now: ',
'Today Only: ',
'Last Chance: '
];
function addQuestion(text) {
// Convert statement to question
if (text.endsWith('?')) return text; // Already a question
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'
];
function addBenefit(text) {
// Add benefit phrases
const benefitPhrases = [
'Save time and money',
'Boost your results',
'Improve efficiency by 50%',
'Reduce costs while increasing quality'
];
return { adVariations };
Resource: Ad
Operation: Create
Ad Group ID: {{$json.adGroupId}}
Headline: {{$json.headline}}
Description: {{$json.description}}
const dateRange = {
start: lastWeek.toISOString().split('T')[0],
end: today.toISOString().split('T')[0]
};
optimizationActions.forEach(action => {
optimizationSummary.byType[action.type] = (optimizationSummary.byType[action.type] ||
});
return { reportData };
Operation: Send
To: [Stakeholder emails]
Subject: Weekly Google Ads Performance Report
Text: [Report summary]
Attachments: [Performance report]
Frequency: Weekly
Day: Wednesday
Time: 10:00 AM
optimizationActions.forEach(action => {
actionCounts[action.status] = (actionCounts[action.status] || 0) + 1;
});
${allCompleted ? 'All optimizations have been completed.' : 'Some optimizations are still
`;
return {
optimizationStatus: {
allCompleted,
actionCounts,
statusMessage
}
};
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:
Customization Options
Add conversion tracking integration with your CRM
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.