# Custom fields

> Extend contacts with typed, discoverable attributes.

## Definitions and values

A custom field adds a structured attribute without changing the contact schema. Definitions live in `field_definitions`; values live in each record's JSONB `custom_fields` object. Definitions are workspace-specific and apply to contacts, organisations, projects, and tasks. Use `fields_list` with an optional `entity` filter to discover the appropriate definitions.

An administrator registers a key and its meaning once. Agents discover it with `fields_list`, then populate it. Unknown keys and values with the wrong type are rejected. Agents cannot create or change definitions.

## Register a field

Run with your application's PHP 8.5 binary:

```sh
php artisan orbit:field:define WORKSPACE_UUID preferred_contact_method text \
  --description="How this person prefers to be contacted" --no-interaction
```

Keys use lowercase letters, digits, and underscores, begin with a letter, and have a maximum length of 64. Supply a description of up to 1,000 characters. `--entity=contact` is the default. Use `--entity=organisation`, `--entity=project`, or `--entity=task` for other records. The same key can have different definitions on different entity types.

Running the identical command again is safe. Existing definitions cannot be retyped or repurposed; register a new key for a different meaning. Administrator registration is audited without attributing it to an agent.

## Supported types

| Type | Accepted values |
| --- | --- |
| `text` | A string of up to 2,048 characters. |
| `number` | A JSON number, not a numeric string. |
| `boolean` | JSON `true` or `false`, not text or integers. |
| `date` | A valid `YYYY-MM-DD` date. |
| `datetime` | ISO-style date/time with seconds and an explicit `Z` or offset. |
| `string_list` | Up to 20 strings, each up to 255 characters. |

Each record supports at most 30 populated keys and 16 KiB of custom-field JSON. Null is not a valid field value or a deletion shortcut.

## Patch values explicitly

After registering `preferred_contact_method`, call `contacts_update`:

```json
{
  "id": "01900000-0000-7000-8000-000000000001",
  "expected_revision": 1,
  "idempotency_key": "contact-preference-001",
  "changes": {"custom_fields": {"preferred_contact_method": "email"}}
}
```

Only supplied custom-field keys change. Other custom fields remain intact. An empty object does not clear them. To remove a value, use `remove_custom_fields` on `contacts_update`:

```json
{
  "id": "01900000-0000-7000-8000-000000000001",
  "expected_revision": 2,
  "idempotency_key": "contact-preference-remove-001",
  "remove_custom_fields": ["preferred_contact_method"]
}
```

A key cannot be set and removed in one request. All keys must be registered, including explicit removals. Removing a value preserves the definition.

## Filter contacts

```json
{"custom_fields": {"preferred_contact_method": "email"}}
```

Pass this to `contacts_search`, `organisations_search`, `projects_search`, or `tasks_search`, using a definition registered for that entity. Filters use typed equality and all supplied filters must match. Date/time custom values retain their supplied offset; equality compares the stored representation, not equivalent instants in different offsets. Missing values do not match. Only scalar fields support equality filters; string-list matching, nested queries, arbitrary query operators, and custom aggregations are deferred.
