Skip to main content
The Structured Query API lets you run read-only SQL over the structured data Cloudglue extracts from your videos — file attributes, extracted entities, and connector source metadata. Where search finds moments and answers, structured queries count, group, join, and aggregate that data — so you can measure and report on your library at scale. You write ordinary SQL (SELECT only) against a small, well-defined set of virtual tables built from your collections. It is not a general database you write to — it is a read-only analytical view over your own extracted data.

Ways to Query

Synchronous runs return small-to-moderate result sets in one round trip; for anything large, aggregate server-side, raise max_rows, or use a background export.
Use Search or Deep Search to find content (“where does anyone mention pricing?”). Use Query to measure it (“how many videos per source platform?”, “which topics dominate our meetings?”).

Running a Query

Send one or more collection IDs and a single read-only SELECT statement. Each query costs 2 credits; failed runs are refunded automatically. Retrieving results is free.
The response is a query_result object with the echoed SQL, columns, rows, and usage:
Completed and failed runs are stored: re-fetch a run with GET /query/{id} and browse history with GET /query.

The Virtual Schema

Every query executes against three virtual tables built on the fly from the collections you select.
The entities and segment_entities tables carry each file’s most recent completed extraction only. This is deliberately different from the collection entities endpoint, which merges all extraction jobs — analytics must not double-count re-extractions.

files

One row per (file, collection) in the selected collections — a file that belongs to more than one selected collection appears once per collection. Files without extractions appear here but have no entities or segment_entities rows, so use LEFT JOIN when joining those tables, and join on both file_id and collection_id (see Joining across tables).

entities

File-level extracted entities, one row per (field, value element), from each file’s most recent completed extraction only. Files without a completed extraction have no rows here — LEFT JOIN from files on both file_id and collection_id to keep them.

segment_entities

Segment-level extracted entities, one row per segment, from each file’s most recent completed extraction only (same job as the entities table). Files without a completed extraction have no rows here — LEFT JOIN from files on both file_id and collection_id to keep them.

Discovering Your Fields

Before writing SQL, call GET /query/schema to see the virtual tables plus what each collection actually extracts:
Alongside the three tables, the response includes a per-collection summary with the collection’s verbatim extract configuration:
  • fields is a flat summary: each extracted field’s name, type, and level.
  • extract_schema is the collection’s extract schema verbatim — it shows the nested value shapes, so you know which JSON paths exist before writing json_extract expressions.
  • prompt is the collection’s extraction prompt verbatim, when one was configured.

Where each field lives

The level on each field tells you which table to query:
  • level: "file" — the field appears as rows in the entities long table. Query it via field / value / value_text:
  • level: "segment" — the field lives inside the segment_entities.entities JSON column. Use json_extract / json_extract_string with a path derived from the extract_schema:
  • Metadata collections have extract_schema: null — there is no extraction to query. Query them through the files table’s metadata and source_metadata JSON columns instead.

Joining across tables

The same file can belong to more than one of the collections you select, in which case it appears once per collection in files (and its entities appear once per collection in entities / segment_entities). Always join on both file_id and collection_id so a shared file doesn’t cross-join across collections:

Dialect and Restrictions

A query must be a single read-only SELECT statement. Everything else is rejected:
  • DDL and DML (CREATE, INSERT, UPDATE, DELETE, DROP, …)
  • ATTACH, COPY, SET, PRAGMA, INSTALL, LOAD
  • Multiple statements in one request
Filesystem and network access are disabled, and each query runs in an isolated context that can only see your account’s data for the selected collections. Standard SQL is supported, including JSON functions (json_extract, json_extract_string, ->, ->>), analytics functions (date_trunc, window functions), and CTEs. See the worked examples below for the JSON extraction syntax. BIGINT values within the safe integer range are returned as JSON numbers.

Worked Examples

Structured queries span file attributes (duration, dimensions, size, timestamps), user metadata, connector source metadata, and extracted entities — you can filter, aggregate, and join across all of them. Count files per ingestion source:
Count videos in each collection:
Filter by file attributes — videos longer than 20 seconds:
Find portrait (“skinny”) videos by aspect ratio:
Filter segments by duration — segments shorter than 20 seconds:
Aggregate a segment-level field across a collection (given a segment-level topic field in the extract schema):
Filter files by connector source metadata (here, Zoom recordings by host):
Filter by user-supplied metadata — files that have a project key set (use IS NOT NULL to test for a key’s presence):
Match inside an array of source-metadata objects — Grain recordings with a given participant email. The [*] wildcard path extracts the whole list of email values, then list_contains tests membership:
Find files that have no completed extraction yet (LEFT JOIN keeps them):

Natural Language Queries

Instead of sql, send a query in plain English. Cloudglue compiles it to SQL against the same virtual schema and runs it, returning the compiled statement in sql so you can inspect, tweak, and resubmit it as a regular sql query.
The response is the same query_result shape as a SQL query, with sql set to the compiled statement. Natural language runs cost 4 credits (refunded if compilation fails). Provide exactly one of sql or query.
Natural language is best for straightforward analytical questions — counts, rankings, group-bys. A question that needs deep nested-JSON unnesting is hit-or-miss; when a question can’t be compiled it fails with 422 rather than guessing, so write the SQL directly for those.

Dry Run

Add dry_run: true to validate — and, for a natural-language query, compile — a statement and get back the effective SQL plus its output column schema without executing it over any data. No rows are produced, and it is billed at a reduced rate: 1 credit for sql, 2 for a natural-language query.
Use it to confirm a query compiles and to preview its columns before running it for real. dry_run cannot be combined with background.

Background Exports

For result sets too large to return inline, run the query as a background export. It streams the full result to a gzipped CSV or JSONL file in cloud storage and hands back a 24-hour signed download URL. Set background: true and a format:
The call returns immediately with status: "in_progress" and an id. Poll GET /query/{id} until status is completed (or failed), then download from download_url:
  • The URL is valid for 24 hours and is not refreshed on read — if it expires, re-run the export.
  • Output is capped at 2 GB compressed; a run that would exceed it fails with an actionable error and is refunded.
  • Natural-language (query) exports are supported too — the query is compiled server-side before streaming.
  • Billing: exports reserve 4 credits, reconciled to +1 credit per 100 MB of compressed output on completion. A failed or cancelled export is refunded.

Cancelling an export

POST /query/{id}/cancel stops an in-progress export — the run is aborted mid-stream (the partial upload is discarded) and the reserved credits are refunded:
Cancelling a run that has already completed returns it unchanged.

Querying from the Agent

The Responses API agent (nimbus-002-preview) can run structured queries on your behalf. When an entity collection is part of its knowledge base, the model is given get_structured_data_schema and run_sql_query tools and decides when to query — e.g. to answer “how many meetings had more than three attendees?” it writes and runs the SQL itself. Each query it runs is surfaced as a cloudglue_query_call item in the response output (with the SQL and row counts; result rows are not included), so you never call /v1/query directly.

Limits, Credits, and Errors

Query shape — the number of output columns, joins, CTEs, and nesting depth — is not capped explicitly. Instead it is bounded by the dataset and execution-time limits above: a query that is too large or too complex fails with a 408 (time) or a 400 rather than a dedicated limit error. Aggregate server-side and select only the columns you need to stay well within these bounds.

Caveats

  • Latest extraction only. entities and segment_entities reflect each file’s most recent completed extraction. Re-extracting a file replaces its rows in query results; historical extractions are not visible here.
  • Truncation. Results are truncated at max_rows, and very large results are also capped when stored — re-fetching such a run via GET /query/{id} replays the truncated rows. Check the truncated flag; if it is true, narrow the query, aggregate further, or run a background export.
Not sure a query is right? Add dry_run: true to check it compiles and preview its columns before spending credits on a full run.