Skip to main content
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 stateBehavior on write
Declared, value presentValidated against data_type, enum_values, reference_object_type
Declared, value absent, has default_valueDefault is applied (create only — PATCH leaves absent keys absent)
Declared, value absent, is_required: true, no default400 attribute_required (create only — PATCH ignores)
Undefined keyStored 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_typeAccepted input
textstring
numberfinite number (no NaN/Infinity)
booleantrue / false
datestring matching YYYY-MM-DD
datetimeISO 8601 string parseable by Date.parse
enumstring ∈ enum_values
referenceUUID matching a row in reference_object_type (person / company / deal) in the same workspace
jsonanything JSON-serializable

Defining an attribute

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