> ## Documentation Index
> Fetch the complete documentation index at: https://trysalty.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema engine

> How custom_attributes validation works. Defined keys checked; unknown keys pass through.

People, Companies, Deals, and Custom Object records all carry a `custom_attributes` (or `data`) JSONB field. The schema engine enforces typing only for keys you've explicitly declared — unknown keys pass through.

## The four cases

| Key state                                                   | Behavior on write                                                                 |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **Declared, value present**                                 | Validated against `data_type`, `enum_values`, `reference_object_type`             |
| **Declared, value absent**, has `default_value`             | Default is applied (create only — PATCH leaves absent keys absent)                |
| **Declared, value absent**, `is_required: true`, no default | `400 attribute_required` (create only — PATCH ignores)                            |
| **Undefined key**                                           | Stored as-is, no validation                                                       |
| **Declared, then deprecated** (`DELETE /schema/...`)        | New writes rejected with `400 attribute_deprecated`; existing data stays readable |

This means an agent can experiment freely with new fields, then formalize the ones that stick by calling `POST /schema/:object_type/attributes`.

## Supported `data_type` values

| `data_type` | Accepted input                                                                                       |
| ----------- | ---------------------------------------------------------------------------------------------------- |
| `text`      | string                                                                                               |
| `number`    | finite number (no NaN/Infinity)                                                                      |
| `boolean`   | true / false                                                                                         |
| `date`      | string matching `YYYY-MM-DD`                                                                         |
| `datetime`  | ISO 8601 string parseable by `Date.parse`                                                            |
| `enum`      | string ∈ `enum_values`                                                                               |
| `reference` | UUID matching a row in `reference_object_type` (`person` / `company` / `deal`) in the same workspace |
| `json`      | anything JSON-serializable                                                                           |

## Defining an attribute

```bash theme={null}
curl -X POST $SALTY_API/schema/person/attributes \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "attribute_key": "tier",
    "display_name": "Tier",
    "data_type": "enum",
    "enum_values": ["free", "pro", "enterprise"],
    "is_required": false,
    "default_value": "free"
  }'
```

Once defined, writes to People are validated: `custom_attributes.tier` must be one of the three enum values.

## Modifying an attribute (`PATCH`)

You can change `display_name`, `default_value`, `is_required`, and **append** to `enum_values`. `data_type` is frozen after creation (changing it requires a data migration; not in v1). Enum removal isn't allowed in v1 — only append.

```bash theme={null}
curl -X PATCH $SALTY_API/schema/person/attributes/tier \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -d '{"enum_values_append": ["growth"]}'
```

## Deprecating an attribute (`DELETE`)

`DELETE` is soft. The definition gets `deprecated_at` set; existing data is left intact; new writes of the key are rejected.

```bash theme={null}
curl -X DELETE $SALTY_API/schema/person/attributes/lead_source \
  -H "Authorization: Bearer $SALTY_API_KEY"
# 204 No Content
```

After deprecation, `GET /schema/person` shows the attribute with `"deprecated": true`.

## Audit trail

Every schema mutation (`POST` / `PATCH` / `DELETE` / `POST /custom-objects`) appends a row to `schema_migrations` with the calling `api_key_id`, the action, and a JSON details blob.
