Integrations

BigQuery

Answer typed questions about every row of a BigQuery table in SQL with Drex's remote functions, deployed by a Terraform module.

Drex for BigQuery deploys Drex, Nace's decision models, as BigQuery remote functions in your own Google Cloud project. Each function answers a typed question about each row and returns calibrated probabilities. See Questions for how to write them.

FunctionAsksReturns
drex.noulthe probability that a yes/no statement is trueFLOAT64
drex.choicewhich label fits bestJSON
drex.scorewhere the row sits on ordered levelsJSON
drex.decideseveral questions at once, about JSON stateJSON, Drex's response
drex.decide_imagequestions about images in Cloud StorageJSON, Drex's response

The Terraform module's repository, nace-ai/drex-bigquery, isn't public yet, so the steps below work once it's published.

Deploy

You need a Google Cloud project with billing, Terraform 1.5 or later and the gcloud CLI. Create a key as shown in Authentication.

Store the key in Secret Manager. The module only reads the secret, so the key never enters Terraform state:

gcloud secrets create nace-api-key --replication-policy=automatic
gcloud secrets versions add nace-api-key --data-file=-   # paste the key, then Ctrl-D

Then add the module and run terraform apply:

module "drex" {
  source = "git::https://github.com/nace-ai/drex-bigquery.git//terraform?ref=v0.1.0"

  project_id     = "my-project"
  region         = "us-central1" # a region in the dataset's location
  location       = "US"          # where your tables are
  api_key_secret = "nace-api-key"
}

The module deploys a Cloud Run function and creates a BigQuery connection, the dataset drex and its five functions. Only the connection's service account can call the function. People who run queries need the BigQuery Connection User role on the connection.

Usage

BigQuery remote functions take no optional arguments, so every argument is required. model is always last, and NULL means the default. Labels and questions are JSON.

Ask one question per row

Each non-NULL row is one Drex request. A NULL input returns NULL and isn't sent or billed.

SELECT
  ticket_id,
  drex.choice(
    body,
    'Which team should handle this support ticket?',
    JSON '{"billing": "Payments, invoices, refunds and charges", "technical": "Bugs, crashes and errors", "sales": null}',
    NULL
  ) AS team,
  drex.noul(body, 'Is the customer asking for a refund?', NULL, NULL) AS refund
FROM support.tickets;
FunctionArgumentsReturns
drex.noulinput, instructions, criteria, modelthe probability of yes
drex.choiceinput, instructions, labels, model{"choice", "probabilities": [{"value", "probability"}], "confidence"}
drex.scoreinput, instructions, levels, model{"score", "probabilities": [{"index", "value", "probability"}], "confidence"}

labels takes a JSON array of labels, an array of {"label", "description"} objects, or an object of label to description. levels takes an array of plain labels, lowest first. criteria is NULL, or a JSON object describing true and false. probabilities always holds every option: a choice's are sorted by label, a score's by index. Reading answers explains each field.

Read a field of a JSON result with BigQuery's JSON functions:

SELECT ticket_id, JSON_VALUE(team.choice) AS team, LAX_FLOAT64(team.confidence) AS confidence
FROM ticket_teams
WHERE LAX_FLOAT64(team.confidence) >= 0.8;

Ask several questions at once

drex.decide sends every question in one request per row, so the row is sent and billed once instead of once per question. It takes JSON state, so a whole row works too, and it returns Drex's response, with the answers under answers:

SELECT
  ticket_id,
  drex.decide(
    TO_JSON(t),
    JSON '''{
      "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["can wait", "this week", "today"]},
      "refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"}
    }''',
    NULL
  ) AS answers
FROM support.tickets AS t;

Read images

Drex Max reads images. drex.decide_image reads them from a Cloud Storage object table through signed URLs, and sends their bytes to Drex. Create the object table with the module's connection, and grant the connection's service account (the module's connection_service_account output) read access to the bucket.

SELECT
  uri,
  drex.decide_image(
    JSON '""',
    TO_JSON(signed_url),
    JSON '{"kind": {"type": "choice", "instructions": "What kind of document is Image 1?", "criteria": ["invoice", "receipt", "other"]}}',
    NULL
  ) AS answer
FROM EXTERNAL_OBJECT_TRANSFORM(TABLE scans.objects, ['SIGNED_URL']);

Pass up to 8 images as a JSON array, such as TO_JSON(ARRAY_AGG(signed_url ORDER BY uri)); prompts refer to them as "Image 1", "Image 2" and so on. PNG, JPEG and WebP are accepted. A row whose request would exceed the 4 MiB body limit returns NULL, so resize large images first. Drex Max's image limits list the rest.

Models

The default model is drex-v2.0, which reads text and JSON, except for drex.decide_image, whose default is drex-max. Pass 'drex-max' as the last argument for Drex Max. drex-v2.0 costs $0.06 and Drex Max $0.03 per million input tokens; see Pricing.

Pin the model in production if your queries rely on thresholds. A new model can shift probabilities.

The function checks a batch before it sends any of it: the model must be one that GET /v1/models lists for your key, images need a model that reads them, and the questions and labels must fit the model's limits. A query that fails a check returns an error.

Concurrency and rate limits

BigQuery sends the function batches of rows, at most 50 at a time (max_batching_rows; 1 for drex.decide_image). Each batch has up to 8 requests in flight, and the module runs one instance that answers one batch at a time, so a deployment sends at most 8 requests at once by default. That fits the in-flight limit of a free account:

TierRPMIn-flight
free1208
paid60016

On a paid account, raise it with the module's variables. The requests in flight are at most max_instances × container_concurrency × inflight_per_request:

module "drex" {
  # ...
  max_instances        = 2
  inflight_per_request = 8
}

Every API key on an account shares its limits (see Scope), so leave room when other clients use the same account. A request over the limit gets a 429 with the wait, and the function retries after it. BigQuery runs at most 10 queries with remote functions at once in a project.

Errors

  • 401, 402 and 422 fail the query. A 422 message lists each issue's path and message; a 402 links to Billing.
  • 429, 529 and 5xx are retried after retry-after-ms, within the batch. A row whose retries run out returns NULL, with a warning in the function's Cloud Run logs.
  • Each request times out after 60 seconds.

The function never answers BigQuery with an error BigQuery retries, so a batch with answered rows is never sent again because of one failed row. See Errors and retries.

What gets sent, and billed

Every row BigQuery evaluates a function on is a request, and every request Drex answers is billed.

  • BigQuery can send a batch again even after the function answered it, and those rows are billed again.
  • Running a query again sends its rows again. BigQuery never caches the result of a query that calls a remote function.
  • Keep the answers with CREATE TABLE ... AS SELECT and read the table:
CREATE TABLE support.ticket_teams AS
SELECT ticket_id, drex.choice(body, 'Which team should handle this support ticket?', JSON '["billing", "technical", "sales"]', NULL) AS team
FROM support.tickets
WHERE created_at >= '2026-10-01';

Cost

Each row is billed for its input tokens at the model's price, and output tokens aren't billed. For 10,000,000 rows:

Input tokens per rowInput tokens for 10,000,000 rowsdrex-v2.0drex-max
50500,000,000$30.00$15.00
2502,500,000,000$150.00$75.00
1,00010,000,000,000$600.00$300.00

The same row counts a different number of tokens on each model, so read usage.input_tokens from drex.decide for a sample of rows on the model you plan to use before running a whole table.

On this page