Most GA4 BigQuery work starts as a folder of saved queries nobody can quite reproduce. Dataform turns that pile into a version-controlled pipeline inside BigQuery, for free. Here's how to build your first one, start to finish.

What Dataform is

Dataform is Google's transformation tool, built into the BigQuery console. It uses SQLX, which is plain SQL plus a config block, builds a dependency graph from how your models reference each other, and can schedule the whole thing.

No extra infrastructure, no license. If you're all-in on BigQuery, it's the path of least resistance to a real pipeline.

Declare your source

You start by telling Dataform which raw tables exist, so it can track them as dependencies rather than guessing.

-- definitions/sources/ga4.sqlx
config {
  type: "declaration",
  database: "your-project",
  schema: "analytics_123456789",
  name: "events_*"
}

Build a staging model

Next, a staging model that flattens the raw export into something clean, referencing the source with ref so Dataform knows the order to run things.

-- definitions/staging/stg_events.sqlx
config { type: "table", schema: "staging" }

SELECT
  PARSE_DATE('%Y%m%d', event_date) AS event_date,
  user_pseudo_id,
  event_name,
  (SELECT value.int_value FROM UNNEST(event_params)
   WHERE key = 'ga_session_id') AS session_id
FROM ${ref("events_*")}

Add a downstream model and a test

Then a model that builds sessions or metrics from staging, again with ref, and an assertion that fails the run if something's wrong, like a null session id.

That assertion is the whole reason to use Dataform over saved queries: the pipeline checks itself before it publishes bad data.

Schedule it

  1. Commit your SQLX to the repository, so every change is versioned.
  2. Run the graph once in the workspace to confirm it builds in order.
  3. Create a release and schedule it, so the whole pipeline runs daily on its own.

The assertion is the point

It's worth dwelling on why the test matters, because it's the feature that separates a pipeline from a pile of queries. An assertion is a query that's supposed to return zero rows; if it returns any, the run fails and the bad table never publishes.

-- definitions/assertions/no_null_sessions.sqlx
config { type: "assertion" }

SELECT *
FROM ${ref("stg_events")}
WHERE session_id IS NULL

Now a schema change upstream or a broken join doesn't quietly poison your dashboards; it stops the pipeline and tells you. That's the difference between finding a bug yourself and having a stakeholder find it in a board meeting.

Why it beats scheduled queries

A folder of scheduled queries has no dependency order, no tests, and no version history. Dataform gives you all three: it runs models in the right sequence, fails loudly on bad data, and every change is in git.

It also makes your work legible to the next person. Because every model declares its dependencies with ref, Dataform draws the DAG for you, so anyone can see how raw events become a sessions table without reverse-engineering a folder of SQL.

Where beginners trip

  • Skipping assertions, so bad data ships silently, which was the whole problem you were solving.
  • One enormous model instead of staging plus marts, which is impossible to debug.
  • Ignoring partitioning, so scheduled runs cost more than they should.
  • Hardcoding table names instead of using ref, which breaks the dependency graph and the run order with it.
  • Committing nothing to git, so the one advantage over saved queries goes unused.

Your first Dataform pipeline is mostly about turning ad-hoc SQL into something ordered, tested, and versioned, all without leaving the BigQuery console or paying for another tool. Declare your sources, stage then model with ref, add an assertion, and schedule it, and your GA4 reporting finally lives in a real pipeline instead of a folder of queries you're afraid to touch.

Want a stronger data analyst role or a raise? Grab the FREE Product Analyst Playbook and get the exact roadmap to your next offer.