Server-Side Custom Templates
When the Community Template Gallery does not have what you need, you build it yourself. Custom templates in sGTM use a sandboxed JavaScript environment with a specific set of APIs designed for server-side processing. The sandbox is intentionally restrictive — no window, no DOM, no arbitrary require() — but the available APIs cover everything you need to receive requests, make outbound calls, read and write cookies, and interact with Google Cloud services.
The sandboxed environment
Section titled “The sandboxed environment”sGTM templates execute in a JavaScript sandbox that is neither Node.js nor browser JavaScript. Standard APIs you might expect are absent or replaced:
Not available:
window,document,navigator,locationXMLHttpRequest,fetchrequire()for arbitrary modulestry/catchin the traditional sense (available but with restrictions)Promise,async/await(use callback patterns instead)newkeyword (cannot instantiate arbitrary classes)
Available via require():
sendHttpRequest— make outbound HTTP requestssendPixel— send a GET request to a URL (lightweight, no response needed)JSON— parse and stringify JSONsha256— compute SHA-256 hashgetTimestampMillis— current Unix timestamp in millisecondslogToConsole— write to Cloud LoggingtemplateDataStorage— per-instance key-value cachegetEventData— read from the Event Model (in tag templates)getAllEventData— read entire Event Model as objectgetRequestHeader— read request header (in client templates)getRequestBody— read request body (in client templates)getRequestPath— read request path (in client templates)claimRequest— claim the request (in client templates)runContainer— execute the container with an Event Model (in client templates)setResponseHeader— set response headers (in client templates)returnResponse— send HTTP response (in client templates)getGoogleAuth— get Google Cloud auth token (for GCP API calls)computeHmac— compute HMAC signature
Template types
Section titled “Template types”Tag templates
Section titled “Tag templates”Tag templates define actions that execute when a trigger fires. They run after a client has claimed the request and built the Event Model. Tag templates:
- Read from the Event Model via
getEventData() - Make outbound HTTP requests via
sendHttpRequest() - Write to Firestore, BigQuery, or other GCP services
- Must call
data.gtmOnSuccess()ordata.gtmOnFailure()to signal completion
Client templates
Section titled “Client templates”Client templates handle incoming HTTP requests. They:
- Evaluate whether to claim a request via
claimRequest() - Read request data (headers, body, path, query params)
- Build the Event Model
- Run the container via
runContainer() - Send the HTTP response via
returnResponse()
Variable templates
Section titled “Variable templates”Variable templates return a computed value. They:
- Read from the Event Model or request context
- Perform transformations (hashing, parsing, formatting)
- Return a single value that other templates reference
Variable templates must return synchronously. They cannot use sendHttpRequest with async callbacks and expect the result to be returned — that pattern requires a tag template instead.
Building a tag template: complete example
Section titled “Building a tag template: complete example”This example builds a complete tag template that sends event data to a custom analytics webhook:
Template code:
const sendHttpRequest = require('sendHttpRequest');const JSON = require('JSON');const getEventData = require('getEventData');const logToConsole = require('logToConsole');const getTimestampMillis = require('getTimestampMillis');
// Build the payload from Event Model dataconst payload = { event_name: getEventData('event_name'), client_id: getEventData('client_id'), user_id: getEventData('user_id'), page_location: getEventData('page_location'), value: getEventData('value'), currency: getEventData('currency'), transaction_id: getEventData('transaction_id'), timestamp_ms: getTimestampMillis(), // Include custom fields configured in template source: data.source,};
// Filter out undefined valuesconst filteredPayload = {};for (const key in payload) { if (payload[key] !== undefined && payload[key] !== null) { filteredPayload[key] = payload[key]; }}
const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + data.authToken, 'X-Source': 'sgtm', }, body: JSON.stringify(filteredPayload), timeout: 5000,};
sendHttpRequest( data.endpointUrl, requestOptions, function(statusCode, headers, body) { if (statusCode >= 200 && statusCode < 300) { data.gtmOnSuccess(); } else { logToConsole(JSON.stringify({ level: 'error', tag: 'webhook_forwarder', status: statusCode, event: getEventData('event_name'), })); data.gtmOnFailure(); } });Template fields (configured in the Fields tab):
endpointUrl— text field, the webhook URLauthToken— text field, marked ascanBeSensitive: truesource— text field with default valueweb
Template permissions (configured in Permissions tab): The sandbox requires explicit permission grants:
sends_pixel_toorsend_http_requestfor the webhook domainloggingforlogToConsoleread_event_dataforgetEventData
Permissions can be auto-detected by clicking Auto-Generate Permissions in the template editor.
Building a variable template: hash and normalize
Section titled “Building a variable template: hash and normalize”// Variable template: SHA-256 hash with normalizationconst sha256 = require('sha256');const getEventData = require('getEventData');
const fieldName = data.fieldName; // template fieldconst rawValue = getEventData(fieldName);
if (!rawValue || typeof rawValue !== 'string') { return undefined;}
// Normalize: lowercase, trim whitespacelet normalized = rawValue.toLowerCase().trim();
// For phone numbers: remove non-digits and formatif (data.fieldType === 'phone') { normalized = normalized.replace(/\D/g, ''); // Ensure country code present (simplified example) if (normalized.length === 10) { normalized = '1' + normalized; // assume US }}
return sha256(normalized);Fields:
fieldName— text (the Event Model field to read)fieldType— dropdown with optionsemail,phone,generic
Building a client template: custom endpoint
Section titled “Building a client template: custom endpoint”const claimRequest = require('claimRequest');const runContainer = require('runContainer');const getRequestPath = require('getRequestPath');const getRequestMethod = require('getRequestMethod');const getRequestBody = require('getRequestBody');const getRequestHeader = require('getRequestHeader');const returnResponse = require('returnResponse');const setResponseStatus = require('setResponseStatus');const JSON = require('JSON');const logToConsole = require('logToConsole');
const path = getRequestPath();const method = getRequestMethod();
// Only handle POST to /trackif (path !== '/track' || method !== 'POST') { return; // Not our request}
claimRequest();
// Validate API keyconst apiKey = getRequestHeader('x-api-key');if (apiKey !== data.expectedApiKey) { setResponseStatus(401); returnResponse(); return;}
// Parse request bodyconst rawBody = getRequestBody();let event;try { event = JSON.parse(rawBody);} catch (e) { setResponseStatus(400); returnResponse(); return;}
// Build Event Modelconst eventModel = { event_name: event.name || 'custom_event', client_id: event.client_id || event.anonymous_id || 'unknown', user_id: event.user_id, // Spread all other properties};
// Copy remaining propertiesfor (const key in event) { if (!eventModel[key]) { eventModel[key] = event[key]; }}
// Run the containerrunContainer(eventModel, function() { setResponseStatus(200); returnResponse();});The template testing framework
Section titled “The template testing framework”Every template has a Tests tab where you can write unit tests:
// Test: tag sends correct payload on purchase eventconst mockPurchaseEvent = { event_name: 'purchase', client_id: 'test123', value: 99.99, currency: 'USD', transaction_id: 'ORD-123',};
// Mock the event datamock('getEventData', function(key) { return mockPurchaseEvent[key];});
// Mock sendHttpRequest to capture what the tag sendslet capturedUrl = '';let capturedBody = '';mock('sendHttpRequest', function(url, options, callback) { capturedUrl = url; capturedBody = options.body; callback(200, {}, '');});
// Run the template coderunCode(data);
// AssertionsassertThat(capturedUrl).isEqualTo('https://webhook.example.com/events');const parsedBody = JSON.parse(capturedBody);assertThat(parsedBody.event_name).isEqualTo('purchase');assertThat(parsedBody.value).isEqualTo(99.99);Write tests for every template you build. The test runner catches regressions when you update template code.
Common mistakes
Section titled “Common mistakes”Using callbacks in variable templates. Variable templates must return synchronously. You cannot call sendHttpRequest inside a variable template and return the response value. If you need async data for a computed value, implement it in a tag template that runs before your data-forwarding tags and stores the result in templateDataStorage.
Not calling data.gtmOnSuccess(). Tag templates must call gtmOnSuccess() or gtmOnFailure() to signal completion to the container. Without this call, tag sequencing breaks and the container hangs waiting for completion.
Ignoring permissions. Templates with missing permissions fail silently in production. Always verify the permissions tab matches the APIs your code uses.
No error handling around JSON.parse. JSON.parse throws on invalid input. Wrap it in a try/catch equivalent pattern and return a sensible default or fail gracefully.
Hardcoding credentials. API keys and tokens should be template fields, not hardcoded strings. This makes credentials configurable via the GTM UI and avoids exposing them in template code.