MCP · Tool
query
Read CRM data through OQL — filter, sort, select fields, and aggregate across contacts, deals, actions, notes, calls, and meetings.
Maintained by the OnePageCRM engineering team · Last updated Aug 27, 2026
query is the primary read tool. It accepts a JSON query object
that the OnePageCRM Query Language (OQL) understands
and runs it. Everything OQL can
express — filters, ordering, field projection, aggregates, date
helpers — is available here.
Signature
{
"type": "object",
"properties": {
"query": {
"type": "object",
"properties": {
"select": { "description": "Field array, [\"*\"], or aggregates." },
"from": { "type": "string", "description": "Entity name." },
"where": { "type": "object", "description": "Filter conditions (AND'd)." },
"order_by": { "type": "array", "description": "Sort order." },
"group_by": { "type": "array", "description": "Group fields (max 3)." },
"having": { "type": "object", "description": "Post-aggregation filter (requires group_by)." },
"distinct": { "type": "boolean", "description": "Unique combinations of the selected fields." },
"limit": { "type": "integer", "description": "Defaults to 100. Maximum 1000 — higher values are rejected, not clamped." },
"after_id": { "type": "string", "description": "Cursor from a previous response, to fetch the next page." }
}
}
},
"required": ["query"]
}
The query argument is a JSON OQL query — the same
shape shown throughout the OQL docs.
Entities you can query
contacts, companies, deals, actions, notes, calls,
meetings. See OQL Entities for the fields
each one exposes.
Examples
Today’s open actions:
{
"query": {
"from": "actions",
"select": ["*"],
"where": { "date": "TODAY()", "completed": false },
"limit": 50
}
}
Won deals this quarter, by owner:
{
"query": {
"from": "deals",
"select": ["count()", { "sum": ["amount"] }],
"where": { "status": "won", "close_date": "THIS_QUARTER()" },
"group_by": ["owner_id"],
"order_by": [{ "sum_amount": "desc" }]
}
}
A specific contact by email:
{
"query": {
"from": "contacts",
"select": ["id", "first_name", "last_name", "emails"],
"where": { "emails.address": "jane.doe@acmesolar.example" }
}
}
For more patterns, see OQL Recipes.
Response shape
{
"rows": [ /* one object per result row */ ],
"row_count": 17,
"total_count": 42
}
row_count is how many rows came back; total_count is how many
matched the query. They differ when a limit or the response cap
clipped the result, and a "truncated": true flag is added in that
case — it is omitted otherwise.
Paging through a large result
Sort by id and the response carries an after_id cursor. Send it
back on the same query to get the next page:
// first page
{ "from": "contacts", "select": ["id", "last_name"], "order_by": ["id"], "limit": 100 }
// → { "rows": [...], "row_count": 100, "total_count": 4210,
// "truncated": true, "after_id": "6a7da40120c72b5cc5402b54" }
// next page — same query, plus the cursor
{ "from": "contacts", "select": ["id", "last_name"], "order_by": ["id"], "limit": 100,
"after_id": "6a7da40120c72b5cc5402b54" }
An id-sorted response with no after_id means you have reached the
end.
Only an ascending id sort is resumable. Any other order_by
returns truncated: true with no after_id — so the rows beyond your
limit cannot be reached at all. When you need the whole set, sort by
id; when you need a top-N, pair limit with the sort you want and
accept that it is a top-N.
Operators and functions
OQL supports =, !=, <, >, <=, >=, in, between, like,
{"is": null}, and {"is not": null}. Date helpers (TODAY(),
THIS_QUARTER(), LAST_QUARTER(), DAYS_AGO(n), …) and the aggregate
functions count(), sum, avg, min, max, median, and
percentile are available, each with an optional distinct modifier.
Grouped results can be filtered after aggregation with having, and
distinct: true returns unique combinations of the selected fields.
The authoritative references are:
Errors
- Schema errors — unknown entity or unknown field on the entity.
- Syntax errors — malformed
whereclauses, unsupported operators. - Execution errors — limit exceeded, type mismatch, etc.
Each error comes back as a structured response (not an exception) so the agent can correct itself and retry on the next turn.
Scope
crm.readonly is sufficient. crm also works (write scope includes
read).
Results are always filtered to records the authenticated user can
see — query doesn’t bypass per-user permissions, even when the
account has multiple users.