SQL queries
SQL queries let you ask questions the assured operations do not cover: counts,
joins across claims and documents, timelines, anything you can express as one
read-only SELECT. They run over the query space, memory_v1: a fixed
set of prepared, read-only views and functions. Every statement is parsed and
validated against the query space before it runs, and anything outside it —
another schema, a function not on the list, a write — is rejected.
The query space itself (every view, column, function and limit) is described in Query space memory_v1. This page covers the seven routes.
All seven need the read scope. Base URL, authentication and error shapes are
described in HTTP API conventions.
How results and errors come back
Statement routes (POST /query/sql, POST /query/sql/explain,
POST /query/saved/{namespace}/{name}/run) return a
QueryResult/v1 in every outcome,
including a rejected or failed statement. A statement problem is not an HTTP
error:
termination_reason | HTTP status | Meaning |
|---|---|---|
completed | 200 | The statement ran. rows holds the result. |
rejected | 200 | The statement was refused before it ran: it did not parse, used something outside the query space, had the wrong parameters, or hit an admission limit. error_code and error_message say which. |
failed | 200 | The statement started and then failed: a timeout, a resource cap, an unavailable store. error_code and error_message say which. |
So check termination_reason (or error_code) on every 200.
HTTP errors come from outside the statement: a saved query that cannot run,
a malformed request body, an argument the server refuses before it builds a
statement. Those use the object form of detail:
{"detail": {"code": "saved_query_not_found", "message": "no saved query named examples.nope"}}The full list of error_code values, with the status each has when it is an
HTTP error, is in Errors and status codes.
Every statement runs under the interactive limits:
| Limit | Default | Hard cap |
|---|---|---|
| Rows returned | 200 | 1,000 |
| Bytes returned | 1,048,576 | 8,388,608 |
| Statement timeout | 5,000 ms | 15,000 ms (5,000 ms when a graph function is used) |
| SQL text | 65,536 bytes | |
| Parameters | 64, at most 262,144 bytes encoded | |
| Concurrent statements | 2 per caller, 8 per deployment | |
| Statement time | 30 s per caller per minute, 120 s per deployment per minute |
A caller here is the kind of credential, not the person: every signed credential counts as one caller, the self-hosted shared secret as another, and an open self-hosted deployment as a third. The analytical tier listed in Query space memory_v1 is not reachable over HTTP.
POST /query/sql
Run one read-only SQL statement.
Request body
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
sql | string | yes | One statement: SELECT, VALUES or WITH … SELECT. At most 65,536 bytes. | |
parameters | array | no | [] | Positional values for $1, $2, …. The count must equal the highest placeholder, and placeholders must be contiguous from $1. |
max_rows | integer | no | 200 | At least 0; values above 1,000 are clamped to 1,000. 0 returns no rows. |
Unknown fields are rejected. Cast parameters in the SQL when the type matters
($1::uuid, $2::timestamptz). Graph functions must take $1 as their first
argument, and $1 must be this deployment's id; the deployment_id field of
any earlier QueryResult or IngestedVersion gives it to you.
Response
200 with a QueryResult/v1. columns names each column with its SQL type;
rows is an array of arrays, one value per column, in column order.
{
"contract": "QueryResult/v1",
"grade": "exploratory_tabular",
"request_id": "2b0f…",
"deployment_id": "5d0c7a52-3b1e-4f55-9a8e-0e6c1f2b7a10",
"surface_manifest_hash": "d8be43966d90048ce3fc8ffe6dfdfc7943999fbf4f018ac2eb7998f2c995aae2",
"query_space_schema": "memory_v1",
"query_hash": "91c4…",
"query_language": "sql",
"saved_query": null,
"referenced_views": ["documents_live"],
"referenced_functions": ["count"],
"source_grain_tags": ["document_lineage_live"],
"columns": [
{"name": "source_kind", "type": "text", "nullable": true},
{"name": "documents", "type": "bigint", "nullable": true}
],
"rows": [["notes", 42], ["transcripts", 17]],
"returned_row_count": 2,
"returned_byte_count": 34,
"limits": {"row_cap": 200, "byte_cap": 1048576, "statement_timeout_ms": 5000, "analytical_tier": false},
"truncated": false,
"truncation_reason": null,
"exact_total_known": false,
"exact_total": null,
"ordered_result": true,
"empty_result": false,
"negative_kind": null,
"execution_started_at": "2026-09-23T10:00:00.120000+00:00",
"evaluated_at": null,
"pg_snapshot_at": "2026-09-23T10:00:00.121000+00:00",
"elapsed_ms": 18.4,
"termination_reason": "completed",
"error_code": null,
"error_message": null,
"warnings": [],
"semantic_invocations": [],
"graph_invocations": []
}Errors
| Status | detail | Cause |
|---|---|---|
422 | validation list | sql missing, parameters not an array, max_rows negative, or an unknown field. |
Everything else about the statement is reported inside the 200 result.
Example
curl -s -X POST "$REMEMBER_API_URL/query/sql" \
-H "Authorization: Bearer $REMEMBER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT source_kind, count(*) AS documents FROM documents_live GROUP BY source_kind ORDER BY documents DESC",
"max_rows": 50}'from remember import Client
memory = Client()
result = memory.open_query(
"SELECT claim_text, asserted_at FROM claims_live"
" WHERE claim_text ILIKE $1 ORDER BY asserted_at DESC",
parameters=["%cutover%"],
max_rows=20,
)
if result["termination_reason"] != "completed":
raise RuntimeError(f"{result['error_code']}: {result['error_message']}")
for row in result.rows:
print(row)open_query returns a dictionary with .rows, .columns and .truncated
shortcuts; query_sql(sql=..., parameters=..., max_rows=...) returns the
plain dictionary. Each row is a list of values in column order.
POST /query/sql/explain
Validate one statement and return PostgreSQL's plan for it without running it.
Request body
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
sql | string | yes | As for POST /query/sql. | |
parameters | array | no | [] | As for POST /query/sql. |
Unknown fields (including max_rows) are rejected.
Response
200 with a QueryResult/v1 whose one row holds the plan as JSON
(EXPLAIN (FORMAT JSON)). The same validation, parameter checks and
admission limits as POST /query/sql apply. Semantic and lexical search
functions are not called: the planner sees an empty relation of the same
shape in their place.
Errors
As for POST /query/sql.
Example
curl -s -X POST "$REMEMBER_API_URL/query/sql/explain" \
-H "Authorization: Bearer $REMEMBER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT count(*) FROM facts_current WHERE subject_entity_id = $1::uuid",
"parameters": ["7c1e5a0e-2f4b-4d8a-9c3e-1b2a3c4d5e6f"]}'plan = memory.explain_query(
"SELECT count(*) FROM facts_current WHERE subject_entity_id = $1::uuid",
parameters=[str(dana_id)],
)GET /query/space
Describe the query space: its views and columns, functions, limits, grammar allowlists and, optionally, the shipped example queries. The answer comes from the checked-in manifest, never from your data, so it is identical for every deployment on the same release.
Parameters
| Name | In | Type | Required | Default | Constraints |
|---|---|---|---|---|---|
pattern | query | string | no | all views | A shell-style glob matched against view names (facts_*). |
include_examples | query | boolean | no | false | Also list the shipped example query names. |
Response
200 with a JSON object:
| Field | Type | Contents |
|---|---|---|
schema | string | memory_v1 |
schema_major | integer | 1 |
surface_manifest_hash | string | The manifest hash every QueryResult also reports. |
headline | string | A short orientation for agents. |
retrieval_choices | array of string | Guidance on choosing between operations, search and SQL. |
honesty_warnings | array of string | What SQL results do not guarantee. |
worked_examples | array of object | Worked examples. |
views | array of object | One per matching view: name, grain, row_key (array), comment, columns (array of [name, type, nullable]). |
functions | array of string | The 12 public function names. |
limits | object | interactive and analytical limit sets. |
core_operation_descriptors | object | The assured operations as the manifest records them. |
function_signatures | object | Every function's arguments, result columns and caps. |
sql_grammar | object | The allowlists: functions, operators, cast_types, statement_node_classes, public_functions, srf_categories, srf_invocations_max_per_category, recursion_depth_max. |
examples | array of string | examples.<name> for each shipped example when include_examples=true; empty otherwise. |
Errors
| Status | detail | Cause |
|---|---|---|
422 | validation list | include_examples is not a boolean. |
Example
curl -s "$REMEMBER_API_URL/query/space?pattern=facts_*" \
-H "Authorization: Bearer $REMEMBER_API_KEY"space = memory.describe_query_space(pattern="facts_*", include_examples=True)GET /query/space/search
Search the query space's own text — view and column comments, function descriptions, operation descriptions, example purposes — for a phrase. It never searches your data.
Parameters
| Name | In | Type | Required | Default | Constraints |
|---|---|---|---|---|---|
query | query | string | yes | At least 1 character, and at least one word. | |
k | query | integer | no | 10 | 1 to 25. |
Response
200 with an array of hits, best first:
| Field | Type | Contents |
|---|---|---|
kind | view | function | core_operation | example | What the hit is. |
name | string | Its name (facts_current, semantic_claims, examples.claims_about). |
score | number | Term-overlap score. A word in the name counts more than a word in the text. |
purpose | string | The comment or description that matched. |
tags | array of string | Grain and key names for views; channel and target for functions. |
Errors
| Status | detail | Cause |
|---|---|---|
422 | validation list | query missing or empty, or k out of range. |
422 | {"code": "invalid_parameter", "message": "query must be non-empty"} | query is only whitespace. |
Example
curl -s -G "$REMEMBER_API_URL/query/space/search" \
-H "Authorization: Bearer $REMEMBER_API_KEY" \
--data-urlencode "query=current facts" \
--data-urlencode "k=5"hits = memory.search_query_space(query="current facts", k=5)GET /query/saved
List saved queries. A saved query is a named, versioned SQL statement stored in
the deployment. Every deployment ships with 18 in the examples namespace;
see Query space memory_v1.
Parameters
| Name | In | Type | Required | Default | Constraints |
|---|---|---|---|---|---|
namespace | query | string | no | all | |
status | query | string | no | active | One of draft, pending_revalidation, active, deprecated, disabled, broken. |
Without status, only active versions of queries that are not disabled are
listed. With status=draft, only the latest draft of each query is listed.
Response
200 with an array, ordered by namespace, name and version:
| Field | Type |
|---|---|
query_id | UUID |
namespace | string |
name | string |
version | integer |
status | string |
description | string or null |
origin | human | agent | import | shipped_example |
assurance | customer_authored | customer_reviewed | shipped_example | null |
query_hash | string |
validated_surface_manifest_hash | string |
Errors
| Status | detail | Cause |
|---|---|---|
422 | validation list | status is not one of the six values. |
Example
curl -s "$REMEMBER_API_URL/query/saved?namespace=examples" \
-H "Authorization: Bearer $REMEMBER_API_KEY"for saved in memory.list_saved_queries(namespace="examples"):
print(saved["name"], saved["version"], saved["description"])GET /query/saved/{namespace}/{name}
Describe one version of a saved query, including its SQL.
Parameters
| Name | In | Type | Required | Default | Constraints |
|---|---|---|---|---|---|
namespace | path | string | yes | ^[a-z][a-z0-9_]*$ (enforced by the remember client). | |
name | path | string | yes | ^[a-z][a-z0-9_]*$ (enforced by the remember client). | |
version | query | integer | no | the active version |
Response
200 with an object: every field of the list above plus sql,
parameter_schema, declared_result_schema, declared_interpretation,
query_space_major (memory_v1), default_limits (any of max_rows,
statement_timeout_ms, max_bytes), validation_report, author_principal
and approver_principal.
The shipped examples have an empty parameter_schema. Their parameters are
the positional placeholders in their SQL, listed in
Query space memory_v1.
Errors
| Status | detail code | Cause |
|---|---|---|
404 | saved_query_not_found | No such query or version. |
422 | validation list | version is not an integer. |
Example
curl -s "$REMEMBER_API_URL/query/saved/examples/claims_about" \
-H "Authorization: Bearer $REMEMBER_API_KEY"detail = memory.describe_saved_query(namespace="examples", name="claims_about")
print(detail["sql"])POST /query/saved/{namespace}/{name}/run
Run the active version of a saved query, or a named version, through the same
validation and limits as POST /query/sql.
Parameters
| Name | In | Type | Required | Constraints |
|---|---|---|---|---|
namespace | path | string | yes | ^[a-z][a-z0-9_]*$ (enforced by the remember client). |
name | path | string | yes | ^[a-z][a-z0-9_]*$ (enforced by the remember client). |
Request body
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
version | integer | no | the active version | At least 1. |
parameters | array | no | [] | Positional values for the query's placeholders. |
max_rows | integer | no | the query's stored default, else 200 | At least 0; clamped to 1,000. |
Unknown fields are rejected. The query's stored default_limits apply
(clamped to the interactive caps); your max_rows takes precedence over the
stored one. Only a version whose status is exactly active runs.
Response
200 with a QueryResult/v1 whose saved_query field is
{"query_id", "namespace", "name", "version", "query_hash"}, all strings.
Statement problems are reported inside the result, as for POST /query/sql.
Errors
| Status | detail code | Cause |
|---|---|---|
404 | saved_query_not_found | No such query or version. |
409 | saved_query_disabled | The query is disabled, or the version is not active (a draft, deprecated or broken version). |
409 | saved_query_revalidation_pending | The query space changed since the version was validated, and it has not been revalidated. |
422 | validation list | Malformed body, version below 1, max_rows negative, or an unknown field. |
500 | execution_error | The deployment could not read its saved-query registry state. |
Example
curl -s -X POST "$REMEMBER_API_URL/query/saved/examples/claims_about/run" \
-H "Authorization: Bearer $REMEMBER_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"parameters\": [\"$DANA_ID\"], \"max_rows\": 20}"result = memory.run_saved_query(
namespace="examples", name="claims_about", parameters=[str(dana_id)], max_rows=20
)Saved queries can only be created, activated or disabled through the deployment's own tooling; there is no HTTP route for it. See Saved queries.