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

# Context graph

The **context graph** holds a project's entities and how they relate: the nodes and relationships
describing its world. Of the project's four stores it is the only one that is a graph, and the only
one you query by traversing relationships. If you have not yet chosen between the four, start at
[where your data goes](/docs/concepts/where-your-data-goes).

## Nodes and relationships

A **node** is a thing — a customer, a deployment, a ticket, a person, a file. It carries a label
naming its type, a merge key, and properties.

A **relationship** is a named, directed connection from one node to another. It has a type, a start
node, an end node, and properties of its own. Relationship properties are stored and read back;
they are not decoration.

The content you save into and query out of the context graph is called **context**. What *kinds* of
node and relationship may exist is decided by the
[project ontology](/docs/concepts/project-ontology), layered over the
[platform schema](/docs/concepts/platform-schema) that every project shares.

### The 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 rather than creating two. That is what makes
repeated saves safe: something that re-reports the same entity every hour converges on one node
instead of accumulating duplicates. The merge key must be present in the properties you send, and a
node without one is refused rather than guessed at.

## What saving context does

[`save-context`](/docs/tools/save-context) takes a list of nodes and a list of relationships and
merges them into the project's context graph. Four things happen that are worth knowing before you
call it.

**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/scope-and-server-bound-arguments).

**The vocabulary extends itself.** New labels and relationship types you use are registered as you
save, so there is no separate step to declare a type before writing data of that type. That is the
Simple-mode behaviour. In Advanced mode `save-context` is not available at all: the call is refused
with a message directing you to [`create-ontology`](/docs/tools/create-ontology) and its governed
flow. Which mode applies is a property of the project — see
[project mode](/docs/concepts/project-mode).

**Property values are flattened.** Scalars store as they are. Arrays and nested objects are stored
JSON-encoded, so they read back as a string rather than as a list or an object. A property whose
value is null is not stored at all. If you need something to come back as structured data, model it
as nodes and relationships rather than as a nested value.

**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 is refused
outright; that refusal is the subject of the [platform schema](/docs/concepts/platform-schema) page,
and it is the single most common surprise on this path.

## What querying context does

[`query-context`](/docs/tools/query-context) runs a read-only Cypher query against the project's
context graph and returns rows. It is the traversal surface: patterns of the form
`(a)-[:RELATES_TO]->(b)` are what it exists for.

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 rather than from anything you send. A caller cannot
widen a query past what they were granted by editing the parameter, because the parameter is not
theirs to set.

**It is bounded.** Returned rows are clamped to a maximum, and so is the query timeout. You can ask
for the query plan instead of the rows when you want to know why something is slow before you run
it.

Two things it is deliberately not for. It is not a text search over code — use
[`search-code`](/docs/tools/search-code) and then [`fetch-code`](/docs/tools/fetch-code), which read
the code graph properly. And for other large string properties, a full-text index call retrieves
better than a `CONTAINS` scan does.

## Why traversal is the point

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. That is the whole reason the context graph is a graph: the questions
worth asking about an organization are usually questions about connections.

It is also the cleanest test for whether you want this store at all. If your question does not
depend on how things are connected, another store will answer it faster and more precisely.

## What it is not

It is not the [knowledge base](/docs/concepts/knowledge-base). The knowledge base holds prose and
finds it by meaning; the context graph holds structure and finds it by traversal. Text that happens
to sit 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/concepts/data-catalog) either. The catalog describes data that
lives outside Wexa; the context graph holds content of its own.

## How it is filled

Three paths, all landing in the same store, and all passing the same policy decisions and writing
the same audit record:

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