Skip to content

Commit e865300

Browse files
feat(spec,formula): a formula field declares a currency result — returnType 'currency' with its own currencyConfig; inferFormulaReturn stamps it when provably money (#22733)
Fixes #22727 Clause-②: yes (widening) Parked on #22744: the merge queue is walled for every lane. `render-projection-diff`'s base archive lacks the generated `registry.ts` since #22706, so `Type Check · source gates` fails on every merge-group run based at or after `ed1de8c2db`. This PR's own head `1e9d5ba927` is green, and it is re-queued once #22744 lands. ## What changed - **`@objectstack/spec`: a formula declares a currency result.** `FieldSchema.returnType` is `number | text | boolean | date | currency`. A `currency` result carries its currency the way a currency field does: in the formula's OWN `currencyConfig`, the same `CurrencyConfigSchema`, the same defaults (absent means dynamic, the tenant default currency) and the same refusals (the `precision` tombstone, the three-character code, the `currencyMode` enum). One shape, not a second one. - **Every exhaustive reader of a formula's `returnType` answers `currency` by name**, never by falling into a default arm (census below). - **Both designer forms move with the enum.** `field.form.ts` and `object.form.ts` offer Currency (same list, same order, which `form-return-type-options.test.ts` derives from the enum). The field designer's Currency Config row now shows for a formula whose return type is Currency (`visibleWhen` widened with `data.returnType == 'currency'`). Help text and the four locale bundles regenerated or re-translated (`en` regenerated; `zh-CN` / `ja-JP` / `es-ES` re-translated by hand for the two changed help texts). - **`@objectstack/formula`: `inferFormulaReturn(expression, fields)`**, the producer behind "authoring stamps it when the inferred type is provably money". It takes the host object's declared field map and returns the declaration to stamp: `{ returnType: 'currency' }`, plus `currencyConfig: { currencyMode: 'fixed', defaultCurrency }` when the source amounts share one fixed currency. Everything it cannot prove to be money answers exactly what `inferExpressionType` answers, which is unchanged. - Hand-written docs (`formulas.mdx`, `field-types.mdx`, `validation-rules.mdx`) list the new member. ## Decision: "taken from the source field" is an authoring-time stamp, not a read-time derivation Measured first: `currencyConfig` is ALREADY admitted on a `formula` field today (a full `FieldSchema` parse of a formula carrying `currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' }` succeeds and keeps it; it is equally admitted, and unread, on a `text` field). So "the field's own config" is an existing key gaining a reader, not a new admission. "From the source field" is realized by `inferFormulaReturn` copying the source currency into that declaration at authoring time. Nothing reads a referenced field's currency at display time. - **Real business need.** HotCRM finding B-15 (formulas over money render as bare numbers). In this repo the two example money formulas — app-crm `opportunity.expected_revenue` and app-showcase `project.budget_remaining` — are both proven money by the new inference (pinned in `formula-return.test.ts`). - **Long-term soundness.** Contract-first: the currency is a declaration the save door checks with the currency field's own schema. A read-time fallback from a formula to "some referenced field" is a consumer-side `??` (AGENTS.md Prime Directive 12): unchecked, and two consumers could pick two different sources. - **Harder for AI to get wrong.** The declaration is visible and refused loudly when malformed; the producer never claims money it cannot prove (two different currencies, money times money, money plus a plain quantity, an unreadable source currency, a roll-up whose unit is not on this object), and in every such case falls through to today's answer. The stamped pair is complete — an absent member means "clear it", so a formula edited from money to a count loses its stale currency. - **Startup focus.** Currency only: no `percent` or `datetime` member. No new gate, no new refusal (a `currencyConfig` on a non-currency formula stays admitted-and-unread, exactly as on every other non-currency type). No MCP surface change. ## Where the stamp lives (mechanism assumption 3, measured) - The act of writing `returnType` lives only in objectui's designer: `packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx:1103-1108`, fed by `celAuthoring.ts:614-627`, which calls `@objectstack/formula`'s `inferExpressionType` and coerces any value outside its local `CEL_VALUE_TYPES` (`:600-602`) to `unknown`. Not edited here (follow-up named in the report). - The inference it reads is in this repo, so the producer half — `inferFormulaReturn` — lands here. - `@objectstack/mcp`'s `validate_expression` reports `inferredType` from `inferExpressionType` (unchanged, still never `currency`); its `describeObject` projection carries no `currencyConfig`, so it could not name the currency anyway. Noted below, not changed. ## Reader census Instrument: `git grep -n "returnType"` over `packages/**` (non-test, non-dist), control: it hits the known reader `filter-boolean-comparand-declared-type.ts:318`. | reader | answer for a `currency` formula | |:---|:---| | `FORMULA_RETURN_TYPE_AS_FIELD_TYPE` (`filter-text-operator-declared-type.ts`), shared by the text, number and boolean doors | `currency` → judged as the `currency` field: text operator refused (`INVALID_FILTER`), number door `judged`, boolean door `not-judged` — each pinned | | door fixtures (`NUMBER_COMPARAND_DOOR_FIXTURE_FIELDS`, `TEXT_OPERATOR_DOOR_FIXTURE_FIELDS`) | gain `f_formula_currency`; their `returnType` type is now derived from `Field['returnType']` instead of a four-literal union | | `display-name.ts` `isTitleEligible` | not title-eligible (only `text` is) — comment updated | | `aggregate-field-type-compatibility.ts` | `formula` refused for sum/avg/min/max whatever it returns — unchanged | | `ui/component.zod.ts` inline form field `returnType` (by reference) | widened with the enum; describe updated | | `service-analytics` `native-sql-strategy.ts` / `measure-result-type.ts` | a formula reaches with no `returnType` and is deferred; the `formula-return-type-measure` walk now holds `currency` to the same `undefined` | | objectql doors, plugin-security RLS compiler | relay the string to the spec verdicts above; no table of their own | ## Tests Package tests on `20d64a3102`; the merge commit after it (`de9f741f33`) brought only `packages/cloud-connection` and its changeset from main. - `@objectstack/spec`: `vitest run --project local` — 643 files, 19205 passed, 1 todo. New `formula-currency-result.test.ts` (a currency formula parses; its `currencyConfig` parses identically to a currency field's and is refused with identical code, path and message; CONTROL `number` unchanged; the enum is exactly the five). `form-return-type-options.test.ts` moved with the enum: `currency` offered by both forms and parses; `datetime` still refused and unoffered. - `@objectstack/formula`: 47 files, 1326 passed. New `formula-return.test.ts`, 11 cases — the card's two pins (a formula over a currency field declares the currency result; CONTROL: a numeric formula stays `number`), the two example-app formulas, every non-money case asserted equal to the plain `inferExpressionType` answer, and every stamped declaration parsed by the rebuilt spec `FieldSchema`. - `@objectstack/objectql` door suites (3 files, 93 passed), `@objectstack/service-analytics` `formula-return-type-measure` (10 passed), `@objectstack/platform-objects` (69 files, 1082 passed). - Typecheck: spec, formula, platform-objects, service-analytics, objectql — all exit 0. ## Gates - **Derived union, run at `de9f741f33`.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 120 families (the same set it derived at `20d64a3102`). All 120 ran at `de9f741f33` and every one exited 0; the `--ran` reconciliation over the recorded exit codes reads "120 run, 0 NOT-MEASURED (a DERIVED zero — all 120 recorded an exit code and none of them is 3)". The set includes `check:generated`, `check:docs`, `check:api-surface`, `check:authorable-surface`, `check:adr-0087-registration` (a widening carries no disposition marker; green), `check:changeset-no-major`, `check:i18n`, `check:i18n-stale-fill`, `check:skills-token-ratchet`, `check:skill-examples`, `check:cross-package-test-inputs`, `check:type-check-coverage`, `check:type-check-debt` and `check:nul-bytes`. - **First round, for the record.** At `20d64a3102` two gates answered PREREQUISITE NOT MET (exit 3, nothing measured): `check:skill-examples` (client-react unbuilt) and `check:dual-build-cjs-loads` (nine package dists absent). Both re-ran green after the builds, and the second round above measured them green directly. - **Generated artifacts.** `check:generated` named one stale artifact after the enum moved, `content/docs/references/**`, regenerated with `gen:docs`; `authorable-surface` and `api-surface` did not move (an enum member is not a key, and no spec export was added). - **Lint, narrowed (CI owns the repo-wide `pnpm lint`).** Population: the 20 lintable files in this diff against the merge base, all under `eslint.config.mjs`'s global `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` object; `--no-inline-config --format json` counted 20 files, 0 errors, 0 warnings. Invariance: the config enables no type-aware linting (no `parserOptions.project`, stated in its own header at lines 327-328), so this diff cannot move a verdict on a file it does not touch. ## Governed surface The skill line `skills/objectstack-data/rules/field-types.md:253` ('(no 'currency')') goes false when this lands. The seat carries it to the skills lane as a card blocked on #22727. ## Acceptance notes - **The renderer is the next half.** objectui at the `.objectui-sha` pin draws an undeclared `returnType` through the text face (`packages/fields/src/widgets/FormulaField.tsx` final branch, `packages/fields/src/index.tsx:2703-2704` cell), so a formula hand-declared `currency` renders as monospace text until objectui formats it. The card names that follow-up (filed beside it, on hold until a spec release carries the member). Nothing in this repo stamps `currency` automatically, and the docs here state the declaration, not a display. - **The designer stamp** (objectui `ObjectFieldInspector.tsx:1103-1108`, `celAuthoring.ts:600-627`) must call `inferFormulaReturn(source, draft.fields)` and write both members; until then it keeps stamping `number` for a money formula, as today. - **`validate_expression` (MCP)** keeps answering `inferredType` from `inferExpressionType`; reporting a currency would need `describeObject` (`packages/mcp/src/stdio-data-bridge.ts:662-680`, `packages/runtime/src/domains/mcp.ts:553`) to project `currencyConfig`. Not changed: no measured pull, and the answer it gives (`number`) is still honoured. - **`currencyConfig` on a formula whose `returnType` is not `currency`** is admitted and unread, as on every non-currency type (`FieldSchema` accepts each per-type key on any type; the forms gate visibility). No refusal added: it would be a narrowing and a new gate. - The docs' "Financial Calculations" examples still declare money formulas `number`; moving them to `currency` belongs with the objectui rendering. - `object.form.ts` was a stop boundary while #22713 was in flight; #22713 merged at 2026-10-10T20:27:42Z, `origin/main` was merged first, and only this file's `returnType` row moved. ## 维护者速读(草稿) ### 改了什么 公式字段现在可以声明"货币"结果:`returnType` 新增 `currency`,货币写在公式自己的 `currencyConfig` 里,与货币字段同一套结构和默认值(不写即 dynamic,用租户默认货币)。新增 `inferFormulaReturn`:作者编辑公式时,能证明结果是钱的就给出 `currency` 和源字段的货币,证明不了就和以前一样。两个设计器表单都提供"货币"选项。 ### 为什么改 HotCRM 实测:引用金额字段的公式只能显示成裸数字。货币作为公式自身的声明,在保存时用货币字段同一套规则校验,不在渲染端回退去猜源字段。 ### 风险与代价(含回滚) 纯加宽,以前合法的都仍合法。objectui 跟进之前,手工声明 `currency` 的公式会以文本样式显示(比数字样式差),所以本 PR 不自动给任何现有公式盖章。回滚:还原本 PR 即可,没有数据迁移。 ### 席位意见 ### 你要做的 本 PR 不改 `skills/**`,不触及受管面。那一行 skill 注释由席位另立 skills 卡(阻塞于 #22727),单独请你审。 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7aa8d51 commit e865300

29 files changed

Lines changed: 715 additions & 60 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/formula": minor
4+
"@objectstack/platform-objects": patch
5+
---
6+
7+
A formula field can declare a currency result: `returnType: 'currency'`, with its currency in its own `currencyConfig`
8+
9+
Clause-②: yes (widening)
10+
11+
- **What is new in `@objectstack/spec`.** `FieldSchema.returnType` accepts `'currency'` beside `'number'`, `'text'`, `'boolean'` and `'date'`. A `currency` result is an amount of money. Its value is a bare number, as a currency field's is. Its currency is the formula field's own `currencyConfig`, the same `CurrencyConfigSchema` a currency field carries, with the same defaults and the same refusals: no `currencyConfig` means dynamic mode (the tenant default currency), and `{ currencyMode: 'fixed', defaultCurrency: 'USD' }` fixes it. Nothing is read at display time from the fields the expression references: the currency is a declaration on the formula.
12+
- **Readers.** The filter doors judge a formula returning `currency` as the currency field it reads like: a text operator on it is refused (`INVALID_FILTER`), and the number door judges its comparands. A formula is still not title-eligible unless it returns `text`, and is still refused for `sum` / `avg` / `min` / `max` (it has no stored column).
13+
- **Designer forms.** The field designer and the object designer's field grid both offer Currency as a return type. The field designer shows its Currency Config row for a formula whose return type is Currency, as it does for a currency field.
14+
- **What is new in `@objectstack/formula`.** `inferFormulaReturn(expression, fields)` returns the declaration authoring stamps onto a formula field, judged over the host object's declared field map: `{ returnType: 'currency' }` when the expression is provably an amount of money, plus `currencyConfig: { currencyMode: 'fixed', defaultCurrency }` when its source amounts are in one fixed currency. "Provably money" is unit checking over the expression: amounts in one currency added, subtracted, scaled by a number or a percent, divided by a number, rounded, or picked by `min` / `max` / `coalesce` / a ternary. Two different currencies, an amount multiplied by an amount, or an amount plus a plain quantity are not money. For everything it cannot prove to be money it answers exactly what `inferExpressionType` answers, so a numeric formula stays `number`. Its types `FormulaReturnDeclaration` and `FormulaSourceField` are exported with it. `inferExpressionType` is unchanged.
15+
- **`@objectstack/platform-objects`.** The designer help text for the return type and the currency config rows names the currency result, in all four locales.
16+
- **Nothing to migrate.** Every value accepted before is accepted now. To declare a formula over money as money, declare `returnType: 'currency'` and copy the source currency field's `currencyConfig` onto the formula, or run `inferFormulaReturn` and write what it returns.

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ Calculated field using a CEL expression (see [Expressions](/docs/data-modeling/f
409409
| Property | Type | Default | Description |
410410
|:---|:---|:---|:---|
411411
| `expression` | `string \| Expression` | **required** | CEL calculation expression |
412-
| `returnType` | `'number' \| 'text' \| 'boolean' \| 'date'` | — | Declared value type of the computed result |
412+
| `returnType` | `'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'` | — | Declared value type of the computed result. `currency`: an amount of money, in the currency the formula's own `currencyConfig` declares (none: dynamic) |
413413

414414
```typescript
415415
{ name: 'total', label: 'Total', type: 'formula', expression: 'record.quantity * record.unit_price', returnType: 'number' }

‎content/docs/data-modeling/formulas.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,14 @@ evaluates (`objectql` computes a formula virtual field from its `expression`
124124
only). Do **not** pass `type: 'currency' | 'number' | …` to `Field.formula` — it
125125
is rejected by the typed `FieldInput` and, untyped, would override `type:'formula'`
126126
so the field silently never computes. To declare what the formula returns, set
127-
**`returnType`** (`'number' | 'text' | 'boolean' | 'date'`). You declare it
127+
**`returnType`** (`'number' | 'text' | 'boolean' | 'date' | 'currency'`). You declare it
128128
yourself — nothing back-fills it — but the agent-callable `validate_expression`
129129
tool reports the type it infers from the expression (`inferredType`) so an AI
130-
author can stamp the matching value. Consumers read the declared `returnType`
130+
author can stamp the matching value. A `'currency'` result is an amount of money
131+
and carries its currency the way a currency field does: in the formula's own
132+
`currencyConfig` (none means dynamic, the tenant default currency). Copy it from
133+
the currency field the formula computes from — `inferFormulaReturn` in
134+
`@objectstack/formula` answers both halves for a formula it can prove is money. Consumers read the declared `returnType`
131135
instead of re-parsing the expression (record-title eligibility, for one: a
132136
formula is title-eligible only when its `returnType` is `'text'`, which is what
133137
lets it be *derived* as the record title — an explicit `nameField` pointer is

‎content/docs/data-modeling/validation-rules.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ application-level emptiness check is needed.
352352
| Property | Type | Default | Validation Behavior |
353353
|:---|:---|:---|:---|
354354
| `expression` | `string` | — | **Required.** CEL formula expression |
355-
| `returnType` | `enum` | — | Optional inferred result type: `number`, `text`, `boolean`, or `date` |
355+
| `returnType` | `enum` | — | Optional inferred result type: `number`, `text`, `boolean`, `date`, or `currency` |
356356

357357
**Default constraints:** Read-only. Value computed at runtime from `expression`. Not directly writable.
358358

‎content/docs/references/data/field.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ const result = CurrencyConfigSchema.parse(data);
9595
| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. |
9696
| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. |
9797
| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` |
98-
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) |
98+
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'>` | optional | Inferred value type of a formula field (number/text/boolean/date/currency). `currency`: an amount of money, in the currency the formula's own `currencyConfig` declares, as a currency field declares it (no `currencyConfig`: dynamic, the tenant default currency). |
9999
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
100100
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
101101
| **step** | `number` | optional | Step increment for slider (default: 1) |
102-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type |
102+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
103103
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
104104
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
105105
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |

‎content/docs/references/data/object.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,11 @@ const result = ApiMethod.parse(data);
260260
| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. |
261261
| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. |
262262
| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` |
263-
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) |
263+
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'>` | optional | Inferred value type of a formula field (number/text/boolean/date/currency). `currency`: an amount of money, in the currency the formula's own `currencyConfig` declares, as a currency field declares it (no `currencyConfig`: dynamic, the tenant default currency). |
264264
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
265265
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
266266
| **step** | `number` | optional | Step increment for slider (default: 1) |
267-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type |
267+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
268268
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
269269
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
270270
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
@@ -601,11 +601,11 @@ const result = ApiMethod.parse(data);
601601
| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. |
602602
| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. |
603603
| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` |
604-
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) |
604+
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'>` | optional | Inferred value type of a formula field (number/text/boolean/date/currency). `currency`: an amount of money, in the currency the formula's own `currencyConfig` declares, as a currency field declares it (no `currencyConfig`: dynamic, the tenant default currency). |
605605
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
606606
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
607607
| **step** | `number` | optional | Step increment for slider (default: 1) |
608-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type |
608+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
609609
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
610610
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
611611
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |

0 commit comments

Comments
 (0)