> Source: https://wexa.ai/docs/context/create-ontology

# create-ontology

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

Define node types, relationships and properties in the project's ontology. Needs the `fabric:ontology.write` grant.

**Declare a node type**

**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":"create-ontology","arguments":{"domain":"","nodes":[{"name":"FixtureCustomer","label":"FixtureCustomer","category":"person","properties":[{"name":"id","type":"string"},{"name":"name","type":"string"}]}]}}}'
```

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000169\",\"result\":{\"live_schema\":{\"nodes\":[{\"name\":\"FixtureCustomer\",\"label\":\"FixtureCustomer\",\"category\":\"person\",\"properties\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"}]}],\"relationships\":[]},\"mode\":\"auto\",\"self_organized\":true,\"status\":\"committed\"}}"
    }
  ],
  "isError": false
}
```

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/create-ontology \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"domain":"","nodes":[{"name":"FixtureCustomer","label":"FixtureCustomer","category":"person","properties":[{"name":"id","type":"string"},{"name":"name","type":"string"}]}]}'
```

```json
{
  "lifecycle_id": "qlc_000169",
  "result": {
    "live_schema": {
      "nodes": [
        {
          "name": "FixtureCustomer",
          "label": "FixtureCustomer",
          "category": "person",
          "properties": [
            {
              "name": "id",
              "type": "string"
            },
            {
              "name": "name",
              "type": "string"
            }
          ]
        }
      ],
      "relationships": []
    },
    "mode": "auto",
    "self_organized": true,
    "status": "committed"
  }
}
```

**TypeScript SDK**

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

const fabric = new Fabric()
const result = await fabric.createOntology({
  domain: '',
  nodes: [
    {
      name: 'FixtureCustomer',
      label: 'FixtureCustomer',
      category: 'person',
      properties: [
        {
          name: 'id',
          type: 'string'
        },
        {
          name: 'name',
          type: 'string'
        }
      ]
    }
  ]
})
```

```ts
// result is the payload itself, with no `.result` to read
{
  live_schema: {
    nodes: [
      {
        name: 'FixtureCustomer',
        label: 'FixtureCustomer',
        category: 'person',
        properties: [
          {
            name: 'id',
            type: 'string'
          },
          {
            name: 'name',
            type: 'string'
          }
        ]
      }
    ],
    relationships: []
  },
  mode: 'auto',
  self_organized: true,
  status: 'committed'
}
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.create_ontology(
    domain="",
    nodes=[
        {
            "name": "FixtureCustomer",
            "label": "FixtureCustomer",
            "category": "person",
            "properties": [
                {
                    "name": "id",
                    "type": "string"
                },
                {
                    "name": "name",
                    "type": "string"
                }
            ]
        }
    ],
)
```

```python
# out is the result, already unwrapped
{
    "live_schema": {
        "nodes": [
            {
                "name": "FixtureCustomer",
                "label": "FixtureCustomer",
                "category": "person",
                "properties": [
                    {
                        "name": "id",
                        "type": "string"
                    },
                    {
                        "name": "name",
                        "type": "string"
                    }
                ]
            }
        ],
        "relationships": []
    },
    "mode": "auto",
    "self_organized": True,
    "status": "committed"
}
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `domain` | string | Required | The ontology domain. Non-empty. |
| `mode` | string | Optional | Advanced mode only: `proposal` stages, `commit` applies (may need approval). Ignored in Simple mode. A proposal does not consume the edit budget; each commit does (default 50 commits per project, a deployment setting). |
| `nodes` | array | Optional | At least one node or relationship is required across `nodes` and `relationships`. Node types: `{name, label, category, properties[], primaryKey, ...}`. `category` is `person`, `interaction`, `context`, `other` or `data` (reserved for platform catalog types). Labels are a letter then letters, digits or underscores; a declared `primaryKey` must be a declared property; property names are unique and non-empty. A catalog type name is refused outside `data`. |
| `relationships` | array | Optional | Relationships: `{name, label, fromNode, toNode, cardinality}`. A relationship `label` follows the same label rule. Both ends must name a type declared in this call or already live. `cardinality` is `1:1`, `1:N`, `N:1` or `N:M`. |
| `resume_token` | string | Optional | Resume token from an approved request (Advanced mode). |

**Errors**

- `ValidationError` (422): a rule above is broken; the message names the index and label.
- `ValidationError`, Advanced mode only: the label collides with a committed one (`label collision with committed ontology`), or the project's edit budget is spent (`ontology edit budget exhausted for project`). Nothing was written.

**Show full response**

A real response, captured from a live call to `create-ontology`:

**Result of create-ontology**

**REST API**

```json
{
  "lifecycle_id": "qlc_000169",
  "result": {
    "live_schema": {
      "version": "",
      "domain": "",
      "nodes": [
        {
          "name": "FixtureCustomer",
          "label": "FixtureCustomer",
          "category": "person",
          "properties": [
            {
              "name": "id",
              "type": "string"
            },
            {
              "name": "name",
              "type": "string"
            }
          ]
        },
        {
          "name": "ProbeCustomer",
          "label": "ProbeCustomer",
          "category": "docsprobedomain",
          "properties": [
            {
              "name": "id",
              "type": "string"
            },
            {
              "name": "name",
              "type": "string"
            }
          ]
        }
      ],
      "relationships": []
    },
    "mode": "auto",
    "self_organized": true,
    "status": "committed"
  }
}
```

**Wexa MCP server**

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000169\",\"result\":{\"live_schema\":{\"version\":\"\",\"domain\":\"\",\"nodes\":[{\"name\":\"FixtureCustomer\",\"label\":\"FixtureCustomer\",\"category\":\"person\",\"properties\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"}]},{\"name\":\"ProbeCustomer\",\"label\":\"ProbeCustomer\",\"category\":\"docsprobedomain\",\"properties\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"}]}],\"relationships\":[]},\"mode\":\"auto\",\"self_organized\":true,\"status\":\"committed\"}}"
    }
  ],
  "isError": false
}
```

**TypeScript SDK**

```ts
// the object you get back IS the result — there is no `.result` to read
{
  live_schema: {
    version: '',
    domain: '',
    nodes: [
      {
        name: 'FixtureCustomer',
        label: 'FixtureCustomer',
        category: 'person',
        properties: [
          {
            name: 'id',
            type: 'string'
          },
          {
            name: 'name',
            type: 'string'
          }
        ]
      },
      {
        name: 'ProbeCustomer',
        label: 'ProbeCustomer',
        category: 'docsprobedomain',
        properties: [
          {
            name: 'id',
            type: 'string'
          },
          {
            name: 'name',
            type: 'string'
          }
        ]
      }
    ],
    relationships: []
  },
  mode: 'auto',
  self_organized: true,
  status: 'committed'
}

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

**Python SDK**

```python
# a dict subclass; `result` is already unwrapped
{
 "live_schema": {
  "version": "",
  "domain": "",
  "nodes": [
   {
    "name": "FixtureCustomer",
    "label": "FixtureCustomer",
    "category": "person",
    "properties": [
     {
      "name": "id",
      "type": "string"
     },
     {
      "name": "name",
      "type": "string"
     }
    ]
   },
   {
    "name": "ProbeCustomer",
    "label": "ProbeCustomer",
    "category": "docsprobedomain",
    "properties": [
     {
      "name": "id",
      "type": "string"
     },
     {
      "name": "name",
      "type": "string"
     }
    ]
   }
  ],
  "relationships": []
 },
 "mode": "auto",
 "self_organized": True,
 "status": "committed"
}

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

**More details**

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

Define graph ontology (node types, relationships, properties) for this project. In Simple/auto mode the ontology self-organizes and commits immediately; in Advanced/manual mode use mode=proposal to stage and mode=commit to apply (commit may require approval).

### Schema and ontology

Three things describe a project's data: the **platform schema** every project shares, the **project ontology** you edit, and the **data catalog** of assets that live outside Wexa, and this page covers all three so you can tell which one a problem belongs to.

You need the `fabric:ontology.write` grant to change a project ontology. If you are still deciding whether what you have is a type at all, read [where your data goes](/docs/context/overview#where-your-data-goes) first.

### How the schema and ontology work

1. The **platform schema** is the fixed vocabulary of node and relationship types every project shares. It is defined in the platform's own source and cannot be created, edited or deleted at runtime, by anyone, through any surface or mode. It is always named in full, never called an ontology.
2. Your **project ontology** is layered over it: your own types, in your own words, for what the platform did not anticipate. It is the only one of a project's four stores that describes the *shape* of another store, saying what kinds of thing may exist in the [context graph](/docs/context/context-graph).
3. The **data catalog** reuses the platform schema's catalog types (`Table`, `Column`, `Dashboard` and so on) to describe data that lives outside Wexa. It is the only store describing that data, and it holds metadata about it rather than the data itself.
4. You change only the project ontology, with [`create-ontology`](/docs/context/create-ontology). Nothing edits the platform schema, so what goes wrong is narrower: you choose a **name** the platform already owns.
5. The catalog is filled by ingestion, and you read it.

The sections below follow that order: change your ontology, fix a name the platform owns, then read and fill the catalog.

### Change your project ontology

How a change happens depends on the project mode. See [project mode](/docs/administration/project-mode) for which one a project is in.

- **Simple mode.** The vocabulary organizes itself. Saving a node with a never-used label registers it.
  - `create-ontology` ensures the types you declare immediately, with no staging, and re-declaring a label extends it.
  - The `mode` argument is ignored and there is no budget.
  - The result reports what the ontology holds afterwards: `status: "committed"` means the labels are present.
- **Advanced mode.** A change is governed and has two steps.
  - `mode: "proposal"` validates and stages the types and returns a proposal identifier and the staged shape. Nothing is live yet.
  - `mode: "commit"` validates again, checks against the live vocabulary and merges. It changes the meaning of everything stored, so policy may hold it for an approval; see [policy and approvals](/docs/governance/policy-and-approvals).
  - `save-context` is unavailable; direct data writes are refused.

Propose a change in Advanced mode:

**Propose an ontology change**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/ontology \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"domain":"sales","mode":"proposal","nodes":[{"name":"PurchaseOrder","label":"PurchaseOrder","category":"context","properties":[{"name":"id","type":"string"}],"primaryKey":"id"}]}'
```

**TypeScript SDK**

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

const fabric = new Fabric()
const result = await fabric.createOntology({
  domain: 'sales',
  mode: 'proposal',
  nodes: [
    {
      name: 'PurchaseOrder',
      label: 'PurchaseOrder',
      category: 'context',
      properties: [{ name: 'id', type: 'string' }],
      primaryKey: 'id',
    },
  ],
})
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.create_ontology(
    domain="sales",
    mode="proposal",
    nodes=[{
        "name": "PurchaseOrder",
        "label": "PurchaseOrder",
        "category": "context",
        "properties": [{"name": "id", "type": "string"}],
        "primaryKey": "id",
    }],
)
```

Send the same call with `mode: "commit"` to apply it. Proposals do not consume the edit budget; only commits do. Batch a coherent change into one commit, since a commit costs the same whatever it declares.

The same validation runs in both modes, and every refusal names the index and label of the type that caused it:

- A label starts with a letter and continues with letters, digits or underscores. The convention is PascalCase, such as `PurchaseOrder`.
- Every node type has a category: `person`, `interaction`, `context`, `other` or `data`. `data` is reserved for the platform's catalog types, so do not use it for your own entities.
- A declared primary key must be a declared property.
- Property names within a type are unique, and an empty name is refused.
- Both ends of a relationship must name a type declared in the same call or already live. A relationship to a type that does not exist yet is refused.
- Cardinality is `1:1`, `1:N`, `N:1` or `N:M`. The verbose spellings such as `ONE_TO_MANY` are accepted and normalized.

### Fix a refused or missing name

The platform schema holds two families of type:

- The **canonical vocabulary** models the world: people, systems, processes, context, time, provenance and their relationships. Its lower layers are base constructs protected against redefinition, and the upper layer is left empty for your project ontology.
- The **data-estate catalog types** are `DataService`, `Database`, `Table`, `Column`, `Dashboard`, `Pipeline`, `MLModel`, `GlossaryTerm`, `Classification` and `DataOwner`. Lineage and quality are computed against them, so redefining `Table` would break the catalog for that project without anything reporting it.

The catalog types are the names most likely to collide with one you would pick yourself.

Which call you made decides what you see:

**`create-ontology` with a catalog type.** Declaring a node whose label is a catalog type, under any category except `data`, is refused in both modes before anything is written:

```text
node[0] label "Table" is reserved for the platform data-estate catalog
```

**`save-context` with a platform-managed label.** A different set of labels is managed for data writes, such as `Organization`, `Project`, `User`, `Connector` and `Skill`. The two sets share no members, so testing one tells you nothing about the other. A node with one of these labels, or a relationship with one at either end, is refused:

```text
node[0]: "User" is a platform-managed type and cannot be written through save-context —
use create-ontology to change the ontology, or choose a different label for your own data
```

That message does not say the platform schema can be changed. It says that if you wanted a type of your own, declare it in your project ontology under a name that is yours.

**A name the storage service owns.** Some names are owned further in, so a declaration clears validation and is silently not stored. In Simple mode `create-ontology` reads the ontology back, and if a label did not survive it returns **HTTP 200** with:

```json
{
  "status": "partially_committed",
  "missing_labels": ["YourLabel"],
  "note": "…"
}
```

The note says the labels were declared but not stored, usually because the platform owns the name. Rename them and retry. A label that breaks PascalCase can produce the same result, so check the spelling too.

A fourth status, `committed_unverified`, means the write was accepted but reading the vocabulary back failed. Treat it as probably fine and unconfirmed, and check again.

To fix any of the three, rename yours with a domain prefix, such as `AcmeTable` or `ContractTable`. Relate it to the platform's types rather than replacing them: a relationship from your type to a `Table` keeps the catalog's meaning. If you wanted to describe a real warehouse table, you wanted the data catalog below, not a type.

The platform schema is fixed for three reasons:

- A runtime edit in one project either leaks into the others, which breaks isolation, or it was never shared.
- Lineage traversal, quality scorecards, classification and the relationship allow-list are compiled against these types by name.
- Your project ontology is already the place for a type of your own. The platform schema is the floor, not the ceiling.

### Read the data catalog

The catalog is the only store describing data that lives outside Wexa, and it is queryable beside your context rather than in a separate tool. A **catalog asset** is one entry. Assets nest the way your warehouse does:

- A `DataService` is a source such as a warehouse, database or stream; a `Database` is a logical database inside it.
- A `Table` is a relation, view or streaming topic; a `Column` is a field in a table.
- `Dashboard` is a report or chart, `Pipeline` a job that moves or transforms data, and `MLModel` a trained model.
- `GlossaryTerm` is a business definition, `Classification` a tag such as PII, and `DataOwner` the person answerable for an asset.

An asset records what the thing is, where it lives and who is answerable for it, not the rows. To fetch rows, use [connector-read](/docs/connectors/overview).

The catalog is read over the REST API rather than through a tool. Calls search assets, read one asset, read its lineage, read the catalog's own type definitions, and read a project's scorecard report.

Assets also appear in the context graph as nodes, so [`query-context`](/docs/context/query-context) can traverse them, including to find which are live-readable, the first step of a connector read.

**Lineage** is the recorded path data took from one asset to another, at two grains: table to table and column to column. Column level is the grain that answers real questions, because "this report is wrong" is usually about one column.

Read forward, lineage answers what breaks if you change something; read backward, where it came from. A `Pipeline` or `MLModel` that writes a table or dashboard is recorded as feeding it, so a job is a step in the chain. Lineage describes movement outside Wexa, not relationships inside the context graph.

A **scorecard** is a quality judgement attached to an asset, computed from the catalog rather than entered by hand. The rules flag:

- columns classified as PII with no description, graded as a warning because an undocumented sensitive field cannot be reviewed;
- columns with no parent table, which usually means an incomplete sync;
- tables with no lineage recorded;
- tables with no description;
- tables with no recorded owner.

Each rule returns how many assets fail it and a sample, and the report carries an overall status.

### Fill the data catalog

Connect a source by [provisioning a connector](/docs/connectors/overview); [ingestion](/docs/context/ingestion) brings its metadata in, so it appears in the catalog and its entities appear in the context graph.

The contents are produced by OpenMetadata, which the platform runs to crawl your sources and compute lineage. Its output is translated into catalog assets and relationships and stored in Wexa under the same project isolation, policy decisions and audit record as everything else.

Synchronization is periodic with a configurable cadence, and a deployment can also be sent an update when a source changes. A call triggers a pass on demand and another reads the statistics from the last one.

Classifications flow onwards: a tag such as PII arriving from a source updates the restricted set the policy engine enforces, so classifying a column at the source tightens what agents may do here.

Source code is a separate path. A **repository connection** links a project to a code repository and [code sync](/docs/context/code-sync) keeps the code graph in step with it. A repository connection is not a connector and is never provisioned.

### Limits and behavior of the ontology

| Behavior | Detail |
|---|---|
| Edit budget | Each successful commit consumes one; a proposal does not. Exhausted: refused with `ontology edit budget exhausted for project`, before anything is written. |
| Budget unit | Commits, not types: one commit declaring twelve node types costs one. |
| Budget ceiling | A deployment setting. Default 50 commits per project; an operator can change it, and a negative value means unlimited. Check your deployment. |
| Why a budget | The ontology is the shape every node is checked against, so unlimited rewrites can leave historical data meaning something else. |
| Existing label, Advanced mode | Refused with `label collision with committed ontology`, naming the label. Re-declaring extends in Simple mode only. |
| Catalog type label | Refused in both modes; see above. |
| Catalog sync | Periodic; on-demand pass available. |

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