From b2a4bb0e0c9bef64ba6a0ef2e97a788827d165ec Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:39:06 +0200 Subject: [PATCH] docs(apps): add Targeting System Fields page (#22856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds a docs page teaching app developers how to reference auto-created **system fields** (`createdAt`, `updatedAt`, `id`, …) from views and other entities, and makes the API it documents real by exporting `generateDefaultFieldUniversalIdentifier` from the SDK. ## Why System fields are provisioned by the server, so they're never declared with `defineField()` and have no importable `universalIdentifier` constant. Since 2.19 their universal identifier is derived deterministically from the application id, the object id and the field name. Hardcoding an invented id fails sync with `INVALID_VIEW_DATA: Field metadata not found` (this is exactly what broke the twenty-partners `createdAt` view column). The twenty-partners app already imports `generateDefaultFieldUniversalIdentifier` from `twenty-sdk/define`, but the function was never exported from the SDK. This PR adds the export and documents the pattern. ## Changes - **New page** `data/system-fields.mdx` — "Targeting System Fields": - Lists the 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`). - Explains the deterministic derivation and the sync error from hardcoding ids. - Documents `generateDefaultFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, fieldName })` with a full `defineView` example. - Contrasts with standard objects (use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS..fields..universalIdentifier`) and notes that `name` is a default, not system, field. - **SDK export** — new `generate-default-field-universal-identifier.ts` wrapping the existing `getFieldUniversalIdentifier` from `twenty-shared/application` (`name` → `fieldName`), exported from `define/index.ts`. - Registered the page in `docs.json` (Data group) and cross-linked it from the Views doc. ## Notes `node_modules` isn't installed in this environment, so `nx typecheck` wasn't run. The wrapper is a signature-matched pass-through and the `twenty-shared/application` subpath + `getFieldUniversalIdentifier` barrel export were both verified to exist. --- _Generated by [Claude Code](https://claude.ai/code/session_017B7VivHcqZYjn3ukestY9U)_ Review in cubic --------- Co-authored-by: github-actions --- .../extend/apps/data/system-fields.mdx | 107 ++++++++++++++++++ .../developers/extend/apps/layout/views.mdx | 2 +- packages/twenty-docs/docs.json | 1 + .../navigation/base-structure.json | 1 + ...stub-twenty-sdk-define.plugin.spec.ts.snap | 1 + packages/twenty-sdk/src/sdk/define/index.ts | 1 + .../src/constants/DocumentationPaths.ts | 2 + 7 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-docs/developers/extend/apps/data/system-fields.mdx diff --git a/packages/twenty-docs/developers/extend/apps/data/system-fields.mdx b/packages/twenty-docs/developers/extend/apps/data/system-fields.mdx new file mode 100644 index 0000000000..becda88783 --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/data/system-fields.mdx @@ -0,0 +1,107 @@ +--- +title: Targeting System Fields +description: Reference auto-created system fields like createdAt or updatedAt from views and other entities with getFieldUniversalIdentifier. +icon: "gears" +--- + +Every object in Twenty ships with a set of **system fields** that you never declare yourself. They are created automatically by the server when the object is provisioned: + +`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` + +Because you don't declare these fields with [`defineField()`](/developers/extend/apps/data/extending-objects), there's no `universalIdentifier` constant for you to import. So how do you reference `createdAt` as a column in a [view](/developers/extend/apps/layout/views)? + +## The problem + +Since Twenty 2.19, a system field's universal identifier is **derived deterministically** by the server from three inputs: the application universal identifier, the object universal identifier, and the field name. Inventing an id and hardcoding it won't work: it matches nothing on the server, and the sync rejects the dangling reference: + +``` +Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found +``` + +## The solution + + + `getFieldUniversalIdentifier` is available from `twenty-sdk` 2.21 onward. + + +Use `getFieldUniversalIdentifier` to resolve the exact same value the server uses. It takes the three inputs and returns the field's universal identifier: + +```ts +import { getFieldUniversalIdentifier } from 'twenty-sdk/define'; + +const createdAtFieldId = getFieldUniversalIdentifier({ + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, + name: 'createdAt', +}); +``` + +- `applicationUniversalIdentifier` is your app's identifier, the one you pass to [`defineApplication()`](/developers/extend/apps/config/application). +- `objectUniversalIdentifier` is the identifier of the object the field belongs to. +- `name` is the system field name, one of the values listed above. + +## Example: a createdAt column in a view + +The typical case is adding a `createdAt` column to a view of one of your custom objects. Resolve the field id and reference it as any other `fieldMetadataUniversalIdentifier`: + +```ts src/views/example-view.ts +import { + defineView, + getFieldUniversalIdentifier, +} from 'twenty-sdk/define'; + +const APPLICATION_UNIVERSAL_IDENTIFIER = + '0b04e15c-27b2-4741-9046-b32e07469072'; +const MY_OBJECT_UNIVERSAL_IDENTIFIER = + 'c782b61c-70fd-4c88-9cd6-4e61ab8d7591'; + +export default defineView({ + universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0', + name: 'All records', + objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + position: 0, + fields: [ + { + universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104', + fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({ + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, + name: 'createdAt', + }), + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +The same resolved id works anywhere a `fieldMetadataUniversalIdentifier` is expected: view fields, filters, sorts, groups, and page-layout widgets. + + + Resolve the id, don't hardcode it. Because the server derives the value from + the application id, the object id and the field name, calling + `getFieldUniversalIdentifier` keeps your reference correct even if those + inputs change, and avoids drift if the derivation ever evolves. + + +## Standard Twenty objects + +For a **standard** Twenty object (Person, Company, Opportunity, …), you don't need to derive anything: the system field identifiers are pre-computed constants you can import directly. + +```ts +import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + +// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier +// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier +``` + +Reach for `getFieldUniversalIdentifier` when the object is one **your app** defines with [`defineObject()`](/developers/extend/apps/data/objects), where no such constant exists. + + + `name` is a **default** field, not a system field. It keeps its own hardcoded + universal identifier and is not resolved through + `getFieldUniversalIdentifier`. On objects you define, reference the + `name` field by the identifier you gave it in `defineObject()`. + diff --git a/packages/twenty-docs/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/developers/extend/apps/layout/views.mdx index 8d265e31e8..e64adf6184 100644 --- a/packages/twenty-docs/developers/extend/apps/layout/views.mdx +++ b/packages/twenty-docs/developers/extend/apps/layout/views.mdx @@ -34,7 +34,7 @@ export default defineView({ - `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. - `key: ViewKey.INDEX` marks the view as the object's main list view (the one an `OBJECT` navigation item opens). -- `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +- `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. To reference an auto-created system field such as `createdAt`, see [Targeting System Fields](/developers/extend/apps/data/system-fields). - You can also declare `filters`, `filterGroups`, `sorts`, `groups`, and `fieldGroups` for advanced configurations. - `position` controls ordering when multiple views exist for the same object. diff --git a/packages/twenty-docs/docs.json b/packages/twenty-docs/docs.json index 37c6846d39..9e1856c115 100644 --- a/packages/twenty-docs/docs.json +++ b/packages/twenty-docs/docs.json @@ -413,6 +413,7 @@ "developers/extend/apps/data/overview", "developers/extend/apps/data/objects", "developers/extend/apps/data/extending-objects", + "developers/extend/apps/data/system-fields", "developers/extend/apps/data/relations" ] }, diff --git a/packages/twenty-docs/navigation/base-structure.json b/packages/twenty-docs/navigation/base-structure.json index 08261a0ce3..372f7c69d6 100644 --- a/packages/twenty-docs/navigation/base-structure.json +++ b/packages/twenty-docs/navigation/base-structure.json @@ -413,6 +413,7 @@ "developers/extend/apps/data/overview", "developers/extend/apps/data/objects", "developers/extend/apps/data/extending-objects", + "developers/extend/apps/data/system-fields", "developers/extend/apps/data/relations" ] }, diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap index ac1d691261..954cca8a18 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap +++ b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap @@ -37,6 +37,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1 "everyEquals", "favoriteRecordIds", "featureFlags", + "getFieldUniversalIdentifier", "hasAnySoftDeleteFilterOnView", "includes", "includesEvery", diff --git a/packages/twenty-sdk/src/sdk/define/index.ts b/packages/twenty-sdk/src/sdk/define/index.ts index a75a2a6061..44320b7839 100644 --- a/packages/twenty-sdk/src/sdk/define/index.ts +++ b/packages/twenty-sdk/src/sdk/define/index.ts @@ -23,6 +23,7 @@ export type { } from '@/sdk/define/fields/composite-fields'; export { defineField } from '@/sdk/define/fields/define-field'; export { FieldType } from '@/sdk/define/fields/field-type'; +export { getFieldUniversalIdentifier } from 'twenty-shared/application'; export { OnDeleteAction } from '@/sdk/define/fields/on-delete-action'; export { RelationType } from '@/sdk/define/fields/relation-type'; export { validateFields } from '@/sdk/define/fields/validate-fields'; diff --git a/packages/twenty-shared/src/constants/DocumentationPaths.ts b/packages/twenty-shared/src/constants/DocumentationPaths.ts index 96d4f6f4e6..ce0ad2214e 100644 --- a/packages/twenty-shared/src/constants/DocumentationPaths.ts +++ b/packages/twenty-shared/src/constants/DocumentationPaths.ts @@ -32,6 +32,8 @@ export const DOCUMENTATION_PATHS = { DEVELOPERS_EXTEND_APPS_DATA_OVERVIEW: '/developers/extend/apps/data/overview', DEVELOPERS_EXTEND_APPS_DATA_RELATIONS: '/developers/extend/apps/data/relations', + DEVELOPERS_EXTEND_APPS_DATA_SYSTEM_FIELDS: + '/developers/extend/apps/data/system-fields', DEVELOPERS_EXTEND_APPS_GETTING_STARTED_CONCEPTS: '/developers/extend/apps/getting-started/concepts', DEVELOPERS_EXTEND_APPS_GETTING_STARTED_LOCAL_SERVER: