> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Defining a schema for a record type

Use collections to define the metadata shape for a record type. Register a record
type, attach a constrained JSON Schema, and choose how the API handles writes that
do not conform. The default validation mode is `off`, so registration does not
change record writes until you choose `warn` or `enforce`.

## Before you begin

Make sure that you have the following:

* A Runtype API key with the `RECORDS:READ` and `RECORDS:WRITE` permissions, or
  `RECORDS:*`. Without them, collection requests return `403`. For more
  information, see [Managing API keys](/user-guide/settings/managing-api-keys).
* Optional: existing records of the target `type`, if you plan to infer a schema
  from them.

Manage collections through the Runtype API, the TypeScript SDK, the `runtype` CLI,
or the Runtype Model Context Protocol (MCP) server. The dashboard does not have a
collections editor.

## What a collection is

A collection is the registration of one record type. Set the collection's `slug`
to the record's `type` value. A collection stores the following information:

* **Display metadata**: `displayName`, `description`, and `icon`.
* **An optional schema**: defines the fields in `metadata`.
* **A validation mode**: `off`, `warn`, or `enforce`. The default is `off`.
* **Schema history**: each schema change increments `schemaVersion` and adds a
  history entry.

A `slug` starts with a letter or digit and contains only letters, digits,
underscores, dots, and hyphens, up to 255 characters. The slugs
`client_conversation` and `types.d.ts` are reserved. A record type whose name
does not fit these rules cannot have a collection.

Record writes for an unregistered type, or for a collection without a schema,
continue without schema validation.

## Schema dialect

Use the following constrained JSON Schema dialect for collection schemas:

* Define the root as an `object` with `properties`, an optional `required` list,
  and an optional `additionalProperties` flag.
* Define no more than 200 properties at the root.
* Name properties with lowercase letters, digits, and underscores, 1 to 64
  characters.
* Use `string`, `number`, `boolean`, `array`, or `object` property types. Each
  property accepts an optional `description`.
* Define one nested object level. Object properties can contain scalar properties.
  Array items can be scalar properties or one nested object.
* Use `enum`, `format`, and `maxLength` with `string` properties. An `enum` holds
  up to 100 values. Supported formats are `date-time`, `email`, `uri`, and
  `record-id`.
* Use `minimum` and `maximum` with `number` properties.
* Leave `additionalProperties` unset to accept extra keys, or set it to `false` to
  reject them.
* Do not use `$ref`, `oneOf`, `anyOf`, `allOf`, `if`, `then`, `patternProperties`,
  or `pattern`.

The `record-id` format accepts any string. The API does not check that the
referenced record exists.

The API normalizes metadata keys before it validates a write from the Runtype API,
the SDK, the CLI, or an agent's record tools. It lowercases each top-level key,
replaces each run of characters other than letters and digits with `_`, and
removes leading and trailing underscores. Declare the normalized names in your
schema. For example, a write with `signedUpAt` is stored and validated as
`signedupat`, and a write with `signed-up-at` as `signed_up_at`. Flow record steps
store and validate keys exactly as the flow writes them, so use lowercase
`snake_case` keys in flow output.

The following JSON defines a schema for customer metadata:

**`Example schema`**

```json title="Example schema"
{
  "type": "object",
  "properties": {
    "tier": { "type": "string", "enum": ["free", "pro", "enterprise"] },
    "mrr": { "type": "number", "description": "Monthly recurring revenue in USD" },
    "signed_up_at": { "type": "string", "format": "date-time" },
    "is_active": { "type": "boolean" }
  },
  "required": ["tier"]
}
```

## Choose a validation mode

Use the following table to choose how the API handles a nonconforming write:

| Mode      | Nonconforming write                                  | `schemaValid` on the record |
| --------- | ---------------------------------------------------- | --------------------------- |
| `off`     | Proceeds without schema validation                   | Unchanged                   |
| `warn`    | Proceeds and returns `schemaWarnings`                | `false`                     |
| `enforce` | Returns `422` with `schema_validation_failed` errors | Not written                 |

In `warn` and `enforce` modes, conforming writes set `schemaValid` to `true`.
Filter on the `schemaValid` field to find nonconforming records.

In `warn` mode, a record create or update response includes a `schemaWarnings`
array. Each entry has the same `field`, `code`, and `message` as a `422` detail:

**`Warn-mode create response (excerpt)`**

```json title="Warn-mode create response (excerpt)"
{
  "id": "RECORD_ID",
  "type": "customers",
  "schemaValid": false,
  "schemaWarnings": [
    {
      "field": "tier",
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Instance does not have required property \"tier\"."
    }
  ]
}
```

### Where validation applies

Every write path validates records against the collection schema. The following
table shows what each path does with a nonconforming write:

| Write path                                         | In `enforce` mode                                                                                                                         | In `warn` mode                                                           |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Create or update a record                          | Returns `422` and writes nothing                                                                                                          | Writes and returns `schemaWarnings`                                      |
| Bulk edit                                          | Rejects the whole edit with `422`, adding `recordId` and `recordName` for the failing record                                              | Writes and returns `schemaWarnings` per record as `{ recordId, errors }` |
| CSV import                                         | Counts the row as failed and imports the other rows                                                                                       | Imports the row                                                          |
| **Upsert Record** and **Update Record** flow steps | Writes no record. With `errorHandling` unset, the step continues with its `defaultValue`. Set `errorHandling` to `fail` to stop the flow. | Writes and adds `schemaWarnings` to the step metadata                    |
| Agent record tools                                 | Returns an error that contains `SCHEMA_VALIDATION_FAILED`                                                                                 | Writes and returns `schemaWarnings`                                      |

If Runtype cannot load the collection schema, every write path refuses the
write in any mode, including for a record type with no collection. The
following table shows what each path returns:

| Write path                                         | When the schema lookup fails                                                                |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Create or update a record                          | Returns `503` with code `COLLECTION_LOOKUP_FAILED` and writes nothing                       |
| Bulk edit                                          | Rejects the whole edit with `503`, adding `recordId` and `recordName`                       |
| CSV import                                         | Counts the row as failed with a `COLLECTION_LOOKUP_FAILED` error and imports the other rows |
| **Upsert Record** and **Update Record** flow steps | Writes no record. `errorHandling` controls continuation, as it does in `enforce` mode       |
| Agent record tools                                 | Returns an error that contains `COLLECTION_LOOKUP_FAILED`                                   |

A lookup failure is temporary. Retry the write.

A record update is validated only when it changes `metadata` or `type`. Moving a
record into an enforced record type with a `type` change is validated against
that type's schema.

## Adopt a schema for an existing record type

Follow these tasks in order. The examples use the `customers` record type. They
read your API key from the `RUNTYPE_API_KEY` environment variable:

**`Set the API key`**

```bash title="Set the API key"
export RUNTYPE_API_KEY=YOUR_API_KEY
```

Replace `YOUR_API_KEY` with your Runtype API key. The TypeScript examples use this
client:

**`Create the SDK client`**

```typescript title="Create the SDK client"
import { RuntypeClient } from '@runtypelabs/sdk'

const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
```

For a new record type with no existing records, you can pass `schema` and
`validationMode` when you create the collection, and skip inference.

### Register the collection

Register the `customers` collection. It starts in `off` mode, so record writes do
not change:

#### cURL

```bash
curl --request POST https://api.runtype.com/v1/collections \
  --header "Authorization: Bearer $RUNTYPE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "slug": "customers", "displayName": "Customers" }'
```

#### TypeScript

```typescript
await client.collections.create({ slug: 'customers', displayName: 'Customers' })
```

#### CLI

```bash
runtype collections create --slug customers --name "Customers"
```

### Infer a schema from existing records

Request a schema proposal. The API samples records by most recent `updatedAt`
and saves nothing. `sample` accepts 1 to 1,000 and defaults to 500. You can
infer a schema before or after you register the collection.

#### cURL

```bash
curl --request POST https://api.runtype.com/v1/collections/customers/infer-schema \
  --header "Authorization: Bearer $RUNTYPE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "sample": 500 }'
```

#### TypeScript

```typescript
const proposal = await client.collections.inferSchema('customers', { sample: 500 })
```

#### CLI

```bash
runtype collections infer customers --output customers-schema.json
```

The response includes the following fields:

* `schema`: the proposed schema, or `null` if the API cannot infer any field, for
  example because the type has no records.
* `fields`: each field's type, `confidence`, `presence`, and sample values.
* `sampledRecords` and `totalRecords`: how many records the API read and how many
  exist.
* `skippedKeys`: metadata keys that the proposal leaves out, because the key name
  is not lowercase letters, digits, and underscores or because the values do not
  map to a supported type.

Review the proposal before you save it. Rename any skipped key that the schema
needs.

### Save the schema in warn mode

Save the schema and set the mode to `warn`. Nonconforming writes proceed, return
`schemaWarnings`, and set `schemaValid` to `false`.

#### cURL

```bash
curl --request PATCH https://api.runtype.com/v1/collections/customers \
  --header "Authorization: Bearer $RUNTYPE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "schema": {
      "type": "object",
      "properties": { "tier": { "type": "string" } },
      "required": ["tier"]
    },
    "validationMode": "warn"
  }'
```

#### TypeScript

```typescript
if (proposal.schema) {
  await client.collections.update('customers', {
    schema: proposal.schema,
    validationMode: 'warn',
  })
}
```

#### CLI

```bash
runtype collections update customers --schema-file customers-schema.json --mode warn
```

### Dry-run existing records

Check existing records against the saved schema without writing anything. The API
checks up to 10,000 records and returns `checked`, `failed`, `truncated`, and
failing `examples`. If `truncated` is `true`, the API did not check every record.

#### cURL

```bash
curl --request POST https://api.runtype.com/v1/collections/customers/validate-existing \
  --header "Authorization: Bearer $RUNTYPE_API_KEY"
```

#### TypeScript

```typescript
const dryRun = await client.collections.validateExisting('customers')
```

#### CLI

```bash
runtype collections validate customers
```

To test a proposed schema before you save it, send it as `schema` in the request
body. In the CLI, pass `--schema-file`.

Update the failing records, then run the dry run again until `failed` is `0`.

### Switch to enforce mode

Set the mode to `enforce`. The response includes an `enforceCheck` summary of up
to 1,000 existing records:

#### cURL

```bash
curl --request PATCH https://api.runtype.com/v1/collections/customers \
  --header "Authorization: Bearer $RUNTYPE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "validationMode": "enforce" }'
```

#### TypeScript

```typescript
if (dryRun.failed === 0 && !dryRun.truncated) {
  const result = await client.collections.update('customers', {
    validationMode: 'enforce',
  })
  console.log(result.enforceCheck)
}
```

#### CLI

```bash
runtype collections update customers --mode enforce
```

After you switch, a write that violates the schema returns this `422` response:

**`422 response`**

```json title="422 response"
{
  "error": "schema_validation_failed",
  "collection": "customers",
  "schemaVersion": 2,
  "details": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Instance does not have required property \"tier\".",
      "field": "tier",
      "path": "metadata.tier"
    }
  ]
}
```

To list collections with their record counts, run `runtype collections list --count`.

## Manage collections with the MCP server

The Runtype MCP server provides these tools for collections: `list_collections`,
`get_collection`, `create_collection`, `update_collection`, `delete_collection`,
`infer_collection_schema`, `validate_collection_records`, and
`get_collection_types`. The Code Mode MCP server, which runs JavaScript against
the Runtype API, exposes the matching methods `listCollections`, `getCollection`,
`createCollection`, `updateCollection`, `deleteCollection`,
`inferCollectionSchema`, `validateExistingRecords`, and `getCollectionTypegen`.
The `runtype://records/collections` MCP resource lists each collection's schema.
For more information, see
[Runtype MCP server](/developer-guides/integrations/runtype-mcp-server).

## Generate TypeScript types

Generate a TypeScript declaration file after you add a schema. The file contains
one interface for the `metadata` of each collection that has a schema, and a
`declare module '@runtypelabs/sdk'` block that augments `RecordCollections`.

To write the declarations to a file, run this CLI command:

**`CLI`**

```bash title="CLI"
runtype records typegen --output runtype-records.d.ts
```

You can also get the declarations as text in the following ways:

* Send `GET /v1/collections/types.d.ts`. The response is `text/plain`.
* Call `client.collections.typegen()` in the TypeScript SDK.
* Call `get_collection_types` on the Runtype MCP server, or
  `getCollectionTypegen()` in Code Mode.

Commit the generated file and run the same command in CI. Diff the result to
detect schema drift:

**`CI drift check`**

```bash title="CI drift check"
runtype records typegen --output runtype-records.d.ts
git diff --exit-code runtype-records.d.ts
```

### Use typed record access

Include the generated file in the paths that `tsconfig` includes. Use
`client.records.from('customers')` to pin record operations to the collection
slug and type `metadata`:

**`Typed records`**

```typescript title="Typed records"
const customers = client.records.from('customers')

// metadata is typed as the customers collection schema
await customers.create({
  name: 'Example Organization',
  metadata: { tier: 'pro' },
})

const record = await customers.get('RECORD_ID')
```

Replace `RECORD_ID` with a record ID. For an unregistered slug,
`client.records.from(slug)` uses `Record<string, unknown>` for `metadata`.

## How agents see your schema

The **Upsert Record**, **Get Record**, and **List Records** built-in tools
(`runtype_record_upsert`, `runtype_record_get`, and `runtype_record_list`) list the field names, types,
and required flags of each collection that has a schema in their tool
descriptions. For more information, see
[Built-in tools](/user-guide/tools/built-in-tools).

## Evolve a schema

When you save a schema, the API classifies the change as additive or breaking:

* **Additive changes** include new optional fields, widened enums, and removing
  the schema.
* **Breaking changes** include adding a schema to a schemaless collection, removed
  fields, new required fields, changed types, narrowed enums, closing
  `additionalProperties`, and tightened constraints.

The update response returns the collection under `collection`. When the schema
changes, it also returns `schemaChange`:

**`Update response (excerpt)`**

```json title="Update response (excerpt)"
{
  "collection": { "slug": "customers", "schemaVersion": 3, "validationMode": "warn" },
  "schemaChange": {
    "kind": "breaking",
    "breakingChanges": ["mrr: property became required"]
  }
}
```

The API rejects a breaking change when the resulting mode is `enforce`:

**`400 response`**

```json title="400 response"
{
  "error": "This schema change is breaking; drop validationMode to \"warn\", migrate existing records, then re-enable \"enforce\"",
  "code": "BREAKING_SCHEMA_CHANGE_REQUIRES_WARN_MODE",
  "details": { "breakingChanges": ["mrr: property became required"] }
}
```

To apply a breaking change to an enforced collection, follow these steps:

1. Send the updated schema with `validationMode` set to `warn` in one update request.
2. Update the records that fail the updated schema.
3. Dry-run existing records until `failed` is `0` and `truncated` is `false`.
4. Set `validationMode` to `enforce`.

To remove a schema, send `"schema": null`.

Each schema change increments `schemaVersion` and adds a history entry with
`version`, `schema`, `changedBy`, `changeKind`, and `createdAt`. To return the
history, send `GET /v1/collections/customers?includeHistory=true` or run
`runtype collections get customers --history`.

> **Note**
>
> Collection `slug` values are immutable. To rename a record type, create another collection and
> migrate records to that type.

## Troubleshoot collection errors

Collection requests return an error body with `error`, `code`, and sometimes
`details`. The following table lists the common codes:

| Code                                        | Status | Cause                                                          | Fix                                                   |
| ------------------------------------------- | ------ | -------------------------------------------------------------- | ----------------------------------------------------- |
| `BREAKING_SCHEMA_CHANGE_REQUIRES_WARN_MODE` | `400`  | A breaking schema change while the mode is `enforce`           | Send the change with `validationMode` set to `warn`   |
| `CONCURRENT_MODIFICATION`                   | `409`  | Another request changed the collection at the same time        | Get the collection again and retry the update         |
| `NO_SCHEMA_TO_VALIDATE`                     | `400`  | A dry run with no saved schema and no `schema` in the body     | Save a schema or send one in the request body         |
| `SCHEMA_INVALID`                            | `400`  | The schema does not follow the dialect; `details` lists issues | Fix the schema parts that `details` names             |
| `SLUG_CONFLICT`                             | `409`  | A collection with this slug exists                             | Update the existing collection                        |
| `SLUG_IMMUTABLE`                            | `400`  | An update tried to change the slug                             | Create another collection for the renamed record type |
| `SLUG_INVALID`                              | `400`  | The slug does not match the slug rules                         | Use letters, digits, underscores, dots, and hyphens   |
| `SLUG_RESERVED`                             | `400`  | The slug is `client_conversation` or `types.d.ts`              | Choose another record type                            |

## Delete a collection

Delete a collection only when you no longer need its schema. Deletion removes the
registration and leaves records of that type unchanged. The records return to
schemaless behavior. Delete records with a separate operation.

> **Warning**
>
> Deleting a collection permanently discards its schema and version history.

To delete the `customers` collection, send this request. The API returns
`{ "success": true }`:

**`Delete the collection`**

```bash title="Delete the collection"
curl --request DELETE https://api.runtype.com/v1/collections/customers \
  --header "Authorization: Bearer $RUNTYPE_API_KEY"
```

In the CLI, run `runtype collections delete customers`. On the Runtype MCP server,
call `delete_collection`.

## Next steps

Continue with one of these topics:

* [What are Records?](/user-guide/records/what-are-records): review record types, metadata, and limits
* [Filtering and searching records](/user-guide/records/filtering-and-searching-records): find nonconforming records with the `schemaValid` field
* [Creating and managing records](/user-guide/records/creating-and-managing-records): create the records that your collections govern
* [Using records in flows](/user-guide/records/using-records-in-flows): read and write validated records from flow steps
* [Record limits and key rules](/user-guide/records/record-limits): check metadata size limits and key normalization rules