## Context
The runtime create-field path and the v2.5
`NormalizeCompositeFieldDefaultsCommand` workspace upgrade both run
composite `defaultValue`s through `nullifyEmptyCompositeDefaultValue`.
The manifest install/sync path was the only write path that skipped it:
[`fromFieldManifestToUniversalFlatFieldMetadata`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util.ts)
passed `fieldManifest.defaultValue` through verbatim.
For the SDK-emitted ACTOR system fields (`createdBy` / `updatedBy`),
`twenty-sdk` ships `{ name: "''", source: "'MANUAL'" }`. After the
runtime or the 2.5 normalize command stores them, the workspace row
holds the canonical four-key form `{ context: null, name: null, source:
"'MANUAL'", workspaceMemberId: null }`. The next install computes its TO
map from the manifest, still gets the raw two-key shape, and diffs it
against the normalized FROM. The dispatcher emits a `defaultValue`
update on each system actor field; the flat-field-metadata validator
rejects it with `FIELD_MUTATION_NOT_ALLOWED`, blocking every re-install
of any application that defines a custom object on a v2.5-normalized
workspace.
## Fix
Normalize composite `defaultValue`s inside the converter, reusing the
same `nullifyEmptyCompositeDefaultValue` helper the three other write
paths already share:
-
[`get-default-flat-field-metadata-from-create-field-input.util.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts)
— `createOneObject` and `createOneField` GraphQL paths.
-
[`sanitize-raw-update-field-input.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts)
— `updateOneField` GraphQL path.
-
[`2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts)
— the upgrade backfill that introduced the divergence.
After the fix, the four write paths agree on the canonical shape, so
re-installs are no-ops on system actor fields regardless of when the 2.5
normalize command ran. Non-composite types pass through unchanged.
## Test
New spec
`from-field-manifest-to-universal-flat-field-metadata.util.spec.ts`
covers:
- Empty-name actor defaults are normalized to the four-key canonical
shape.
- The converter is idempotent: feeding its own output back in produces
the same result (so two consecutive syncs of the same manifest never
emit a `defaultValue` update).
- When the manifest omits `defaultValue`, the converter falls back to
`generateDefaultValue` and normalizes the result.
- Non-composite defaults pass through unchanged.
```
PASS src/engine/core-modules/application/application-manifest/converters/__tests__/from-field-manifest-to-universal-flat-field-metadata.util.spec.ts
fromFieldManifestToUniversalFlatFieldMetadata
composite defaultValue normalization
✓ normalizes empty-name actor defaults to the canonical four-key shape
✓ is idempotent: re-running the converter on its own output yields the same defaultValue
✓ falls back to the generated default and normalizes it when defaultValue is omitted
✓ leaves non-composite defaults untouched
Tests: 4 passed
```
## CI gap that let this through
The integration suites covering manifest install (`appDevOnce` against
the test workspace) never re-installed an existing app on a workspace
whose composite fields had already been put through the 2.5 normalize
command. They synced once, then ran assertions on the resulting state;
the second sync that would have re-triggered the `defaultValue` diff was
never exercised.
If we want to catch this class of regression at the integration level
too, we'd add a test that (1) syncs an app whose manifest includes an
ACTOR system field with the raw SDK shape, (2) invokes
`NormalizeCompositeFieldDefaultsCommand` directly on the test workspace,
(3) re-syncs the same manifest, and (4) asserts no
`FIELD_MUTATION_NOT_ALLOWED` errors. The unit-level idempotency check in
this PR is the minimal version of that same coverage. Happy to ship that
integration spec in a follow-up if it'd help.