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

# save-context

[Can be held for approval](/docs/sdks-and-apis/how-every-tool-call-works#lifecycle-and-governance)

Save nodes and relationships into the project's context graph (Simple mode). Needs the `fabric:ontology.write` grant.

**Save a node**

**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":"save-context","arguments":{"nodes":[{"label":"FixtureCustomer","key":"id","properties":{"id":"c-1001","name":"Acme"}}]}}}'
```

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000168\",\"result\":{\"nodes_merged\":1,\"relationships_merged\":0,\"ontology_extended\":true,\"new_labels\":[\"FixtureCustomer\"],\"success\":true}}"
    }
  ],
  "isError": false
}
```

**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":"FixtureCustomer","key":"id","properties":{"id":"c-1001","name":"Acme"}}]}'
```

```json
{
  "lifecycle_id": "qlc_000168",
  "result": {
    "nodes_merged": 1,
    "relationships_merged": 0,
    "ontology_extended": true,
    "new_labels": [
      "FixtureCustomer"
    ],
    "success": true
  }
}
```

**TypeScript SDK**

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

const fabric = new Fabric()
const result = await fabric.saveContext({
  nodes: [
    {
      label: 'FixtureCustomer',
      key: 'id',
      properties: {
        id: 'c-1001',
        name: 'Acme'
      }
    }
  ]
})
```

```ts
// result is the payload itself, with no `.result` to read
{
  nodes_merged: 1,
  relationships_merged: 0,
  ontology_extended: true,
  new_labels: [ 'FixtureCustomer' ],
  success: true
}
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.save_context(
    nodes=[
        {
            "label": "FixtureCustomer",
            "key": "id",
            "properties": {
                "id": "c-1001",
                "name": "Acme"
            }
        }
    ],
)
```

```python
# out is the result, already unwrapped
{
    "nodes_merged": 1,
    "relationships_merged": 0,
    "ontology_extended": True,
    "new_labels": ["FixtureCustomer"],
    "success": True
}
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `nodes` | array | Optional | Provide at least one node or relationship across `nodes` and `relationships`. Each node is `{label, key, properties}`; `key` names the merge-key property, which must be in `properties`, starts with a letter and holds only letters, digits and underscores. Property names may not start with `_`. At most 10,000 nodes and relationships per call, counted together; more is refused (413) and nothing is written, so split the batch. A bad or platform-managed label is refused. |
| `relationships` | array | Optional | Each is `{type, from, to, properties}`, where `from` and `to` are `{label, key, value}`. Endpoints are looked up, never created: send their nodes in this call or an earlier one. |

**Errors**

- `ValidationError` (422): the project is an Advanced/manual project. Nothing was written.
- `UpstreamError` (502): nodes were sent and none was written; the message lists why for each item. A partial write is not an error: see [partial writes](#partial-writes-check-success).
- `UpstreamError` (504): the call ran past about 25 seconds. None, part or all of the batch may have been written; re-sending is safe.

**Show full response**

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

**Result of save-context**

**REST API**

```json
{
  "lifecycle_id": "qlc_000168",
  "result": {
    "nodes_merged": 1,
    "relationships_merged": 0,
    "ontology_extended": true,
    "new_labels": [
      "FixtureCustomer"
    ],
    "success": true
  }
}
```

**Wexa MCP server**

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000168\",\"result\":{\"nodes_merged\":1,\"relationships_merged\":0,\"ontology_extended\":true,\"new_labels\":[\"FixtureCustomer\"],\"success\":true}}"
    }
  ],
  "isError": false
}
```

**TypeScript SDK**

```ts
// the object you get back IS the result — there is no `.result` to read
{
  nodes_merged: 1,
  relationships_merged: 0,
  ontology_extended: true,
  new_labels: [ 'FixtureCustomer' ],
  success: true
}

// 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_000168'
```

**Python SDK**

```python
# a dict subclass; `result` is already unwrapped
{
 "nodes_merged": 1,
 "relationships_merged": 0,
 "ontology_extended": True,
 "new_labels": [
  "FixtureCustomer"
 ],
 "success": True
}

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

- `success`: `true` only when every node and relationship sent was written
- `nodes_merged`: Nodes written (created or updated)
- `relationships_merged`: Relationships written (created or updated)
- `errors`: One message per item left out, naming it as `node[i]` or `relationship[i]`; absent when there are none
- `ontology_extended`: A label or relationship type new to this project was added
- `new_labels`, `new_rel_types`: Which ones

**More details**

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

Save nodes and relationships into this project's context graph (Simple/auto mode). The ontology self-organizes: new labels/relationship types are registered automatically. Nodes: [\{label,key,properties\}]; relationships: [\{type,from:\{label,key,value\},to:\{label,key,value\},properties\}]. Relationship `properties` ARE persisted. Scalars store as-is; arrays AND nested objects are stored JSON-encoded (so they read back as a string, not a list); a null value is not stored at all. Property names may not start with `_` (reserved for graph metadata). Round-trip: a node returned by query-context can be passed straight back here. The server owns `project_id`, `row_pk`, `updated_at` and `wx_source` and rewrites all four on every write, so leaving them in the payload is harmless — `wx_source` is dropped rather than stored, and setting it to anything other than "save-context" is refused because delete-context trusts it.

### Partial writes: check `success`

A call that sends nodes and writes **none** of them fails, and both SDKs raise. A call that writes
**some** items succeeds — and so does a batch of **only relationships**, even when none was written:
it answers 200 with `success: false`, the counts of what landed, and one `errors` entry per item left
out. The SDKs return that result rather than raising, so the counts are not lost — your code has to
check `success`.

```python
out = fabric.save_context(nodes=nodes, relationships=relationships)
if not out["success"]:
    print(out["nodes_merged"], out["relationships_merged"], out.get("errors"))
```

What ends up in `errors` instead of failing the call:

- **A relationship whose `from` or `to` node does not exist** — `relationship[3] (ISSUED_TO): endpoint
  node(s) not found`. Endpoints are looked up, never created: send the nodes in the same call or an
  earlier one.
- **A batch write that failed on the server** — `not confirmed written … re-sending the save is safe`.
- **A node whose access scope cannot be worked out**, when the project has scope tagging on.

A merge key — a `key`, `from.key` or `to.key` — must start with a letter followed only by letters, digits
and underscores (`order_id`, not `order-id` or `order id`). Any other key refuses the whole call with `422`
before anything is written, naming the item, e.g. `node[1] (Order): invalid merge key "my-key"`.

### Limits and re-sending

- **Size:** at most 10,000 nodes + relationships per call, counted together. More is refused with
  `413` before the approval gate. A few thousand items per call is a comfortable size.
- **Time:** about 25 seconds per call. A call that runs out answers `504` (`UpstreamError` in both
  SDKs). It may have written none, part or all of the batch: writing stops at the timeout, but a batch
  already running on the server can still land.
- **Re-sending:** every write is an idempotent MERGE on the merge key, so sending the same batch
  again — after a timeout, or to retry the items listed in `errors` — never creates duplicates. If you
  sent an `idempotency_key` (REST or the Python SDK), re-send under a new key or none: a key whose call
  timed out answers `409` "outcome unknown" and is never run again. A re-send can time out again: the
  server checks every relationship type that is new to the project before writing anything, and that
  check is slow on a large graph. If it does, split the batch into smaller calls with fewer new
  relationship types in each.

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).
