Skip to content

Commit a71692e

Browse files
os-zhuangclaude
andauthored
docs(guides): formula fields declare result type via returnType, not type (#2263)
* docs(guides): formula fields declare result type via `returnType`, not `type` `Field.formula`'s config is `FieldInput = Omit<Partial<Field>, 'type'>`, so passing `type: 'currency' | 'text' | 'number' | …` is a type error — and untyped (in an .mdx example) it OVERRIDES `type:'formula'`, which makes objectql skip the field entirely: the engine computes a formula virtual field from its `expression` only (engine.ts), so the field silently never evaluates. Several guide examples taught that broken pattern; some also used a `formula:` key that isn't on `FieldSchema` (the source key is `expression`). Fix all examples to declare the computed value's type with `returnType` (`number`/`text`/`boolean`/`date` — added in framework#2255) and put the CEL source on `expression`; restore the valid `scale` I had dropped; add a Callout in formula.mdx explaining a formula field's `type` is always `formula`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: empty changeset for docs-only formula guide fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c1a754a commit a71692e

3 files changed

Lines changed: 43 additions & 26 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs: formula guides declare a formula result type via `returnType` (not the forbidden `type`); CEL source on `expression`.

content/docs/guides/business-logic.mdx

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,8 @@ Formula fields are CEL. Use ternary `cond ? a : b` for conditionals and
386386
// Tiered pricing
387387
Field.formula({
388388
label: 'Discount Tier',
389-
type: 'text',
390-
formula: `
389+
returnType: 'text',
390+
expression: `
391391
record.amount > 1000000 ? "Platinum" :
392392
record.amount > 500000 ? "Gold" :
393393
record.amount > 100000 ? "Silver" : "Bronze"
@@ -397,8 +397,8 @@ Field.formula({
397397
// Status indicator
398398
Field.formula({
399399
label: 'Health Score',
400-
type: 'text',
401-
formula: `
400+
returnType: 'text',
401+
expression: `
402402
record.is_active && record.days_since_contact < 30 ? "Healthy" :
403403
record.days_since_contact < 90 ? "At Risk" : "Critical"
404404
`,
@@ -411,49 +411,50 @@ Field.formula({
411411
// Days until close
412412
Field.formula({
413413
label: 'Days to Close',
414-
type: 'number',
415-
formula: 'daysBetween(record.close_date, today())',
414+
returnType: 'number',
415+
expression: 'daysBetween(record.close_date, today())',
416416
})
417417

418418
// Contract end in 30 days
419419
Field.formula({
420420
label: 'Expiring Soon',
421-
type: 'boolean',
422-
formula: 'record.status == "active" && daysBetween(record.end_date, today()) <= 30',
421+
returnType: 'boolean',
422+
expression: 'record.status == "active" && daysBetween(record.end_date, today()) <= 30',
423423
})
424424

425425
// Age in days
426426
Field.formula({
427427
label: 'Age (Days)',
428-
type: 'number',
429-
formula: 'daysBetween(today(), record.created_date)',
428+
returnType: 'number',
429+
expression: 'daysBetween(today(), record.created_date)',
430430
})
431431
```
432432

433433
### Financial Calculations
434434

435435
```typescript
436-
// Gross margin
436+
// Gross margin (computes a number; a formula's type is always 'formula' —
437+
// declare the value type with returnType, and decimal places with scale)
437438
Field.formula({
438439
label: 'Gross Margin %',
439-
type: 'percent',
440-
formula: 'record.revenue > 0 ? ((record.revenue - record.cost) / record.revenue) * 100 : 0',
440+
returnType: 'number',
441+
expression: 'record.revenue > 0 ? ((record.revenue - record.cost) / record.revenue) * 100 : 0',
441442
scale: 2,
442443
})
443444

444445
// Weighted pipeline
445446
Field.formula({
446447
label: 'Weighted Amount',
447-
type: 'currency',
448-
formula: 'record.amount * (record.probability / 100)',
448+
returnType: 'number',
449+
expression: 'record.amount * (record.probability / 100)',
449450
scale: 2,
450451
})
451452

452453
// Total with tax
453454
Field.formula({
454455
label: 'Total with Tax',
455-
type: 'currency',
456-
formula: 'record.subtotal * 1.0825', // 8.25% tax
456+
returnType: 'number',
457+
expression: 'record.subtotal * 1.0825', // 8.25% tax
457458
scale: 2,
458459
})
459460
```
@@ -464,22 +465,22 @@ Field.formula({
464465
// Full name
465466
Field.formula({
466467
label: 'Full Name',
467-
type: 'text',
468-
formula: 'record.first_name + " " + record.last_name',
468+
returnType: 'text',
469+
expression: 'record.first_name + " " + record.last_name',
469470
})
470471

471472
// Uppercase
472473
Field.formula({
473474
label: 'Code',
474-
type: 'text',
475-
formula: 'upper(record.sku)',
475+
returnType: 'text',
476+
expression: 'upper(record.sku)',
476477
})
477478

478479
// Has a company email
479480
Field.formula({
480481
label: 'Internal',
481-
type: 'boolean',
482-
formula: 'endsWith(lower(record.email), "@example.com")',
482+
returnType: 'boolean',
483+
expression: 'endsWith(lower(record.email), "@example.com")',
483484
})
484485
```
485486

@@ -609,8 +610,8 @@ flows: [
609610
// Formula: Lead score
610611
Field.formula({
611612
label: 'Lead Score',
612-
type: 'number',
613-
formula: `
613+
returnType: 'number',
614+
expression: `
614615
(record.rating == "hot" ? 30 : record.rating == "warm" ? 20 : 10) +
615616
(record.annual_revenue > 1000000 ? 25 : 0) +
616617
(record.number_of_employees > 500 ? 20 : 0) +

content/docs/guides/formula.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const Invoice = ObjectSchema.create({
8989
titleFormat: tmpl`Invoice {{record.invoice_no}} – {{record.customer.name}}`,
9090
fields: {
9191
total: Field.formula({
92-
type: 'currency',
92+
returnType: 'number',
9393
expression: F`record.subtotal + record.subtotal * record.tax_rate`,
9494
}),
9595
po_number: Field.text({
@@ -103,6 +103,18 @@ export const Invoice = ObjectSchema.create({
103103
});
104104
```
105105

106+
<Callout type="warn">
107+
**A formula field's `type` is always `formula`** — that is the key the runtime
108+
evaluates (`objectql` computes a formula virtual field from its `expression`
109+
only). Do **not** pass `type: 'currency' | 'number' | …` to `Field.formula` — it
110+
is rejected by the typed `FieldInput` and, untyped, would override `type:'formula'`
111+
so the field silently never computes. To declare what the formula returns, set
112+
**`returnType`** (`'number' | 'text' | 'boolean' | 'date'`); it is inferred and
113+
stamped automatically by the AI build path, and consumers (dashboard measures,
114+
formatting) read it instead of re-parsing the expression. The CEL source is the
115+
**`expression`** key (the `formula` alias is normalized to it on save).
116+
</Callout>
117+
106118
The three CEL tagged-template helpers — `cel`, `F` (formula alias), `P`
107119
(predicate alias) — are interchangeable; pick whichever reads best at the
108120
call site. `cron` and `tmpl` produce their respective envelopes.

0 commit comments

Comments
 (0)