Files
twenty/packages/twenty-docs/user-guide/data-model/capabilities/fields.mdx
Félix Malfait d602f35cbd feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary

Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).

A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).

## What ships

### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
  - BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.

### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.

### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.

### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.

### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.

### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.

### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.

## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
2026-05-25 17:47:09 +02:00

123 lines
5.4 KiB
Plaintext

---
title: Fields
description: Understand the role of fields and how to manage them.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## About Fields
Fields are like columns in a spreadsheet. They store different types of data like text, numbers, or dates. Fields can be standard (built-in) or custom (the ones you create).
### Standard Fields
Standard fields come built-in with Twenty to handle common business needs.
For example, `First Name` and `Last Name` are standard fields in the `People` object. They store text data for individual names.
You cannot delete standard fields, but you can deactivate them if you don't need them.
You can also customize the options of the standard `SELECT` type fields, for example the options for the `Stage` on Opportunities.
<img src="/images/user-guide/fields/standard-fields.png" style={{width:'100%'}}/>
### Custom Fields
Custom fields can be added to any object. You can store text, numbers, dates, dropdown selections, and more. Use custom fields to track information that's specific to your business.
For instance, a custom field for SpaceX could be `Rocket Active Status`, indicating if a rocket is operational.
<img src="/images/user-guide/fields/custom-fields.png" style={{width:'100%'}}/>
## Field Types
Twenty supports various field types:
| Type | Description | Example |
|------|-------------|---------|
| Address | Structured address with street, city, state, country, postal code | Office Address |
| Array | List of text values | Tags |
| Boolean | True/false checkbox | Is Active |
| Currency | Monetary value with currency code | Deal Amount (USD) |
| Date | Date values | Close Date |
| Date & Time | Date with time | Meeting Time |
| Domain | Website domain (used for Companies) | acme.com |
| Email | Email addresses (with primary + additional) | Contact Email |
| JSON | Structured JSON data | Custom metadata |
| Links | URLs with labels (primary + secondary) | Website, LinkedIn |
| Long Text | Multi-line text | Description, Notes |
| Multi-Select | Multiple choices from a predefined list | Tags, Categories |
| Number | Numeric values (integers or decimals) | Quantity, Score |
| Phone | Phone numbers with country code | Work Phone |
| Rating | Star rating (1-5) | Priority, Score |
| Relation | Links to records in other objects | Company → People |
| Select | Single choice from a predefined list | Stage, Status |
| Text | Single line of text | Name, Title |
## Create a Custom Field
To add a custom field to any object, follow these steps:
1. Go to `Settings` in the left sidebar.
2. Go to `Data Model`, then select the object you wish to customize.
3. Proceed by clicking on `Add Field`.
4. Choose a field name and type that suits your requirements. Consider adding a field description for better understanding.
Your newly created field is now available within the application's fields. To display it on a specific view, click on the options menu, then select `Fields`.
<VimeoEmbed videoId="927628219" title="Video demonstration" />
**Quick way:** Click the **+** button at the top right of any object table, then select `Customize fields`. This takes you directly to the Data Model settings.
<img src="/images/user-guide/fields/quick-new-field.png" style={{width:'100%'}}/>
## Deactivate a Field
You can deactivate a field to hide it from the app without losing your data. Think of it as hiding the field rather than deleting it.
Here's how you can do it:
1. Find the field you want to deactivate in your object settings.
2. Click the three dots `⋮` next to the field to open the menu.
3. Select `Deactivate` from the dropdown.
<img src="/images/user-guide/fields/deactivate-field.png" style={{width:'100%'}}/>
What happens when you deactivate a field?
1. **In the app:** The field disappears and you can't add new values to it.
2. **Existing relationships:** If it's a relation field, existing connections stay but you can't create new ones.
3. **API access:** You can still access the field and its data through the API.
You can reactivate Standard and Custom Fields or have the option to permanently delete them.
## Make Fields Unique
Make a field unique to ensure distinct records cannot have the same value. For example, email addresses are unique for each person.
If you get an error when setting uniqueness, check for duplicate values in your data (including deleted records).
## Indexes (Advanced)
Database indexes are managed automatically — adding your own is rarely necessary and easy to get wrong. With Advanced mode on, each object has an **Indexes** section under `Settings → Data Model → <object>` for the cases where you know you need one.
## Field Configuration Best Practices
### Naming Conventions and Limitations
- **Singular and plural named must be distinct**: Our GraphQL API needs distinct names for mutations
- **Protected field names**: some names are reserved for system usage (e.g., `Type`, `Application`)
### Currency and Phone Fields
- **Default currency**: can be configured via the data model
- **Default country codes**: can be configured for phone fields via the data model
### Select Fields
- **A default option can be selected** for each Select field
### Record Text Fields
- **Each object has one main display field**: This field appears in the leftmost column and represents the record when linked to other objects. It must be a text field. For example, People uses `Name` as the main field, so when you link a person to a company, you'll see their name in the company's view.