Agenda
- Overview of Looker Studio Connectors
- Prerequisites & Setup
- Connector Architecture
- Building the Connector
- Testing & Deployment
- Example: Weather API
- Q&A
What Are Looker Studio Community Connectors?
Pull data from any API or source into Looker Studio.
Use Cases:
- Connect to proprietary or less common APIs
- Customize data retrieval and shape as needed
Prerequisites & Setup
- Google Account with access to Apps Script
- Google Apps Script or CLI-based connector dev
- Familiarity with JavaScript/TypeScript
- Basic understanding of Looker Studio data model (dimensions, metrics)
Connector Architecture
getAuthType()
– Defines how users authenticate (NONE, OAUTH2, etc.)getConfig()
– Defines user-input parameters (API keys, etc.)getSchema()
– Describes data fields (dimensions, metrics, types)getData()
– Fetches data from the API, returns rows in the schema
Optional methods: isAdminUser()
, isAuthValid()
, etc.
Step 1: Create Apps Script Project
- Go to script.google.com
- Create a new project
- Rename the project (e.g., “My Looker Studio Connector”)
- You’ll have a default
Code.gs
file - Show and edit
appsscript.json
as needed
{
"timeZone": "Europe/Berlin",
"dependencies": {
},
"dataStudio": {
"name": "Weather Api (Diconium)",
"company": "Ralph",
"logoUrl": "https://24898528.fs1.hubspotusercontent-eu1.net/hub/24898528/hubfs/2024%20theme%20assets/Layer%201.png?width=594&height=344&name=Layer%201.png",
"addonUrl": "https://www.ataraxie.de",
"supportUrl": "https://www.ataraxie.de",
"description": "Looker Weather connector"
},
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request"
],
"urlFetchWhitelist": [
"https://weather.visualcrossing.com/",
"https://archive-api.open-meteo.com/"
],
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
Step 2: Define Authentication
Simple example for no-auth connectors. If your API requires OAuth2 or API keys, configure accordingly.
// https://developers.google.com/datastudio/connector/reference#getauthtype
function getAuthType() {
return { type: 'NONE' }; // No authentication required
}
Step 3: Configure User Inputs
Define input fields for your connector’s UI via getConfig()
.
function getConfig() {
var config = cc.getConfig();
config.newTextInput()
.setId("latitude")
.setName("Latitude")
.setHelpText("Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg")
.setPlaceholder("53.5511");
config.newTextInput()
.setId("longitude")
.setName("Longitude")
.setHelpText("Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg")
.setPlaceholder("9.9937");
var timezones = [
['Auto', 'auto'], ['UTC', 'UTC'], ['Europe/London', 'Europe/London'],
['Europe/Berlin', 'Europe/Berlin'], ['America/New_York', 'America/New_York'],
['America/Chicago', 'America/Chicago'], ['America/Los_Angeles', 'America/Los_Angeles'],
['Asia/Tokyo', 'Asia/Tokyo']
];
var tzSelect = config.newSelectSingle().setId("timezone").setName("Timezone");
timezones.forEach(function (tz) {
tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));
});
return config.build();
}
Step 4: Define the Schema
The schema lists each column’s name, label, data type, and usage (DIMENSION vs. METRIC). Ensure names match how you populate values in getData()
.
function getSchema() {
return {
schema: [
{
name: "date",
label: "Date",
dataType: "STRING",
semantics: {
conceptType: "DIMENSION",
semanticType: "YEAR_MONTH_DAY"
}
},
{ name: "temperature_2m_max", label: "Max Temperature (°C)", dataType: "NUMBER" },
{ name: "temperature_2m_min", label: "Min Temperature (°C)", dataType: "NUMBER" },
{ name: "temperature_2m_mean", label: "Mean Temperature (°C)", dataType: "NUMBER" },
{ name: "relative_humidity_2m_mean", label: "Mean Humidity (%)", dataType: "NUMBER" },
{ name: "precipitation_sum", label: "Precipitation (mm)", dataType: "NUMBER" },
{ name: "wind_speed_10m_max", label: "Max Wind Speed (km/h)", dataType: "NUMBER" },
{ name: "wind_direction_10m_dominant", label: "Dominant Wind Direction (°)", dataType: "NUMBER" }
]
};
}
Step 5: Fetch & Return Data
- Ensure the order of values matches the schema
- Convert arrays/objects to strings if necessary
- Handle null or missing values gracefully
function getData(request) {
var config = request.configParams;
var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';
// Validate coordinates
if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
cc.newUserError()
.setDebugText('Invalid coordinates')
.setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')
.throwException();
}
// Process fields and dates
var requestedFields = request.fields || [];
var fieldNames = requestedFields.map(function (f) { return f.name; });
var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });
schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });
var startDate, endDate;
if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {
startDate = formatDateForAPI(request.dateRange.startDate);
endDate = formatDateForAPI(request.dateRange.endDate);
} else {
var today = new Date();
var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');
endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');
}
// Build API URL
var params = {
latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,
daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',
timezone: tz
};
var url = 'https://archive-api.open-meteo.com/v1/archive?' +
Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');
var weatherData;
try {
var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
if (response.getResponseCode() !== 200) {
throw new Error('API error: ' + response.getResponseCode());
}
weatherData = JSON.parse(response.getContentText());
if (!weatherData.daily || !weatherData.daily.time) {
throw new Error('No data available');
}
Logger.log('Data fetched from API');
} catch (error) {
cc.newUserError()
.setDebugText('API Error: ' + error.toString())
.setText('Unable to fetch weather data. Check coordinates and try again.')
.throwException();
}
// Process data efficiently
var daily = weatherData.daily;
var fieldMap = {
'date': daily.time,
'temperature_2m_max': daily.temperature_2m_max,
'temperature_2m_min': daily.temperature_2m_min,
'temperature_2m_mean': daily.temperature_2m_mean,
'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,
'precipitation_sum': daily.precipitation_sum,
'wind_speed_10m_max': daily.wind_speed_10m_max,
'wind_direction_10m_dominant': daily.wind_direction_10m_dominant
};
var rows = daily.time.map(function (_, i) {
return {
values: schema.map(function (field) {
var data = fieldMap[field.name];
if (field.name === 'date') {
// Format date as YYYYMMDD for Looker Studio date range control
var dateStr = String(data[i] || '');
return dateStr.replace(/-/g, '');
}
return (data && data[i] !== undefined ? data[i] : null);
})
};
});
Logger.log('Processed ' + rows.length + ' rows');
return { schema: schema, rows: rows };
}
Step 6: Testing Your Connector
- Use
Logger.log()
in Apps Script to debug - In Looker Studio:
- Create a new data source → Select “Build Your Own” → Choose your connector
- Enter config parameters (API key, etc.) → “Connect”
- Verify fields → “Create Report” to visualize

Deployment
- Deploy in Apps Script: Click “Deploy” → “New Deployment” → Type: “Web app”
- Set access level (e.g., “Anyone” if no auth required)
- Share the deployment URL for others to use
Common Pitfalls
- Schema vs. Rows mismatch (field count must match)
- Data type mismatches (STRING vs. NUMBER)
- Missing/wrong Auth if API requires credentials
- Caching – Looker Studio may cache data
Best Practices
- Error Handling: Return readable error messages if API fails
- Refactor: Keep config, schema, and data logic organized
- Logging: Use
Logger.log()
for debugging - Documentation: Provide usage instructions, parameters, rate limits
Resources
Example: Weather API (Visual Crossing)
Connector Parameters:
- API Key: Required for authentication with the Visual Crossing Weather API.
- Location: City or latitude/longitude.
- Start and End Dates: Range to query.
Example Request:
https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/Hamburg/2025-01-01/2025-01-03?unitGroup=metric&include=days&key=LMGNFA7QYWBT2Z4G38UB89L79&contentType=json
Q&A
Thank you for attending!
Example: Weather API (Open-Meteo)
Connector Parameters:
- Latitude: e.g. 53.5511 for Hamburg
- Longitude: e.g. 9.9937 for Hamburg
- Timezone: auto, UTC, or IANA names like Europe/Berlin
- Date range handled via Looker Studio’s picker; defaults to last 30 days
Example Request:
https://archive-api.open-meteo.com/v1/archive?latitude=53.5511&longitude=9.9937&start_date=2025-06-01&end_date=2025-06-15&daily=temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max
// Looker Studio Plugin for Open-Meteo Weather API
// Community Connector implementation for Google Data Studio
var cc = DataStudioApp.createCommunityConnector();
// https://developers.google.com/datastudio/connector/reference#getauthtype
function getAuthType() {
return { type: 'NONE' }; // No authentication required
}
function isAdminUser() {
return true; // Allow all users to act as admin
}
function getConfig() {
var config = cc.getConfig();
config.newTextInput()
.setId("latitude")
.setName("Latitude")
.setHelpText("Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg")
.setPlaceholder("53.5511");
config.newTextInput()
.setId("longitude")
.setName("Longitude")
.setHelpText("Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg")
.setPlaceholder("9.9937");
var timezones = [
['Auto', 'auto'], ['UTC', 'UTC'], ['Europe/London', 'Europe/London'],
['Europe/Berlin', 'Europe/Berlin'], ['America/New_York', 'America/New_York'],
['America/Chicago', 'America/Chicago'], ['America/Los_Angeles', 'America/Los_Angeles'],
['Asia/Tokyo', 'Asia/Tokyo']
];
var tzSelect = config.newSelectSingle().setId("timezone").setName("Timezone");
timezones.forEach(function (tz) {
tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));
});
return config.build();
}
function getSchema() {
return {
schema: [
{
name: "date",
label: "Date",
dataType: "STRING",
semantics: {
conceptType: "DIMENSION",
semanticType: "YEAR_MONTH_DAY"
}
},
{ name: "temperature_2m_max", label: "Max Temperature (°C)", dataType: "NUMBER" },
{ name: "temperature_2m_min", label: "Min Temperature (°C)", dataType: "NUMBER" },
{ name: "temperature_2m_mean", label: "Mean Temperature (°C)", dataType: "NUMBER" },
{ name: "relative_humidity_2m_mean", label: "Mean Humidity (%)", dataType: "NUMBER" },
{ name: "precipitation_sum", label: "Precipitation (mm)", dataType: "NUMBER" },
{ name: "wind_speed_10m_max", label: "Max Wind Speed (km/h)", dataType: "NUMBER" },
{ name: "wind_direction_10m_dominant", label: "Dominant Wind Direction (°)", dataType: "NUMBER" }
]
};
}
// Helper function to format date for API (Open-Meteo uses YYYY-MM-DD)
function formatDateForAPI(dateString) {
return dateString && dateString.length === 8 ?
dateString.substring(0, 4) + '-' + dateString.substring(4, 6) + '-' + dateString.substring(6, 8) :
dateString;
}
function getData(request) {
var config = request.configParams;
var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';
// Validate coordinates
if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
cc.newUserError()
.setDebugText('Invalid coordinates')
.setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')
.throwException();
}
// Process fields and dates
var requestedFields = request.fields || [];
var fieldNames = requestedFields.map(function (f) { return f.name; });
var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });
schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });
var startDate, endDate;
if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {
startDate = formatDateForAPI(request.dateRange.startDate);
endDate = formatDateForAPI(request.dateRange.endDate);
} else {
var today = new Date();
var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');
endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');
}
// Build API URL
var params = {
latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,
daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',
timezone: tz
};
var url = 'https://archive-api.open-meteo.com/v1/archive?' +
Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');
var weatherData;
try {
var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
if (response.getResponseCode() !== 200) {
throw new Error('API error: ' + response.getResponseCode());
}
weatherData = JSON.parse(response.getContentText());
if (!weatherData.daily || !weatherData.daily.time) {
throw new Error('No data available');
}
Logger.log('Data fetched from API');
} catch (error) {
cc.newUserError()
.setDebugText('API Error: ' + error.toString())
.setText('Unable to fetch weather data. Check coordinates and try again.')
.throwException();
}
// Process data efficiently
var daily = weatherData.daily;
var fieldMap = {
'date': daily.time,
'temperature_2m_max': daily.temperature_2m_max,
'temperature_2m_min': daily.temperature_2m_min,
'temperature_2m_mean': daily.temperature_2m_mean,
'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,
'precipitation_sum': daily.precipitation_sum,
'wind_speed_10m_max': daily.wind_speed_10m_max,
'wind_direction_10m_dominant': daily.wind_direction_10m_dominant
};
var rows = daily.time.map(function (_, i) {
return {
values: schema.map(function (field) {
var data = fieldMap[field.name];
if (field.name === 'date') {
// Format date as YYYYMMDD for Looker Studio date range control
var dateStr = String(data[i] || '');
return dateStr.replace(/-/g, '');
}
return (data && data[i] !== undefined ? data[i] : null);
})
};
});
Logger.log('Processed ' + rows.length + ' rows');
return { schema: schema, rows: rows };
}
function testApiCall() {
return getData({
configParams: { latitude: "53.5511", longitude: "9.9937", timezone: "Europe/Berlin" },
dateRange: { startDate: "20250101", endDate: "20250112" },
fields: [{ name: "date" }, { name: "temperature_2m_max" }, { name: "temperature_2m_min" }]
});
}