What it does
On-demand BigQuery bills by bytes scanned, so one select * on an unpartitioned events table can cost more than a day of dashboards. The billing console shows the total for the project; it does not say that most of it came from one analyst's notebook or a BI tile with no date filter.
The agent reads the INFORMATION_SCHEMA.JOBS view for the last 7 days, turns bytes billed into dollars at the on-demand rate, and ranks users and repeated queries by cost. The top line usually has an obvious fix: a partition filter, a clustered column or a cached result.
How it works
1Sum bytes billed for the week
Query `
region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT(use your region) wherecreation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY),job_type = 'QUERY'andstatement_type != 'SCRIPT'(script parents would double count their children). Sumtotal_bytes_billed` and divide by 1024^4 for TiB.- 2
2Turn it into money
Multiply TiB by the on-demand rate, $6.25 per TiB in the US as of October 2026 (first TiB each month free). Jobs with a
reservation_idrun on capacity pricing; report theirtotal_slot_msseparately instead of bytes. 3Rank users
Group the same rows by
user_email(service accounts included) for bytes, cost and query count, top 10.4Rank repeated queries
Group by
query_info.query_hashes.normalized_literals, which treats queries that differ only in literal values as one; keep a samplequery, the run count, the user and the cost, top 10. Notecache_hitshare for BI service accounts.- 5
5Report
Total TiB and cost against the 7 days before, the top users and queries, and for the most expensive query one concrete fix (partition filter, clustering, fewer columns, a materialized table).
Post it to the board
With
BRIEFKIN_TOKENset, the agent sends the result as one card that updates in place every run. Without it, you get the report in the chat.
What you get
- Bytes billed in the last 7 days and the estimated on-demand cost, with the change.
- By user: TiB, cost and query count.
- By repeated query: a sample, runs, user and cost.
- Capacity jobs: slot hours, if you use reservations.
- One fix for the most expensive query.
Install and run it
Pick your agent, where the data comes from and how to schedule it; the commands below follow.
Install the skill
One command. It puts the SKILL.md in ~/.claude/skills and in ~/.agents/skills, which Codex, Cursor, Gemini CLI, Copilot CLI and OpenCode read too.
shell
curl -fsSL https://briefkin.dev/skills/bigquery-cost-by-user-and-query/install | shRun it
Ask Claude Code:
prompt
Run the bigquery-cost-by-user-and-query skillOr paste the prompt instead of installing:
prompt
In BigQuery (read only), query `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT for query jobs created in the last 7 days, excluding SCRIPT parent jobs. Sum total_bytes_billed as TiB and estimate the cost at $6.25 per TiB (on-demand, US), and compare with the 7 days before. Rank the top 10 users (user_email) by cost with query counts, and the top 10 repeated queries grouped by query_info.query_hashes.normalized_literals with a sample query, runs, user and cost. Report jobs with a reservation_id separately by slot hours. For the most expensive query, suggest one concrete fix such as a partition filter or clustering. Use my region and price if I give them.Put it on a schedule
Mondays at 8:00, from cron on any machine where Claude Code is signed in:
crontab
0 8 * * 1 claude -p "Run the bigquery-cost-by-user-and-query skill"Add it with
crontab -e. A laptop asleep at that time runs it when it wakes if you use launchd or Task Scheduler instead; GitHub Actions works for the API route.Send it to your board
Connect Briefkin once with
curl -fsSL https://briefkin.dev/install | shand every run lands on your board as a stat tile. How setup works
View the SKILL.mdRaw file
SKILL.md---
name: bigquery-cost-by-user-and-query
description: "Every Monday: BigQuery bytes billed and estimated on-demand cost for the last 7 days, with the users and repeated queries behind the biggest share. Use when the user asks for this, or when it runs on a schedule."
---
# BigQuery cost by user and query
Every Monday: BigQuery bytes billed and estimated on-demand cost for the last 7 days, with the users and repeated queries behind the biggest share.
On-demand BigQuery bills by bytes scanned, so one `select *` on an unpartitioned events table can cost more than a day of dashboards. The billing console shows the total for the project; it does not say that most of it came from one analyst's notebook or a BI tile with no date filter.
The agent reads the `INFORMATION_SCHEMA.JOBS` view for the last 7 days, turns bytes billed into dollars at the on-demand rate, and ranks users and repeated queries by cost. The top line usually has an obvious fix: a partition filter, a clustered column or a cached result.
## Two ways to get the data
Use the API or MCP route when you have an MCP server, a CLI or an API token for the app. Otherwise, if you can drive a browser the user is signed in to (Claude in Chrome, a Playwright or Chrome DevTools MCP server), use the browser route. Say which one you used.
## Route A: API or MCP server
### What you need
- A role with `bigquery.jobs.listAll` on the project, such as BigQuery Resource Viewer, to read everyone's jobs; `bigquery.jobs.create` to run the query.
- Access through the `bq` CLI (`bq query --use_legacy_sql=false`), the BigQuery API, or a BigQuery MCP server.
If something is missing, say what and stop; do not invent numbers.
### Steps
1. **Sum bytes billed for the week** (BigQuery): Query `` `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT `` (use your region) where `creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)`, `job_type = 'QUERY'` and `statement_type != 'SCRIPT'` (script parents would double count their children). Sum `total_bytes_billed` and divide by 1024^4 for TiB.
2. **Turn it into money**: Multiply TiB by the on-demand rate, $6.25 per TiB in the US as of October 2026 (first TiB each month free). Jobs with a `reservation_id` run on capacity pricing; report their `total_slot_ms` separately instead of bytes.
3. **Rank users** (BigQuery): Group the same rows by `user_email` (service accounts included) for bytes, cost and query count, top 10.
4. **Rank repeated queries** (BigQuery): Group by `query_info.query_hashes.normalized_literals`, which treats queries that differ only in literal values as one; keep a sample `query`, the run count, the user and the cost, top 10. Note `cache_hit` share for BI service accounts.
5. **Report**: Total TiB and cost against the 7 days before, the top users and queries, and for the most expensive query one concrete fix (partition filter, clustering, fewer columns, a materialized table).
## Route B: the browser
### What you need
- Signed in to the Google Cloud console with a role that includes `bigquery.jobs.listAll` on the project (BigQuery Resource Viewer or Admin) to see everyone's jobs, and `bigquery.jobs.create` to run the query.
If something is missing, say what and stop; do not invent numbers.
### Steps
Start at https://console.cloud.google.com/bigquery. The user is signed in already; never type passwords or codes, and ask if a sign-in page appears.
1. **Open BigQuery Studio on the project**: The console asks for a project if none is selected; pick the one whose jobs you pay for. Click **SQL query** (the plus button) to get an empty editor tab. The **Job history** panel at the bottom only lists recent jobs with a few columns, so the report comes from a query instead.
2. **Sum the week**: Paste and run a query on `` `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT `` (your region) with `creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY)`, `job_type = 'QUERY'` and `statement_type != 'SCRIPT'`, summing `total_bytes_billed` per week. Divide by 1024^4 for TiB and multiply by $6.25 for the on-demand estimate. The editor shows the bytes it will scan before you run.
3. **Rank users and repeated queries**: Run the same rows grouped by `user_email` (top 10), then grouped by `query_info.query_hashes.normalized_literals` with `ANY_VALUE(query)`, run count, user and bytes (top 10). Note the share of `cache_hit` for BI service accounts. Jobs with a `reservation_id` go in a separate line by `total_slot_ms`.
4. **Keep the results**: Results show in pages below the editor; use **Save results** > CSV or Google Sheets if you want them kept, and sort the user table by the cost column before reading it off.
5. **Report**: Total TiB and cost for the last 7 days against the 7 before, the top users and repeated queries, slot hours for reservation jobs, and one concrete fix for the most expensive query. Cross-check the total in **Billing** > **Reports** filtered to the BigQuery Analysis SKU if the numbers look off.
Note: The INFORMATION_SCHEMA.JOBS views keep 180 days and are free to query, but the query runs under your account, so it appears in next week's job history. The results panel shows up to a few hundred rows per page; the top 10 tables fit, the raw jobs do not. The on-demand rate differs by region; the first TiB each month is free and is not deducted here.
## The report
- **Bytes billed** in the last 7 days and the estimated on-demand cost, with the change.
- **By user**: TiB, cost and query count.
- **By repeated query**: a sample, runs, user and cost.
- **Capacity jobs**: slot hours, if you use reservations.
- One fix for the most expensive query.
## Post it to Briefkin
When `BRIEFKIN_TOKEN` is set, or `~/.config/briefkin/env` exists (the installer from https://briefkin.dev/docs writes it), also send the result to the user's Briefkin board as one card. Keep the `item_key` the same on every run so the card updates in place, use a new `event_id` per run, and put today's real numbers in place of the example ones. If the request fails, say so once and carry on.
```bash
[ -n "${BRIEFKIN_TOKEN:-}" ] || . ~/.config/briefkin/env
curl -sS -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
-H "authorization: Bearer $BRIEFKIN_TOKEN" \
-H "content-type: application/json" \
-d @- <<'JSON'
{
"v": 1,
"event_id": "bigquery-cost-by-user-and-query/<date of this run, e.g. 2026-10-16>",
"item_key": "bigquery-cost-by-user-and-query",
"project": "acme-analytics",
"source": "BigQuery cost by user and query skill",
"kind": "metric",
"title": "$186 of BigQuery queries this week, up 13%",
"metric": {
"label": "Estimated on-demand cost, last 7 days",
"value": 186.38,
"prefix": "$",
"better": "lower",
"period_start": "<start of the period, ISO 8601>",
"period_end": "<end of the period, ISO 8601>"
},
"glance": [
{
"type": "Grid",
"props": {
"columns": 3
},
"children": [
{
"type": "Stat",
"props": {
"label": "Billed",
"value": 29.82,
"unit": "TiB"
}
},
{
"type": "Stat",
"props": {
"label": "Queries",
"value": 18460
}
},
{
"type": "Stat",
"props": {
"label": "Top user",
"value": "dbt-prod"
}
}
]
}
],
"widget": [
{
"type": "Table",
"props": {
"columns": [
"User",
"TiB",
"Cost",
"Queries"
],
"align": [
"left",
"right",
"right",
"right"
],
"rows": [
[
"dbt-prod@acme-analytics.iam",
12.4,
"$77.50",
1204
],
[
"looker@acme-analytics.iam",
8.1,
"$50.63",
15890
],
[
"ana@example.com",
4.95,
"$30.94",
212
],
[
"ben@example.com",
2.61,
"$16.31",
388
],
[
"Everyone else",
1.76,
"$11.00",
766
]
]
}
},
{
"type": "Table",
"props": {
"columns": [
"Repeated query",
"Runs",
"Cost"
],
"align": [
"left",
"right",
"right"
],
"rows": [
[
"select * from analytics.events where user_id = ...",
26,
"$24.38"
],
[
"select date, count(*) from analytics.sessions group by 1",
2410,
"$19.06"
],
[
"merge analytics.fct_orders using staging.orders ...",
168,
"$14.75"
]
]
}
},
{
"type": "Callout",
"props": {
"text": "The events lookup scans the whole 150 GiB table each time. Adding a filter on event_date, the partition column, would cut it to a few GiB per run.",
"tone": "warn"
}
}
],
"size": "md"
}
JSON
```
Widgets come from the Briefkin catalog (https://briefkin.dev/v1/catalog). Without a token, just give the user the report.
Set `period_start` and `period_end` to the window the number covers: the tile names it from them ("Last 7 days", "Today", "This month"). For a count right now (open items, a balance), send `"period_label": ""` so no period shows; for another name, send it as `period_label` (at most 24 characters).
## Run it on a schedule
Suggested: Mondays at 8:00 (`0 8 * * 1`). From cron, with the agent signed in on that machine:
```bash
0 8 * * 1 claude -p "Run the bigquery-cost-by-user-and-query skill"
# Codex: codex exec, Gemini CLI: gemini -p, Copilot CLI: copilot -p, OpenCode: opencode run
```
Or use the agent's own scheduler (Claude Code routines, Codex automations, Claude or ChatGPT scheduled tasks): https://briefkin.dev/skills/bigquery-cost-by-user-and-query#schedule
Source: https://briefkin.dev/skills/bigquery-cost-by-user-and-query
What your agent needs
A role with
bigquery.jobs.listAllon the project, such as BigQuery Resource Viewer, to read everyone's jobs;bigquery.jobs.createto run the query.Access through the
bqCLI (bq query --use_legacy_sql=false), the BigQuery API, or a BigQuery MCP server.
Connecting BigQuery
- BigQuery MCP server, Google's remote server at
https://bigquery.googleapis.com/mcp, available in any project where the BigQuery API is enabled. Add it to Claude Code, Codex or Cursor as a remote HTTP server and sign in with Google OAuth using thehttps://www.googleapis.com/auth/bigqueryscope; Google's client setup guide covers each client. Its tools list datasets and tables, read schemas and run SQL;execute_sql_readonlyrejects DML and DDL, and queries stop after three minutes or 3,000 rows (docs). - The
bqCLI or the API with a service account.bq query --use_legacy_sql=falseruns the same SQL with your gcloud login. For scheduled runs, pointGOOGLE_APPLICATION_CREDENTIALSat a service account key and setBIGQUERY_PROJECT_ID.
Grant BigQuery Job User on the project and BigQuery Data Viewer on the datasets the report reads (plus roles/mcp.toolUser for the MCP server), and BigQuery Resource Viewer only if the report reads other people's jobs, which needs bigquery.jobs.listAll. An IAM deny policy on the writable execute_sql tool leaves the agent with the read-only one.
Questions
Will the estimate match the invoice?
Closely for on-demand projects, with two gaps: the first TiB each month is free, and the invoice may include storage, streaming and BI Engine charges that are not query jobs. For projects on BigQuery editions (capacity pricing), bytes do not drive cost, so the skill reports slot hours instead.
Can it cover every project in the organization?
JOBS_BY_ORGANIZATION covers all projects, but it needs the permission at the organization level. Most teams run the skill per project, or list the two or three projects that matter, which only needs the project-level role.
Do I need Briefkin to use the BigQuery cost by user and query skill?
No. The skill runs with your own agent and gives you the report in the chat. With a Briefkin token set, each run also posts it to your board as a stat tile, so you can glance at it instead of asking again.
Which agents can run it?
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, OpenCode, Windsurf and Amp all read SKILL.md files. The install command puts it in ~/.claude/skills and ~/.agents/skills, where they look. ChatGPT, the Claude app and Grok take the prompt instead. Pick your agent under "Install and run it" and the commands change to match.
Can it work without an API or MCP server, from my browser?
Yes. Switch the data source to "Your browser" and the steps change to the BigQuery pages themselves: the agent opens console.cloud.google.com/bigquery in a browser you are signed in to and reads the numbers off the page. Claude Code, Codex, Cursor, Claude app and ChatGPT can drive a browser out of the box; Gemini CLI, Copilot CLI and OpenCode need a Playwright or Chrome DevTools MCP server first. It is slower than the API and needs you signed in, so it suits a report you run by hand or from a scheduler on your own computer.