Reten Docs

Preview

Runs an event filter against BigQuery and returns a bounded sample of rows plus the total it matches.

Preview

Runs an event filter against BigQuery and returns a sample of the real rows it selects, plus the total. They are two routes with the same response: one runs a definition sent in the body and the other an already saved filter.

RouteDefinition sourceSuccess code
POST /api/event-criterias/preview-from-bodyThe body201
POST /api/event-criterias/:id/previewThe saved filter201

Auth: Required — event-criteria:view-sql permission on both routes

Unlike Validate, which does a dry-run, preview runs the query: it consumes BigQuery quota and its cost shows up in bytesProcessed.


POST /api/event-criterias/preview-from-body

Request Body

FieldTypeRequiredDescription
queryBlocksQueryBlockDto[]YesAt least 1 element. The definition to run.
limitnumberNoSample size. Integer between 1 and 500. Default 100.

The structure of each block is in Definition.

This endpoint receives only the definition and the limit: name, description and format are not part of the body and sending them produces a 400 for an unknown property.

Example

curl -X POST https://api.reten.ai/api/event-criterias/preview-from-body \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 50,
    "queryBlocks": [
      {
        "order": 0,
        "from": { "table": "event_transactions", "joins": [] },
        "select": {
          "distinct": true,
          "items": [
            { "kind": "column", "column": { "table": "event_transactions", "column": "event_id" } },
            { "kind": "column", "column": { "table": "event_transactions", "column": "event_date" } }
          ]
        },
        "timeWindow": {
          "timeOperator": "IN_THE_LAST",
          "reference": { "table": "event_transactions", "column": "event_date" },
          "unit": "DAYS",
          "value": 30
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "Facturada"
            }
          ]
        }
      }
    ]
  }'
import axios from 'axios';

const body = {
  limit: 50,
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions', joins: [] },
      select: {
        distinct: true,
        items: [
          { kind: 'column', column: { table: 'event_transactions', column: 'event_id' } },
          { kind: 'column', column: { table: 'event_transactions', column: 'event_date' } },
        ],
      },
      timeWindow: {
        timeOperator: 'IN_THE_LAST',
        reference: { table: 'event_transactions', column: 'event_date' },
        unit: 'DAYS',
        value: 30,
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'Facturada',
          },
        ],
      },
    },
  ],
};

const response = await axios.post(
  'https://api.reten.ai/api/event-criterias/preview-from-body',
  body,
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const { rows, totalCount, bytesProcessed } = response.data;

POST /api/event-criterias/:id/preview

Runs an already saved filter. The definition is read from the filter and validated before running it.

Path Parameters

ParameterTypeDescription
idUUIDThe event filter id.

Request Body

FieldTypeRequiredDescription
limitnumberNoSample size. Integer between 1 and 500. Default 100.

The body does not carry the definition: the saved filter is the definition. Sending queryBlocks produces a 400 for an unknown property.

Example

curl -X POST https://api.reten.ai/api/event-criterias/6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a/preview \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>" \
  -H "Content-Type: application/json" \
  -d '{ "limit": 50 }'
import axios from 'axios';

const id = '6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a';
const response = await axios.post(
  `https://api.reten.ai/api/event-criterias/${id}/preview`,
  { limit: 50 },
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const { rows, totalCount, bytesProcessed } = response.data;

limit — sample size

limit bounds only how many rows the response brings in rows (the sample), not the universe that the filter selects.

  • Range: integer 1..500 (less than 1 or greater than 500 ⇒ 400).
  • Default: 100 when omitted.

Preview runs two queries over the same definition: one SELECT … LIMIT <limit> for the sample and one SELECT COUNT(*) for the total. That is why you can see rows.length = 50 with totalCount = 4821: rows is a glance, totalCount is the real number of rows the filter selects.

Response

Status 201 Created on both routes.

FieldTypeDescription
rowsRecord<string, unknown>[]Sample of real rows, bounded by limit. The columns depend on the definition's select (and on its aliases); that is why rows has no fixed shape.
totalCountnumberTotal rows the filter selects, not bounded by limit.
bytesProcessednumberBytes processed by BigQuery in the run. Sums the bytes of both queries (sample + count).

Example

201 response to the example above (limit: 50). The filter selects 4821 events; rows shows the first 50 (trimmed to 3 here for brevity), each with the projected columns:

{
  "rows": [
    { "event_id": "evt_9f3a1c", "event_date": { "value": "2026-07-14T18:02:11.000Z" } },
    { "event_id": "evt_2b7e04", "event_date": { "value": "2026-07-14T17:45:03.000Z" } },
    { "event_id": "evt_c081ff", "event_date": { "value": "2026-07-13T09:21:58.000Z" } }
  ],
  "totalCount": 4821,
  "bytesProcessed": 5242880
}

The keys of each object in rows (event_id, event_date) come from the definition's select: another definition produces other columns.

A TIMESTAMP column does not arrive as a string: it arrives wrapped as { "value": "<ISO 8601>" }. The ISO date is in row.event_date.value, not in row.event_date. The other catalog types arrive as JSON scalars.

Validation Rules

Before executing, the definition goes through the two layers of validation. Business validation runs on both routes: preview-from-body validates the definition in the body, and :id/preview validates the saved filter before executing it.

The complete catalog of rules and their exact messages is in Definition → Validation Rules.

If the definition passes the two layers but the BigQuery execution fails —by exceeding the billable bytes limit (10 GB), running out of the 30 s timeout, or any other engine error— the response is also 400, with a message of the form Query execution failed: <detail>.

Error Responses

StatusDescription
400Invalid DTO (limit outside 1..500, wrong type, enum out of range, unknown property), a violated business rule, or a BigQuery execution failure (Query execution failed: …). In :id/preview, also if the id is not a valid UUID.
401The Authorization: Bearer is missing or invalid.
403The user does not have the event-criteria:view-sql permission, or the x-tenant-id is not authorized.
404In :id/preview, no event filter with that id exists in the tenant (Event criteria not found). On both routes, if the tenant has no dataset configured.