Skip to content

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 where clauses, 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.