Skip to content

Custom Fields

Matt Dula edited this page Apr 18, 2026 · 1 revision

Custom Fields

Workspace-scoped named fields on any entity type. Values live in each row's data JSONB; this registry just declares which keys the workspace expects so agents can see them.

Why

The data JSONB column on every entity is free-form, which is great for agent flexibility but bad for discoverability. Without a registry, an agent syncing from Apollo might write data.linkedin, another writing from a CSV might write data.linkedin_url, and a third might write data.li. Now you've got three keys for the same concept.

Custom fields are the opt-in "yes, we track this; call it that" declaration.

Create

Owners and admins:

curl -X POST http://localhost:8000/custom-fields \
  -H "Authorization: Bearer nk_..." \
  -d '{
    "entity_type": "contact",
    "name": "linkedin_url",
    "label": "LinkedIn URL",
    "field_type": "url",
    "required": false,
    "description": "profile URL on linkedin.com"
  }'

Fields:

Field Meaning
entity_type contact | company | deal | activity | note | task
name snake_case key that goes into data.<name>. Unique per (entity_type, workspace).
label human-readable; for UI / docs
field_type string | text | number | bool | date | url | email | select
required advisory in v1 (no runtime validation yet)
default_value JSONB; can be any shape
options for select — list of allowed values
description free-form

List

curl -s http://localhost:8000/custom-fields?entity_type=contact \
  -H "Authorization: Bearer nk_..."

Members can read. The agent should call this at session start to know what fields the workspace cares about, then set them in data.<name> on writes.

Update / delete

curl -X PATCH http://localhost:8000/custom-fields/<id> \
  -H "Authorization: Bearer nk_..." \
  -d '{"label":"LinkedIn","required":true}'

curl -X DELETE http://localhost:8000/custom-fields/<id> \
  -H "Authorization: Bearer nk_..."

Values go in data, not on the column

Nakatomi doesn't alter the schema when you register a custom field. Values live in the row's data JSONB. So a contact with a registered linkedin_url field looks like:

{
  "id": "...",
  "first_name": "Ada",
  "email": "ada@example.com",
  "data": {
    "linkedin_url": "https://linkedin.com/in/adalovelace"
  }
}

This keeps migrations free — registering or unregistering a field is a row-level operation in custom_field_definitions, never a ALTER TABLE.

Filtering by custom fields

Filter on JSONB keys via Postgres-native operators:

-- contacts with a linkedin_url set
SELECT * FROM contacts
WHERE workspace_id = '...'
  AND data ? 'linkedin_url';

-- deals tagged as strategic in data
SELECT * FROM deals
WHERE workspace_id = '...'
  AND data->>'strategic' = 'true';

The REST list endpoints don't accept JSONB filters as query params in v1 — that's on the roadmap. For now, if an agent needs to filter by a custom field, it paginates and filters client-side, or queries Postgres directly (advanced).

In /schema

GET /schema surfaces the custom_field entity pointer so agents know the endpoint exists. The per-workspace field list comes from GET /custom-fields — call both for full context.

Planned (v2)

  • Runtime validation (required fields enforced on write)
  • Typed coercion (e.g. number fields validated as numeric on write)
  • REST filter params (?data.linkedin_url=https://...)
  • MCP tools for custom field management
  • UI surface on the dashboard

Clone this wiki locally