---
title: Schema CRUD Operations
nextjs:
  metadata:
    title: Schema CRUD Operations — TerminusDB
    description: Create, read, update, and delete schema classes and properties in TerminusDB using the TypeScript client, Python client, or HTTP API.
    keywords: terminusdb, schema, crud, create schema, read schema, update schema, delete schema, graph_type, schema operations, typescript client, python client, http api
    alternates:
      canonical: https://terminusdb.com/docs/schema-crud-operations/
    openGraph:
      images: https://assets.terminusdb.com/docs/technical-documentation-terminuscms-og.png
tags:
  - typescript
  - python
  - schema
  - how-to
  - intermediate
---

{% callout type="note" %}
**Prerequisites**
- TerminusDB running locally ([install guide](/docs/install-terminusdb-as-a-docker-container/))
- A database created ([create database guide](/docs/create-a-database/))
- Familiarity with schema concepts ([schema reference](/docs/schema-reference-guide/))
{% /callout %}

{% callout type="note" %}
**What you'll achieve**
By the end of this guide, you will be able to create, inspect, modify, and delete schema classes in TerminusDB using the HTTP API, TypeScript client, or Python client.
{% /callout %}

Schema defines the structure of your documents — their types, properties, keys, and relationships. All schema operations use the **Document API** with `graph_type=schema`. This guide covers the full lifecycle: creating classes, inspecting existing schema, updating fields, and removing types.

---

{% prerequisites-clone /%}

## Create schema classes

Add new document types to your database. Each class is a JSON object with `@type: "Class"`.

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const createSchema = async () => {
  const schema = [
    {
      "@type": "Class",
      "@id": "Country",
      "@key": { "@type": "Lexical", "@fields": ["name"] },
      "name": "xsd:string",
      "population": { "@type": "Optional", "@class": "xsd:integer" },
    },
    {
      "@type": "Class",
      "@id": "Person",
      "@key": { "@type": "Lexical", "@fields": ["name"] },
      "name": "xsd:string",
      "age": "xsd:integer",
      "nationality": "Country",
    },
  ]

  const result = await client.addDocument(schema, { graph_type: "schema" })
  console.log("Created:", result)
  // ["terminusdb:///schema#Country", "terminusdb:///schema#Person"]
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
schema = [
    {
        "@type": "Class",
        "@id": "Country",
        "@key": {"@type": "Lexical", "@fields": ["name"]},
        "name": "xsd:string",
        "population": {"@type": "Optional", "@class": "xsd:integer"},
    },
    {
        "@type": "Class",
        "@id": "Person",
        "@key": {"@type": "Lexical", "@fields": ["name"]},
        "name": "xsd:string",
        "age": "xsd:integer",
        "nationality": "Country",
    },
]

results = client.insert_document(schema, graph_type="schema")
print("Created:", results)
# ["terminusdb:///schema#Country", "terminusdb:///schema#Person"]
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root -X POST \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Add+classes" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "@type": "Class",
      "@id": "Country",
      "@key": {"@type": "Lexical", "@fields": ["name"]},
      "name": "xsd:string",
      "population": {"@type": "Optional", "@class": "xsd:integer"}
    },
    {
      "@type": "Class",
      "@id": "Person",
      "@key": {"@type": "Lexical", "@fields": ["name"]},
      "name": "xsd:string",
      "age": "xsd:integer",
      "nationality": "Country"
    }
  ]'
```

**Expected response:**

```json
["terminusdb:///schema#Country", "terminusdb:///schema#Person"]
```
{% /code-tab %}
{% /code-tabs %}

{% callout type="warning" %}
Schema changes create a new commit. If instance documents already exist that violate the new schema, the operation will fail. Use [schema migration](/docs/schema-migration-reference-guide/) to transform existing data during schema changes.
{% /callout %}

---

## Read schema (inspect classes)

Retrieve your schema to inspect existing classes, their fields, and relationships. Use the same Document API with `graph_type=schema` for GET operations.

### List all schema classes

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const listSchema = async () => {
  const classes = await client.getDocument({
    graph_type: "schema",
    as_list: true,
  })
  console.log("Schema classes:", classes)
}
```

Returns all schema objects including the `@context`:

```json
[
  {
    "@type": "@context",
    "@schema": "terminusdb:///schema#",
    "@base": "terminusdb:///data/"
  },
  {
    "@type": "Class",
    "@id": "Country",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "population": {"@type": "Optional", "@class": "xsd:integer"}
  },
  {
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country"
  }
]
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
classes = list(client.get_all_documents(graph_type="schema"))
print("Schema classes:", classes)
```

Returns:

```python
[
    {"@type": "@context", "@schema": "terminusdb:///schema#", "@base": "terminusdb:///data/"},
    {"@type": "Class", "@id": "Country", "@key": {"@type": "Lexical", "@fields": ["name"]}, "name": "xsd:string", "population": {"@type": "Optional", "@class": "xsd:integer"}},
    {"@type": "Class", "@id": "Person", "@key": {"@type": "Lexical", "@fields": ["name"]}, "name": "xsd:string", "age": "xsd:integer", "nationality": "Country"},
]
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&as_list=true"
```

**Expected response:**

```json
[
  {"@type": "@context", "@schema": "terminusdb:///schema#", "@base": "terminusdb:///data/"},
  {"@type": "Class", "@id": "Country", "@key": {"@type": "Lexical", "@fields": ["name"]}, "name": "xsd:string", "population": {"@type": "Optional", "@class": "xsd:integer"}},
  {"@type": "Class", "@id": "Person", "@key": {"@type": "Lexical", "@fields": ["name"]}, "name": "xsd:string", "age": "xsd:integer", "nationality": "Country"}
]
```
{% /code-tab %}
{% /code-tabs %}

### Get a single class definition

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const getClass = async () => {
  const personClass = await client.getDocument({
    graph_type: "schema",
    id: "Person",
  })
  console.log("Person class:", personClass)
}
```

Returns:

```json
{
  "@type": "Class",
  "@id": "Person",
  "@key": {"@type": "Lexical", "@fields": ["name"]},
  "name": "xsd:string",
  "age": "xsd:integer",
  "nationality": "Country"
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
person_class = client.get_document("Person", graph_type="schema")
print("Person class:", person_class)
```

Returns:

```json
{"@type": "Class", "@id": "Person", "@key": {"@type": "Lexical", "@fields": ["name"]}, "name": "xsd:string", "age": "xsd:integer", "nationality": "Country"}
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&id=Person"
```

**Expected response:**

```json
{
  "@type": "Class",
  "@id": "Person",
  "@key": {"@type": "Lexical", "@fields": ["name"]},
  "name": "xsd:string",
  "age": "xsd:integer",
  "nationality": "Country"
}
```
{% /code-tab %}
{% /code-tabs %}

### Filter by type

Retrieve only classes (excluding the `@context` object):

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const getClassesOnly = async () => {
  const classes = await client.getDocument({
    graph_type: "schema",
    type: "Class",
    as_list: true,
  })
  console.log("Document classes:", classes)
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
classes = list(client.get_all_documents(graph_type="schema", doc_type="Class"))
print("Document classes:", classes)
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&type=Class&as_list=true"
```
{% /code-tab %}
{% /code-tabs %}

---

## Update schema (modify classes)

Update an existing class definition using PUT (full replacement of the class document). The class `@id` identifies which class to update.

### Add a field to an existing class

To add an **optional** field, simply PUT the class with the new field included. Optional fields are backward-compatible — existing documents remain valid.

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const addOptionalField = async () => {
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country",
    "email": { "@type": "Optional", "@class": "xsd:string" },
  }

  await client.updateDocument(updatedPerson, { graph_type: "schema" })
  console.log("Added email field to Person")
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
updated_person = {
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country",
    "email": {"@type": "Optional", "@class": "xsd:string"},
}

client.update_document(updated_person, graph_type="schema")
print("Added email field to Person")
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root -X PUT \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Add+email+field" \
  -H "Content-Type: application/json" \
  -d '{
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country",
    "email": {"@type": "Optional", "@class": "xsd:string"}
  }'
```
{% /code-tab %}
{% /code-tabs %}

{% callout type="warning" %}
**Adding a required field** (not wrapped in `Optional`) is a **strengthening** operation. It will fail if documents of that type already exist — they would lack the new required field. Use [schema migration](/docs/schema-migration-reference-guide/) with a `CreateClassProperty` operation and a default value instead.
{% /callout %}

### Change a field type (schema weakening)

Widening a type (e.g., from a specific class to a more general one) is a **weakening** operation and succeeds directly:

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const widenField = async () => {
  // Change nationality from required "Country" to optional
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": { "@type": "Optional", "@class": "Country" },
    "email": { "@type": "Optional", "@class": "xsd:string" },
  }

  await client.updateDocument(updatedPerson, { graph_type: "schema" })
  console.log("Made nationality optional")
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
updated_person = {
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": {"@type": "Optional", "@class": "Country"},
    "email": {"@type": "Optional", "@class": "xsd:string"},
}

client.update_document(updated_person, graph_type="schema")
print("Made nationality optional")
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root -X PUT \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Make+nationality+optional" \
  -H "Content-Type: application/json" \
  -d '{
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": {"@type": "Optional", "@class": "Country"},
    "email": {"@type": "Optional", "@class": "xsd:string"}
  }'
```
{% /code-tab %}
{% /code-tabs %}

{% callout type="note" %}
**Schema weakening** (making fields optional, adding optional fields, adding new classes) always succeeds because existing data still conforms. **Schema strengthening** (making fields required, narrowing types, removing fields) requires migration. See [What is Schema Weakening?](/docs/what-is-schema-weakening/) for details.
{% /callout %}

---

## Delete schema classes

Remove a class from your schema. You must first remove all instance documents of that type and any properties in other classes that reference it.

### Delete a class

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const deleteClass = async () => {
  await client.deleteDocument({
    graph_type: "schema",
    id: "Country",
  })
  console.log("Deleted Country class")
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
client.delete_document("Country", graph_type="schema")
print("Deleted Country class")
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root -X DELETE \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Delete+Country+class" \
  -H "Content-Type: application/json" \
  -d '["Country"]'
```

**Expected response:**

```json
["terminusdb:///schema#Country"]
```
{% /code-tab %}
{% /code-tabs %}

{% callout type="warning" %}
**Deletion order matters.** You cannot delete a class that is referenced by other classes. First remove or update properties that reference the class, then delete it. If instance documents of the class exist, delete those first.
{% /callout %}

### Safe deletion sequence

When removing a class that is referenced elsewhere:

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const safeDeleteCountry = async () => {
  // 1. Remove field referencing Country from Person
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "email": { "@type": "Optional", "@class": "xsd:string" },
    // nationality removed
  }
  await client.updateDocument(updatedPerson, { graph_type: "schema" })

  // 2. Delete all Country instance documents
  const countries = await client.getDocument({ type: "Country", as_list: true })
  if (countries.length > 0) {
    const ids = countries.map((c: { "@id": string }) => c["@id"])
    await client.deleteDocument({ id: ids })
  }

  // 3. Now safe to delete the class
  await client.deleteDocument({ graph_type: "schema", id: "Country" })
  console.log("Country class safely deleted")
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
# 1. Remove field referencing Country from Person
updated_person = {
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "email": {"@type": "Optional", "@class": "xsd:string"},
    # nationality removed
}
client.update_document(updated_person, graph_type="schema")

# 2. Delete all Country instance documents
countries = list(client.get_all_documents(doc_type="Country"))
if countries:
    ids = [c["@id"] for c in countries]
    client.delete_document(ids)

# 3. Now safe to delete the class
client.delete_document("Country", graph_type="schema")
print("Country class safely deleted")
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
# 1. Update Person to remove nationality field
curl -u admin:root -X PUT \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Remove+nationality" \
  -H "Content-Type: application/json" \
  -d '{
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "email": {"@type": "Optional", "@class": "xsd:string"}
  }'

# 2. Delete Country instance documents (if any)
curl -u admin:root -X DELETE \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?author=admin&message=Delete+countries&type=Country"

# 3. Delete the Country class
curl -u admin:root -X DELETE \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Delete+Country+class" \
  -H "Content-Type: application/json" \
  -d '["Country"]'
```
{% /code-tab %}
{% /code-tabs %}

---

## Full schema replacement

Replace the entire schema in one operation. This is useful for CI/CD pipelines or programmatic schema management where you maintain the schema as code.

{% callout type="warning" %}
Full replacement overwrites **all** schema documents. If your new schema is incompatible with existing instance data, the operation will fail. Use this for new databases or with [schema migration](/docs/schema-migration-reference-guide/) for existing data.
{% /callout %}

### Replace all schema classes

{% code-tabs %}
{% code-tab label="TypeScript" %}
```typescript
const replaceFullSchema = async () => {
  const fullSchema = [
    {
      "@type": "@context",
      "@schema": "terminusdb:///schema#",
      "@base": "terminusdb:///data/",
    },
    {
      "@type": "Class",
      "@id": "Product",
      "@key": { "@type": "Lexical", "@fields": ["sku"] },
      "sku": "xsd:string",
      "name": "xsd:string",
      "price": "xsd:decimal",
      "category": { "@type": "Optional", "@class": "xsd:string" },
    },
    {
      "@type": "Class",
      "@id": "Order",
      "@key": { "@type": "Random" },
      "product": "Product",
      "quantity": "xsd:integer",
      "placed_at": "xsd:dateTime",
    },
  ]

  await client.updateDocument(fullSchema, {
    graph_type: "schema",
    create: true,
  })
  console.log("Full schema replaced")
}
```
{% /code-tab %}
{% code-tab label="Python" %}
```python
full_schema = [
    {
        "@type": "@context",
        "@schema": "terminusdb:///schema#",
        "@base": "terminusdb:///data/",
    },
    {
        "@type": "Class",
        "@id": "Product",
        "@key": {"@type": "Lexical", "@fields": ["sku"]},
        "sku": "xsd:string",
        "name": "xsd:string",
        "price": "xsd:decimal",
        "category": {"@type": "Optional", "@class": "xsd:string"},
    },
    {
        "@type": "Class",
        "@id": "Order",
        "@key": {"@type": "Random"},
        "product": "Product",
        "quantity": "xsd:integer",
        "placed_at": "xsd:dateTime",
    },
]

client.update_document(full_schema, graph_type="schema", create=True)
print("Full schema replaced")
```
{% /code-tab %}
{% code-tab label="HTTP" %}
```bash
curl -u admin:root -X PUT \
  "http://localhost:6363/api/document/admin/tdb-example-mydb?graph_type=schema&author=admin&message=Replace+schema&create=true" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "@type": "@context",
      "@schema": "terminusdb:///schema#",
      "@base": "terminusdb:///data/"
    },
    {
      "@type": "Class",
      "@id": "Product",
      "@key": {"@type": "Lexical", "@fields": ["sku"]},
      "sku": "xsd:string",
      "name": "xsd:string",
      "price": "xsd:decimal",
      "category": {"@type": "Optional", "@class": "xsd:string"}
    },
    {
      "@type": "Class",
      "@id": "Order",
      "@key": {"@type": "Random"},
      "product": "Product",
      "quantity": "xsd:integer",
      "placed_at": "xsd:dateTime"
    }
  ]'
```
{% /code-tab %}
{% /code-tabs %}

### The @context object

When replacing the full schema, always include the `@context` object first. It defines the URI prefixes for your schema and instance data:

```json
{
  "@type": "@context",
  "@schema": "terminusdb:///schema#",
  "@base": "terminusdb:///data/",
  "xsd": "http://www.w3.org/2001/XMLSchema#"
}
```

| Field | Purpose | Default |
|-------|---------|---------|
| `@schema` | URI prefix for class names | `terminusdb:///schema#` |
| `@base` | URI prefix for document IDs | `terminusdb:///data/` |
| Custom prefixes | Additional URI namespaces | (none) |

If you omit `@context` from a full replacement, TerminusDB uses the defaults. Custom prefixes you previously defined will be lost.

---

## Schema migration (breaking changes)

When schema changes conflict with existing instance data, use the **Migration API** to transform data automatically. Migrations handle operations that direct PUT cannot:

- Adding **required** fields (with default values for existing documents)
- **Renaming** properties (preserving data)
- **Casting** field types (with type conversion)
- **Deleting** classes (removing instance data)

### Example: add a required field with a default

```bash
curl -u admin:root -X POST "http://localhost:6363/api/migration/admin/tdb-example-mydb" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "admin",
    "message": "Add required sku field with default",
    "operations": [
      {
        "@type": "CreateClassProperty",
        "class": "Product",
        "property": "sku",
        "type": "xsd:string",
        "default": {"@type": "Default", "value": "UNKNOWN"}
      }
    ]
  }'
```

All existing `Product` documents receive `"sku": "UNKNOWN"` automatically.

### Preview with dry-run

Test migrations without applying them:

```bash
curl -u admin:root -X POST \
  "http://localhost:6363/api/migration/admin/tdb-example-mydb?dry_run=true" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "admin",
    "message": "Preview migration",
    "operations": [
      {"@type": "CreateClassProperty", "class": "Product", "property": "sku", "type": "xsd:string", "default": {"@type": "Default", "value": "UNKNOWN"}}
    ]
  }'
```

For the full migration operation reference, see [Schema Migration Reference](/docs/schema-migration-reference-guide/).

---

## Common patterns

### Schema as code (CI/CD)

Maintain your schema in version control and apply it on deployment:

```typescript
import { readFileSync } from "fs"

const deploySchema = async () => {
  const schema = JSON.parse(readFileSync("./schema.json", "utf-8"))
  await client.updateDocument(schema, {
    graph_type: "schema",
    create: true,
  })
  console.log("Schema deployed from schema.json")
}
```

### Inspect schema changes (diff)

Compare schema between branches to review changes before merging:

```bash
curl -u admin:root \
  "http://localhost:6363/api/diff/admin/tdb-example-mydb/local/branch/main/local/branch/feature?document_id=Person&graph_type=schema"
```

### Branch-based schema development

Develop schema changes on a branch, test them, then merge:

```typescript
const schemaOnBranch = async () => {
  // Create a feature branch
  await client.branch("schema-update")
  client.checkout("schema-update")

  // Make schema changes on the branch
  const newClass = {
    "@type": "Class",
    "@id": "Address",
    "@key": { "@type": "Random" },
    "street": "xsd:string",
    "city": "xsd:string",
    "postcode": "xsd:string",
  }
  await client.addDocument(newClass, { graph_type: "schema" })

  // Test, then merge back to main
  client.checkout("main")
  await client.rebase({ rebase_from: "admin/tdb-example-mydb/local/branch/schema-update" })
}
```

---

## Next steps

- [Schema Reference Guide](/docs/schema-reference-guide/) — complete schema language specification
- [Schema Migration](/docs/schema-migration-reference-guide/) — transform data during breaking changes
- [Schema Weakening](/docs/what-is-schema-weakening/) — understand backward-compatible changes
- [Add Documents](/docs/add-a-document/) — insert data conforming to your schema
- [Schema Queries with WOQL](/docs/schema-queries-with-woql/) — query schema programmatically with WOQL
