Performance Max (PMax) has fundamentally changed how we manage paid search, shifting control from manual keyword levers to automated, machine-learning-driven intent matching. However, for Data Analysts and PPC Engineers, this shift introduced a significant visibility problem.
PMax campaigns are frequently criticized as "black boxes." While you can easily see campaign-level CPA or ROAS, determining exactly which headline, image, or video asset triggered that conversion is notoriously difficult. The Google Ads UI buries this data in the "Asset Detail" drawer, making it impossible to analyze at scale across hundreds of asset groups.
If you are trying to programmatically retrieve granular asset performance data to build custom dashboards or automate creative refreshes, standard reporting endpoints often return empty rows.
This guide provides a robust, technical solution using Google Ads Scripts and GAQL (Google Ads Query Language) to extract asset-level performance labels from your PMax campaigns.
The Root Cause: Why PMax Data is Hard to Retrieve
Before writing the code, it is critical to understand the underlying architecture of the Google Ads API regarding Performance Max. This prevents wasted time querying the wrong resources.
In traditional Search campaigns, an ad is a static entity. In PMax, an ad is dynamically assembled. Google’s algorithm mixes and matches headlines, descriptions, images, and logos in real-time based on the user's signals.
The Attribution Disconnect
Because the "ad" technically doesn't exist until the moment of impression, Google does not attribute hard metrics (Clicks, Cost, Conversions) to a single asset (e.g., "Headline 1"). Instead, the system evaluates the asset's contribution to the overall Asset Group.
Consequently, if you attempt to query ad_group_ad_asset_view (standard for RSA) for a PMax campaign, you will fail.
The Solution: asset_group_asset
To access PMax creative data, we must query the asset_group_asset resource.
Crucially, as of the current API version, this resource does not provide numeric performance metrics (like 15 clicks or $5.00 cost). Instead, it provides a performance_label. This enum (values: BEST, GOOD, LOW, LEARNING, PENDING) is the algorithmic grading of that asset's contribution to the objective.
The script below solves the visibility problem by extracting these labels alongside the actual asset content (Text or Image URLs), allowing you to identify creative fatigue programmatically.
The Fix: Automated Asset Performance Extraction
The following Google Ads Script performs these operations:
- Selects all active Performance Max campaigns.
- Executes a GAQL query against the
asset_group_assetresource. - Joins data with the
assetresource to retrieve the actual text or image URL. - Exports a matrix of Campaign, Asset Group, Asset Type, Content, and Performance Label to a Google Sheet.
Configuration
Create a new Google Sheet. Copy the URL. You will paste this into the SPREADSHEET_URL constant below.
The Script
/**
* PMax Asset Performance Exporter
* Extracts asset-level performance labels (BEST, GOOD, LOW) for PMax Campaigns.
*
* @author Principal Engineering Team
* @version 2.0.0 (Modern GAQL Implementation)
*/
const CONFIG = {
SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL_HERE', // Replace with your Sheet URL
SHEET_NAME: 'PMax_Assets',
// Filter for specific performance labels if needed.
// Leave empty to fetch all: []
LABEL_FILTER: ['LOW', 'GOOD', 'BEST']
};
function main() {
Logger.log('🚀 Starting PMax Asset Extraction...');
// validate spreadsheet access immediately
const sheet = prepareSpreadsheet();
// GAQL Query
// Note: We join asset_group_asset with 'asset' to get the content (text/image)
// and 'campaign' for context.
const query = `
SELECT
campaign.name,
asset_group.name,
asset_group_asset.field_type,
asset_group_asset.performance_label,
asset_group_asset.status,
asset.id,
asset.name,
asset.type,
asset.text_asset.text,
asset.image_asset.full_size.url,
asset.youtube_video_asset.youtube_video_id
FROM asset_group_asset
WHERE
campaign.advertising_channel_type = 'PERFORMANCE_MAX'
AND asset_group_asset.status = 'ENABLED'
AND campaign.status = 'ENABLED'
`;
// Use AdsApp.search for standard, high-performance iterator
const searchIterator = AdsApp.search(query);
const rows = [];
let count = 0;
// Modern iterator pattern
for (const row of searchIterator) {
const assetGroupAsset = row.assetGroupAsset;
const asset = row.asset;
const campaign = row.campaign;
const assetGroup = row.assetGroup;
// Normalize the performance label
const perfLabel = assetGroupAsset.performanceLabel || 'PENDING';
// Apply strict filtering if config is set
if (CONFIG.LABEL_FILTER.length > 0 && !CONFIG.LABEL_FILTER.includes(perfLabel)) {
continue;
}
// Determine the actual content based on asset type
const assetContent = extractAssetContent(asset);
rows.push([
campaign.name,
assetGroup.name,
asset.id,
assetGroupAsset.fieldType, // e.g., HEADLINE, DESCRIPTION, MARKETING_IMAGE
assetContent, // The actual text or URL
perfLabel, // BEST, GOOD, LOW, PENDING
assetGroupAsset.status
]);
count++;
// Batch write every 2000 rows to prevent memory limits
if (rows.length >= 2000) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
rows.length = 0; // Clear buffer
}
}
// Write remaining rows
if (rows.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}
Logger.log(`✅ Extraction Complete. Processed ${count} assets.`);
}
/**
* Helper to determine what actual data to display based on asset type.
* @param {Object} asset - The asset resource from GAQL
* @return {string} - Text string, Image URL, or Video ID
*/
function extractAssetContent(asset) {
if (asset.textAsset) {
return asset.textAsset.text;
}
if (asset.imageAsset && asset.imageAsset.fullSize) {
return asset.imageAsset.fullSize.url;
}
if (asset.youtubeVideoAsset) {
return `https://www.youtube.com/watch?v=${asset.youtubeVideoAsset.youtubeVideoId}`;
}
return 'N/A';
}
/**
* Initializes the spreadsheet with headers.
* @return {GoogleAppsScript.Spreadsheet.Sheet}
*/
function prepareSpreadsheet() {
const ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(CONFIG.SHEET_NAME);
}
sheet.clear();
// Set Header Row
const headers = [
'Campaign Name',
'Asset Group',
'Asset ID',
'Field Type',
'Content (Text/URL)',
'Performance Label',
'Status'
];
sheet.getRange(1, 1, 1, headers.length)
.setValues([headers])
.setFontWeight('bold')
.setBackground('#f3f3f3');
return sheet;
}
Deep Dive: How the Query Works
The magic of this script lies in the GAQL FROM clause.
1. asset_group_asset
This is the junction object. It links a reusable asset (which lives at the account level) to a specific asset_group (the PMax equivalent of an Ad Group). This is where the performance_label lives. If you query the asset resource directly, you will not get performance data specific to the campaign.
2. Field Type Segmentation
We request asset_group_asset.field_type. This is distinct from the asset's inherent type.
- Asset Type:
IMAGE - Field Type:
MARKETING_IMAGE,LOGO, orSQUARE_MARKETING_IMAGE.
Knowing the field_type is essential for analysis. A generic image performing "LOW" as a Logo might perform "BEST" as a Marketing Image. The script captures this nuance.
3. Handling null Performance
New assets often return a null or PENDING performance label. The script handles this gracefully:
const perfLabel = assetGroupAsset.performanceLabel || 'PENDING';
This ensures your data pipelines don't break when encountering new creatives that the algorithm hasn't yet graded.
Common Pitfalls and Edge Cases
When implementing this solution in a production environment, be aware of three specific edge cases that often trip up developers.
1. The "Learning" Phase
PMax requires significant data volume to assign labels. If your report returns mostly "PENDING" or "LEARNING," it is not a script error. It indicates the campaign has not reached the statistical significance threshold required by Google's backend to grade the asset.
2. Large Account Timeouts
Google Ads Scripts have a 30-minute execution limit. If you are running this across an MCC (Manager Account) with thousands of PMax asset groups, the script may time out.
- Solution: Use the
AdsApp.select(account)method to iterate through accounts, or implement script execution chaining (saving state to a spreadsheet and resuming).
3. Video Asset Privacy
For YouTube assets, the API returns the Video ID. However, if the video is set to "Private" in YouTube Studio, the script will retrieve the ID, but the generated link in the spreadsheet will be inaccessible to users without permissions. Ensure your creative team sets ads to "Unlisted" or "Public."
Conclusion
Performance Max doesn't have to be a black box. While we cannot currently fetch "Clicks per Headline" due to the dynamic nature of the ad server, the performance_label is the exact metric Google's own algorithm uses to prioritize delivery.
By automating the extraction of these labels with the script provided, you can build alerts for "LOW" performing assets, automate creative tickets for your design team, and finally bring data-driven rigor to your PMax creative strategy.