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

# Context graph

The context graph holds a project's entities and how they relate, as nodes and relationships. Reading needs the `fabric:query.read` grant and saving needs `fabric:ontology.write`. To explore it in the console:

1. Open **Context Graph**, then **Context Graph**.
2. Type a question or an entity name in the search box and press Enter. Select **Show / write Cypher** to write a query yourself.
3. Narrow the view with the type and connector filters, and select a node or relationship to read its properties and connections.
4. If you can curate context, use **Add entity** to add one, or the edit and delete buttons on a selected entity. An entity from a source shared into your project may be read-only to you, and the button's hint says why.

## Limits

| Behavior | Detail |
|---|---|
| Merge key | Required in the properties. A node without one is refused. |
| Project | Bound from your scope. Passing it is refused. |
| Property names | Cannot begin with an underscore. |
| Property values | Arrays and objects are stored JSON-encoded. A null is not stored. |
| Platform-managed labels | Refused outright. |
| Advanced mode | `save-context` is unavailable. Use `create-ontology`. |
| Query writes | Rejected. |
| Query rows and time | Clamped to a maximum. The [reference](/docs/context/query-context#rows-and-time) gives the numbers. |
| Grants | `fabric:ontology.write` to save, `fabric:query.read` to query. |

**More details**

The **context graph** holds a project's entities and how they relate, the nodes and relationships describing its world, which makes it the only one of the four stores that is a graph and the only one you query by traversing relationships.

Every role that sees the Context Graph group in the console sidebar can read the graph. Curating it needs a role that can curate context. Through the API, saving needs the `fabric:ontology.write` grant and querying needs `fabric:query.read`. If you have not yet chosen between the four stores, start at [where your data goes](/docs/context/overview#where-your-data-goes).

### Explore the graph in the console

An empty project shows "No entities yet" and asks you to connect a source. See [connectors](/docs/connectors/overview). **Lineage** in the same sidebar group traces where a piece of data came from.

### How the context graph works

1. **A node is a thing**, such as a customer, a deployment, a ticket, a person or a file. It carries a label naming its type, a merge key and properties.
2. **A relationship connects two nodes.** It is named and directed, with a type, a start node, an end node and properties of its own. Relationship properties are stored and read back.
3. **You save context.** [`save-context`](/docs/context/save-context) merges nodes and relationships into the project's graph.
4. **The project ontology says what may exist.** The [project ontology](/docs/context/create-ontology) decides which kinds of node and relationship are allowed, layered over the [platform schema](/docs/context/create-ontology) that every project shares.
5. **You query context.** [`query-context`](/docs/context/query-context) runs read-only Cypher and returns rows.

The content you save into and query out of the graph is called **context**.

### Choose a merge key

Every node you save names one of its own properties as the merge key. Saving the same label with the same merge-key value twice updates one node instead of creating two. That makes repeated saves safe. Something that re-reports the same entity every hour converges on one node.

The merge key must be present in the properties you send. A node without one is refused rather than guessed at.

### Save context

Four things happen when you save, and each is worth knowing before you call.

- **The project is filled in for you.** The project the write lands in is taken from your scope and stamped onto every node. You never pass it, and passing it is refused. See [scope and server-bound arguments](/docs/get-started/core-ideas#know-what-scope-a-call-carries).
- **The vocabulary extends itself.** New labels and relationship types are registered as you save, so there is no step to declare a type first. That is the Simple-mode behaviour.
- **Property values are flattened.** Scalars store as they are. Arrays and nested objects are stored JSON-encoded, so they read back as a string. A null is not stored at all.
- **Some names are not yours to use.** Property names may not begin with an underscore, which the graph reserves for its own metadata. A small set of labels is platform-managed and refused outright. That refusal is the most common surprise on this path and is explained on [schema and ontology](/docs/context/create-ontology).

If you need something to come back as structured data, model it as nodes and relationships rather than as a nested value.

**Save a customer and an order**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/save-context \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"nodes":[{"label":"Customer","key":"id","properties":{"id":"c-1001","name":"Acme"}},{"label":"Order","key":"id","properties":{"id":"o-5001","total":240}}],"relationships":[{"type":"PLACED","from":{"label":"Customer","key":"id","value":"c-1001"},"to":{"label":"Order","key":"id","value":"o-5001"},"properties":{}}]}'
```

**TypeScript SDK**

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

const fabric = new Fabric()
const result = await fabric.saveContext({
  nodes: [
    { label: 'Customer', key: 'id', properties: { id: 'c-1001', name: 'Acme' } },
    { label: 'Order', key: 'id', properties: { id: 'o-5001', total: 240 } },
  ],
  relationships: [
    {
      type: 'PLACED',
      from: { label: 'Customer', key: 'id', value: 'c-1001' },
      to: { label: 'Order', key: 'id', value: 'o-5001' },
      properties: {},
    },
  ],
})
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.save_context(
    nodes=[
        {"label": "Customer", "key": "id", "properties": {"id": "c-1001", "name": "Acme"}},
        {"label": "Order", "key": "id", "properties": {"id": "o-5001", "total": 240}},
    ],
    relationships=[
        {
            "type": "PLACED",
            "from": {"label": "Customer", "key": "id", "value": "c-1001"},
            "to": {"label": "Order", "key": "id", "value": "o-5001"},
            "properties": {},
        }
    ],
)
```

In Advanced mode `save-context` is not available at all. The call is refused with a message directing you to [`create-ontology`](/docs/context/create-ontology) and its governed flow. Which mode applies is a property of the project. See [project mode](/docs/administration/project-mode).

### Query context

[`query-context`](/docs/context/query-context) is the traversal surface. Patterns of the form `(a)-[:RELATES_TO]->(b)` are what it exists for.

**Find a customer's orders**

**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 (c:Customer)-[:PLACED]->(o:Order) WHERE c.project_id = $project_id RETURN c.name AS customer, o.id AS order LIMIT 25"}'
```

**TypeScript SDK**

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

const fabric = new Fabric()
const result = await fabric.queryContext({
  query:
    'MATCH (c:Customer)-[:PLACED]->(o:Order) WHERE c.project_id = $project_id RETURN c.name AS customer, o.id AS order LIMIT 25',
})
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
rows = fabric.query_context(
    query="MATCH (c:Customer)-[:PLACED]->(o:Order) "
    "WHERE c.project_id = $project_id "
    "RETURN c.name AS customer, o.id AS order LIMIT 25",
)
```

Three constraints shape how you write one:

- **It is read-only.** Write clauses are rejected. The ways into the graph are saving context, ingestion and code sync. A query is not one of them.
- **It is pinned to your project.** Your query filters on `project_id = $project_id`, and that parameter is bound server-side from your scope. A caller cannot widen a query by editing it, because the parameter is not theirs to set.
- **It is bounded.** Returned rows are clamped to a maximum, and so is the query timeout. Ask for the query plan instead of the rows to learn why something is slow before you run it.

It is deliberately not a text search over code. Use [`search-code`](/docs/context/search-code) and then [`fetch-code`](/docs/context/fetch-code), which read the code graph properly. For other large string properties, a full-text index call retrieves better than a `CONTAINS` scan.

### Decide whether you want this store

A store that matches records one at a time can tell you that a thing exists. A graph can tell you what a thing is connected to, and what those are connected to, without you knowing in advance how many steps away the answer is. The questions worth asking about an organization are usually questions about connections.

That is also the cleanest test. If your question does not depend on how things are connected, another store will answer it faster and more precisely.

It is not the [knowledge base](/docs/context/knowledge-base). Text that sits on a node is a property of that node, not a document, and nothing searches it by similarity. It is not the [data catalog](/docs/context/create-ontology) either. The catalog describes data that lives outside Wexa, and the graph holds content of its own.

### How the graph is filled

Three paths land in the same store. All pass the same policy decisions and write the same audit record.

- Directly, by saving context through any of the four surfaces.
- Through [ingestion](/docs/context/ingestion), which brings a connected source's data in.
- Through [code sync](/docs/context/code-sync), which maintains the code graph for a connected repository.
