Server-side GTM Firestore lookup variables

last verified · against GTM server-side Firestore API docs as of 2026-07

Reference for the server-side GTM Firestore Lookup variable and Firestore Sandboxed JS API: read, query, write, transactions, IAM roles, caching, and limits.

What this is

The Firestore Lookup variable and the Firestore Sandboxed JS API let a server-side Google Tag Manager container read and write documents in a Cloud Firestore (Native mode) database during request handling, enriching tags with server-held data. Both shipped for server-side containers on 2022-03-24, alongside the Promise Sandboxed JS API — release text: “Launched Firestore Sandboxed JS API, Firestore Lookup variable, and Promise Sandboxed JS API for server-side containers.”

Capabilities (launched 2022-03-24)

Capability Surface Use
Firestore Lookup variable Container UI variable Read one field/value from Firestore into a tag
Firestore Sandboxed JS API Custom templates Firestore.read / query / write / runTransaction
Promise Sandboxed JS API Custom templates Await the promises the Firestore API returns

Support constraints

Constraint Value
Firestore mode Native mode only — not Datastore mode
Database (default) only — named databases unsupported
projectId Optional; defaults to env GOOGLE_CLOUD_PROJECT
Result cache Enabled by default, for the duration of the request
Query limit Default 5
Transaction retries Up to two on conflict

Firestore Sandboxed JS API — functions

Function Resolves to Rejects / notes
Firestore.read(path, options) {id, data} {reason:'not_found'} when the document is missing
Firestore.query(collection, queryConditions, options) Array of matching documents Conditions ANDed; limit defaults to 5
Firestore.write(path, input, options) Written document ID No ID when inside a transaction (writes are batched)
Firestore.runTransaction(callback) Array of document IDs, one per write Retried up to two times on conflict

Options

Option Applies to Default Effect
projectId All functions env GOOGLE_CLOUD_PROJECT Target a different GCP project
disableCache read, query false (cache on) Bypass the per-request result cache
limit query 5 Maximum documents returned
merge write false Merge supplied keys vs overwrite the whole document
transaction read, write Transaction identifier from runTransaction

queryConditions

Shape: queryConditions: [[key, operator, expectedValue], ...]. Conditions are ANDed.

Element Position Meaning
key 0 Field path to test
operator 1 Comparison operator
expectedValue 2 Value compared against

The operator set is not enumerated on the API page — it defers to Firestore REST semantics. Assume Firestore REST operators (==, <, <=, >, >=, array-contains) and verify in your container. The element shape of each returned document is not stated verbatim on that page (“an array of Firestore documents that match”); confirm whether it matches read’s {id, data} empirically.

Errors

Reject shape: {reason: <code>}.

reason Trigger
not_found Firestore.read on a missing document
Firestore REST API error codes Any function; e.g. permission_denied

Firestore Lookup variable — UI fields

Practitioner-documented (Simo Ahava, Measurelab); official developer docs mention the variable only in passing. Treat labels as version-dependent and verify in the container.

Field Behavior
Lookup Type: Document Path Direct single-document read by collection/document path — fastest
Lookup Type: Collection Path & Query Match within a collection; if several match, only the FIRST document is used
Key Path Field/value to return; dot notation for nested/array values
Default Value Returned when nothing is found
Project ID Override to read Firestore in another GCP project

IAM roles

Role ID Grants
Cloud Datastore User roles/datastore.user Read + write — the role GTM’s setup guide specifies for Firestore access
Cloud Datastore Viewer roles/datastore.viewer Read-only — likely enough for a pure lookup, but not documented as supported
Cloud Datastore Admin roles/datastore.admin Administrative access to Cloud Datastore

Deployment env vars (container outside Google Cloud)

Variable Purpose
GOOGLE_APPLICATION_CREDENTIALS Path to the mounted service-account JSON key
GOOGLE_CLOUD_PROJECT Project the server selects implicitly; default projectId for lookups

Common patterns

Pattern Approach
Enrichment before dispatch BigQuery computes CLV / tier / price / consent, scheduled-exports to Firestore keyed by user_id / client_id / transaction_id; the variable reads it in real time to augment GA4 / Ads / Floodlight tags
Value-based bidding Store item monetary value in Firestore; inject it on purchase via a Firestore Lookup inside an Augment event transformation
Transaction de-duplication Write-then-check a Firestore document to block duplicate purchase events
Cross-device stitching Query a GCP database in real time to stitch visitor identity
Prefer Document Path When the document ID is derivable from request data (client_id, transaction_id) — a single direct read, the fastest option
Co-locate data and container Keep Firestore in the same GCP project when possible — the lookup runs synchronously in request handling and adds latency per tagged event

Gotchas

  1. Every lookup fails although the document exists — cause: the database is in Datastore mode or is a named (non-default) database — fix: provision Firestore in Native mode and use the (default) database; no supported alternative exists.
  2. Cross-project lookup returns nothing or permission_denied — cause: the container’s runtime service account has no IAM binding on the target Firestore project — fix: grant that service account roles/datastore.user on the project holding the data.
  3. A custom template aborts when a looked-up document is absent — cause: Firestore.read rejects with {reason:'not_found'}, surfacing as an unhandled rejection — fix: .catch() and branch on reason === 'not_found'; for the variable, set a Default Value.
  4. A value written earlier in the request reads back stale — cause: read / query cache results for the duration of the request — fix: pass disableCache:true, or perform the read and write inside Firestore.runTransaction.
  5. Collection Path & Query returns the wrong record — cause: results are bounded (limit default 5) and only the first match is used, so a loose query is non-deterministic — fix: make conditions match exactly one document, or key a Document Path lookup by a unique ID.
  6. Lookups behave oddly when a field holds an empty array — cause: a community-reported edge case whose root cause is not confirmed in official docs — fix: store a sentinel or omit empty-array fields and validate; verify against your container version.

Quick recipes

Direct read by document path, with missing-document handling:

firestore-read.js
const Firestore = require('Firestore');
// projectId omitted -> defaults to the GOOGLE_CLOUD_PROJECT env var.
return Firestore.read('users/' + clientId).then(function (doc) {
// Resolves to { id, data }.
return doc.data.tier;
}, function (err) {
// Rejects with { reason: 'not_found' } when the document is absent.
if (err.reason === 'not_found') return 'unknown';
return undefined;
});

Query a collection (conditions ANDed, capped by limit):

firestore-query.js
const Firestore = require('Firestore');
// Each condition is [key, operator, expectedValue].
const conditions = [
['country', '==', 'SE'],
['tier', '==', 'gold']
];
return Firestore.query('customers', conditions, { limit: 5 }).then(function (docs) {
// Resolves to an array of matching documents (default limit 5).
if (docs.length === 0) return 'none';
return docs[0];
});

Read the freshest value written earlier in the same request:

firestore-fresh-read.js
const Firestore = require('Firestore');
// disableCache bypasses the per-request result cache.
return Firestore.read('counters/daily', { disableCache: true }).then(function (doc) {
return doc.data.count;
});

Write with merge vs overwrite:

firestore-write.js
const Firestore = require('Firestore');
// merge:true updates only supplied keys; default (false) overwrites the whole document.
return Firestore.write('users/' + clientId, { tier: 'gold' }, { merge: true }).then(function (id) {
// Resolves to the written document ID.
return id;
});

Atomic read-modify-write inside a transaction:

firestore-transaction.js
const Firestore = require('Firestore');
// Retried up to two times on conflict; pass the transaction id to each read/write.
return Firestore.runTransaction(function (transaction) {
return Firestore.read('counters/daily', { transaction: transaction }).then(function (doc) {
const next = doc.data.count + 1;
// Batched write: returns no ID inside the transaction.
return Firestore.write('counters/daily', { count: next }, { transaction: transaction });
});
}).then(function (ids) {
// Resolves to an array of document IDs, one per write.
return ids;
});

Grant a cross-project service account read + write on Firestore:

grant-datastore-user.sh
# Bind the sGTM container's runtime service account on the project that holds
# the Firestore data (needed when Project ID points at another GCP project).
gcloud projects add-iam-policy-binding TARGET_PROJECT \
--member=serviceAccount:SA_EMAIL \
--role=roles/datastore.user

Sources

Official pages these values were read from (read 2026-07-20):

Practitioner references for the variable’s UI fields (not official Google docs):

Two limitations are worth stating explicitly. No dedicated official page documents the Lookup variable’s UI fields, so exact labels are version-dependent and should be verified in the container. And whether the read-only roles/datastore.viewer suffices for a pure lookup is unconfirmed — GTM’s setup guide specifies roles/datastore.user (read + write).

Changelog

  • — Initial version, verified against official documentation.

dataLayer

0 events · 0 sent

    • home /
      writing /writing
      guides /guides
      work /#work
      about /about
      colophon /colophon
      toggle analyst mode ctrl+.
      print session receipt /#receipt