Mon, June 2, 2025
This article, like many others on this blog, appeared by chance — thanks to a question in the chat.
Last week, in our private PRO ANALYTICS course chat, Lena Kolisnyk asked: “Is it possible to export annotation data from GA4 into Google Sheets so that it can later be used in custom reports?” Neither I nor the students recalled a ready-made solution.
However, about a month ago on LinkedIn — and later in our internal ProAnalytics.Team chat — I came across a solution by Tamás Geiger that allows you to automatically create annotations in GA4 when publishing versions in GTM using Google Apps Script.
"If it's possible to create annotations via the API, then it must be possible to retrieve a list of them," I thought — and wrote a fairly simple script that pulls annotation data from GA4 and sends it to a Google Sheet, so you can then display it in your Looker, Power BI, or Tableau reports. Or use it for any other purpose.
To be honest, writing the actual solution took several times less effort than describing it here on the blog.
Here’s what you’ll need to do:
This is the simplest step. Go to your Google Drive and create a new Google Spreadsheet (we’ll call it “GA4 Annotations” for reference). In the top menu, select
Extensions → Apps Script. This will open the Google Apps Script environment linked to your spreadsheet.
Our script will use the Advanced Service “Google Analytics Admin API”, so we first need to activate this API in Google Cloud and then connect it to our script.
If you already have a GCP project, you can use it. If not, you’ll need to create a new one.
Once you’ve selected the appropriate project, in the left-hand menu choose APIs & Services → Library.
Use the search bar to find Google Analytics Admin API → open it → click Enable. This will allow you to call the ReportingDataAnnotations endpoints.
Go back to the Apps Script editor (the one that opened from your Google Sheet). In the left-hand menu, click the “+” icon next to “Services”.
In the list, find Google Analytics Admin API. Click “Add”.
After this, an entry for the advanced service will automatically be added to your appsscript.json
file. You’ll also need to add the required OAuth scopes (analytics.readonly
and spreadsheets
) to your appsscript.json
.
To open appsscript.json
, click the Project Settings icon (gear) in the left sidebar and enable Show “appsscript.json” manifest file in editor by checking the box.
Close the settings window. You should now see a new file called appsscript.json
in the file list on the left (next to Code.gs
).
In the end, your appsscript.json
file should look like this:
{
"timeZone": "Europe/Kyiv",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "AnalyticsAdmin",
"serviceId": "analyticsadmin",
"version": "v1alpha"
}
]
},
"oauthScopes": [
"https://www.googleapis.com/auth/analytics.readonly",
"https://www.googleapis.com/auth/spreadsheets"
]
}
The setup is almost complete. Now it’s time to add the main function — the one responsible for:
Create a new file called Code.gs
in your Apps Script project (if it doesn’t already exist) and paste in the following code.
Be sure to save the script (click the disk icon or press Ctrl+S).
/**
* Maks Hapchuk https://www.linkedin.com/in/maks-hapchuk/
*
* Main function: fetches all annotations from a GA4 Property and writes them to a Google Sheet.
*/
function fetchGA4AnnotationsToSheet() {
// Replace with your GA4 Property ID (numbers only, without the "properties/" prefix)
var PROPERTY_ID = '253674377';
// Construct the parent resource string: "properties/<PROPERTY_ID>"
var parent = 'properties/' + PROPERTY_ID;
// Call the list() method from the AnalyticsAdmin Advanced Service
var response;
try {
response = AnalyticsAdmin.Properties.ReportingDataAnnotations.list(parent);
} catch (e) {
throw new Error(
'Failed to fetch annotations from GA4. ' +
'Verify that PROPERTY_ID is correct and that you have access to the Analytics Admin API.'
);
}
// Extract the array of annotations
var annotations = response.reportingDataAnnotations || [];
// Access the active spreadsheet and locate (or create) the "Annotations" sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetName = 'Annotations';
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
} else {
sheet.clearContents();
}
// Write column headers
var headers = ['Title', 'Description', 'Start Date', 'End Date', 'Color'];
sheet.appendRow(headers);
// Make the header row bold
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
// If there are no annotations, stop execution
if (annotations.length === 0) {
return;
}
// Loop through each annotation and append a row to the sheet
annotations.forEach(function(annotation) {
var title = annotation.title || '';
var description = annotation.description || '';
// Determine whether this annotation has a single date or a date range
var startDate = '';
var endDate = '';
if (annotation.annotationDate) {
// Single-date annotation
startDate = formatDateObject(annotation.annotationDate);
endDate = startDate;
} else if (annotation.annotationDateRange) {
// Multi-date annotation
startDate = formatDateObject(annotation.annotationDateRange.startDate);
if (annotation.annotationDateRange.endDate) {
endDate = formatDateObject(annotation.annotationDateRange.endDate);
} else {
endDate = startDate;
}
}
var color = annotation.color || '';
sheet.appendRow([title, description, startDate, endDate, color]);
});
}
/**
* Helper function: formats an object {year, month, day} into "YYYY-MM-DD"
*/
function formatDateObject(obj) {
var year = obj.year.toString();
var month = (obj.month < 10 ? '0' : '') + obj.month.toString();
var day = (obj.day < 10 ? '0' : '') + obj.day.toString();
return year + '-' + month + '-' + day;
}
IMPORTANT:
var PROPERTY_ID = '111111111111';
with your actual GA4 Property ID.
To find your Property ID: open GA4 → go to Admin → in the “Property” column → Property details
The first time you run any function from Apps Script that needs access to external APIs or your Spreadsheet, Google will ask you to authorize the script.
To do this: In the Apps Script editor, select the function fetchGA4AnnotationsToSheet
from the dropdown (next to the “Run” button), then click “Run”.
After successful authorization, the script will run — and in your Spreadsheet, a new sheet named “Annotations” will appear, containing a set of columns:
and all your GA4 annotations listed in rows below.
Naturally, running this script manually every day wouldn’t be much better than copying annotations from the GA4 interface by hand —
so let’s configure it to run automatically once a day and keep your data up to date.
In the Apps Script editor (where you wrote your code), click on Triggers in the left sidebar. Then click + Add Trigger.
In the trigger setup form, configure it like this:
fetchGA4AnnotationsToSheet.
That’s it — from now on, your annotation data will be updated in the spreadsheet automatically every day.
I hope this method for pulling annotations from GA4 into Google Sheets — and then into any of your reports or dashboards — helps make the data in your reports more transparent and the decisions based on it more informed.
If you spot a bug, have an idea to improve this solution, or simply want to say thank you — the comments are always open for you.
Web Analyst, Marketer