> Source: https://wexa.ai/docs/context/query-context

# query-context

Run a read-only Cypher query against the project's context graph. Needs the `fabric:query.read` grant.

**Query the graph**

**Wexa MCP server**

```bash
curl -sS -X POST https://fabric.wexa.ai/mcp/$FABRIC_PROJECT_ID \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -H "content-type: application/json" -H "accept: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query-context","arguments":{"query":"MATCH (n) WHERE n.project_id = $project_id RETURN n LIMIT 1"}}}'
```

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000167\",\"result\":{\"columns\":[\"n\"],\"row_count\":1,\"rows\":\"\\u2026\",\"stats\":{\"elapsed_ms\":240},\"truncated\":false}}"
    }
  ],
  "isError": false
}
```

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/query-context \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"query":"MATCH (n) WHERE n.project_id = $project_id RETURN n LIMIT 1"}'
```

```json
{
  "lifecycle_id": "qlc_000167",
  "result": {
    "columns": [
      "n"
    ],
    "row_count": 1,
    "rows": "\u2026",
    "stats": {
      "elapsed_ms": 240
    },
    "truncated": false
  }
}
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

const fabric = new Fabric()
const result = await fabric.queryContext({
  query: 'MATCH (n) WHERE n.project_id = $project_id RETURN n LIMIT 1'
})
```

```ts
// result is the payload itself, with no `.result` to read
{
  columns: [ 'n' ],
  row_count: 1,
  rows: '\u2026',
  stats: {
    elapsed_ms: 240
  },
  truncated: false
}
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.query_context(
    query="MATCH (n) WHERE n.project_id = $project_id RETURN n LIMIT 1",
)
```

```python
# out is the result, already unwrapped
{
    "columns": ["n"],
    "row_count": 1,
    "rows": "\u2026",
    "stats": {
        "elapsed_ms": 240
    },
    "truncated": False
}
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | Required | Read-only Cypher; must filter on `project_id = $project_id`. Write clauses are rejected. |
| `parameters` | object | Optional | Values the query refers to as `$name`; see [passing values](#passing-values-parameters). Never `project_id` or `project_ids`. |
| `limit` | integer | Optional | Sets the `LIMIT` added when the query has none and caps any `LIMIT` it has. 1 or more; above 1000 is reduced to 1000. Without a `LIMIT` the server adds `LIMIT 100`. |
| `timeout_ms` | integer | Optional | Query timeout in ms. A call has about 25 seconds in total, so values above about 25000 have no effect. |
| `explain` | boolean | Optional | Return the query plan instead of rows. |
| `explain_denials` | boolean | Optional | When your access profile hid rows or masked fields, also return a `governance` block naming them and the clearance each needed (never values). |

**Errors**

- `UpstreamError` (504): the call ran past about 25 seconds. See [rows and time](#rows-and-time).

**Show full response**

A real response, captured from a live call to `query-context`:

`rows` came back populated and is shown as `…` here, because the real value runs to
thousands of characters. Read it from your own call rather than from this page.

**Result of query-context**

**REST API**

```json
{
  "lifecycle_id": "qlc_000167",
  "result": {
    "columns": [
      "n"
    ],
    "row_count": 1,
    "rows": "\u2026",
    "stats": {
      "elapsed_ms": 240
    },
    "truncated": false
  }
}
```

**Wexa MCP server**

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000167\",\"result\":{\"columns\":[\"n\"],\"row_count\":1,\"rows\":\"\\u2026\",\"stats\":{\"elapsed_ms\":240},\"truncated\":false}}"
    }
  ],
  "isError": false
}
```

**TypeScript SDK**

```ts
// the object you get back IS the result — there is no `.result` to read
{
  columns: [ 'n' ],
  row_count: 1,
  rows: '…',
  stats: {
    elapsed_ms: 240
  },
  truncated: false
}

// The TS SDK names these camelCase — lifecycleId, traceId — where Python uses
// lifecycle_id and trace_id. Both are non-enumerable: readable, but omitted by
// Object.keys() and JSON.stringify(). `result.lifecycleId` here is undefined.
result.lifecycleId // 'qlc_000167'
```

**Python SDK**

```python
# a dict subclass; `result` is already unwrapped
{
 "columns": [
  "n"
 ],
 "row_count": 1,
 "rows": "\u2026",
 "stats": {
  "elapsed_ms": 240
 },
 "truncated": False
}

out.lifecycle_id  # 'qlc_000167' — an attribute, not a key
```

**More details**

Full description from the tool, as the gateway advertises it:

Run a read-only Cypher query against this project's Wexa context graph. Queries must filter on project_id = $project_id (bound server-side). Write clauses are rejected. For CODE TEXT search (symbols, strings, file contents), use search-code then fetch-code — do NOT invent Cypher for that. query-context is for structural graph queries (callers, imports, ontology). For other large string properties (diffs, notes), prefer `CALL db.index.fulltext.queryNodes('idx_codetext_fulltext', '\<terms\>') YIELD node, score` over `CONTAINS`/regex.

### Which projects a query reads

| Filter | Reads |
| --- | --- |
| `n.project_id = $project_id` | This project, plus the projects that org-shared connectors and data sources shared with this project write into |
| `n.project_id IN $project_ids` | Every project you can access |

The server binds both from your credential. On servers built before 10 October 2026, `= $project_id` also
matched every other project you can access; to be sure of one project there, check `project_id` in the rows
you get back. The map form `(n {project_id: $project_id})` reads only this project. Do not put `project_id`
or `project_ids` in `parameters`: a `project_id` other than your own is refused.

### Passing values: `parameters`

```python
rows = fabric.query_context(
    query="""
        MATCH (t:Ticket)
        WHERE t.project_id = $project_id
          AND t.status IN $statuses
          AND t.due >= $from AND t.due < $to
        RETURN t.id AS id, t.due AS due
        ORDER BY due
        LIMIT 200
    """,
    parameters={"statuses": ["open", "blocked"], "from": "2026-10-15", "to": "2026-10-29"},
)
```

Refer to each value as `$name` instead of pasting it into the query text. Range comparisons (`>=`, `<`, …)
work on any property. Values are stored exactly as save-context received them: a number stays a number, and a
date sent as text stays text. Text compares character by character, so a range filter on a date stored as
text only works when the date is ISO 8601 (`2026-10-15`); `10/15/2026` sorts wrongly across months and years.

### Rows and time

- **Rows:** without a `LIMIT` the server adds `LIMIT 100`, and a `LIMIT` above 1000 is reduced to 1000.
  The `limit` argument sets the `LIMIT` added when the query has none and caps any `LIMIT` it has (1 or
  more; above 1000 it is reduced to 1000).
- **Time:** a call has about 25 seconds in total; past that it answers `504` (`UpstreamError` in both SDKs).
  `timeout_ms` can only shorten that, so values above about 25000 have no effect.

The captured call returned one row, so the example shows `rows` as `"\u2026"`, the value the capture holds in place of the row itself.

Marked consequential: it is audited in full, and a policy rule may hold it for a person to approve. See [Policy and approvals](/docs/governance/policy-and-approvals).

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
