Found while implementing #4047 (PR #4068). Filed unassigned; not fixed there, because it needs a decision the card did not cover.
What happens
@objectstack/spec lets a field's defaultValue be a runtime instruction rather than a value — the DEFAULT_VALUE_TOKENS family ('NOW()', 'current_user') or a CEL Expression envelope { dialect, source }. The server resolves those per insert, in ObjectQL.applyFieldDefaults, for any field that arrives absent or null.
A create form therefore cannot seed them: putting the literal text NOW() into a datetime input and submitting it as the field's value suppresses the very resolution the declaration asked for (applyFieldDefaults only fills fields that arrive empty). PR #4068 makes every object-form container leave such fields empty for exactly that reason.
That is correct for an optional field. Combined with required: true it is not:
remind_at: Field.datetime({ required: true, defaultValue: 'NOW()' }),
The control opens empty, the client-side required rule refuses the submit, and there is nothing sensible for the user to type — the declaration already said what the value is, and the server would have supplied it had the field simply been omitted. This is the same shape as #4047 (required + a default producing the worst create experience of any modelling choice), one layer down: #4047 covered the static half, this is the runtime half.
Why it is not just "seed it client-side"
Resolving these in the browser means evaluating CEL, and reproducing the server's clock / actor / timezone snapshot, in a second place. Two implementations of one contract is how they drift. So the fix is a design choice, not a patch, which is why it is filed rather than folded into #4068.
Rough options, for whoever picks this up:
- A. Suppress the client-side
required rule when the field declares a runtime defaultValue and the form is in create mode. The field is not really "missing" — the producer guarantees a value at insert. Cheap and local; the control still reads oddly (a required marker over an empty input the user is allowed to skip).
- B. Render a resolved preview value, non-authoritative — show "now" for
NOW(), the acting user for current_user — and still omit the field from the payload so the server resolves it. Reads best; needs a display-only path that is not confusable with a real value, and needs the actor/clock, which the form does not have today.
- C. Have the server serve resolved defaults with the object schema (a per-request
resolvedDefaults alongside fields), so the form seeds real values from the one authority. Cleanest contract, largest surface — an API change in the framework repo.
- D. Rule it an authoring error and reject
required: true + a runtime defaultValue at publish/lint time.
Not measured how many real declarations hit this: defaultValue: 'NOW()' is common on platform objects, but those columns are typically not required in the authored sense. Severity is for triage to grade — filing it plainly rather than sitting on it.
Reproduce
packages/plugin-form/src/createDefaults.test.tsx (added in #4068) already renders the runtime-token case and asserts the control is empty; make that field required: true and attempt a submit.
Generated by Claude Code
Found while implementing #4047 (PR #4068). Filed unassigned; not fixed there, because it needs a decision the card did not cover.
What happens
@objectstack/speclets a field'sdefaultValuebe a runtime instruction rather than a value — theDEFAULT_VALUE_TOKENSfamily ('NOW()','current_user') or a CEL Expression envelope{ dialect, source }. The server resolves those per insert, inObjectQL.applyFieldDefaults, for any field that arrives absent or null.A create form therefore cannot seed them: putting the literal text
NOW()into a datetime input and submitting it as the field's value suppresses the very resolution the declaration asked for (applyFieldDefaultsonly fills fields that arrive empty). PR #4068 makes every object-form container leave such fields empty for exactly that reason.That is correct for an optional field. Combined with
required: trueit is not:The control opens empty, the client-side required rule refuses the submit, and there is nothing sensible for the user to type — the declaration already said what the value is, and the server would have supplied it had the field simply been omitted. This is the same shape as #4047 (
required+ a default producing the worst create experience of any modelling choice), one layer down: #4047 covered the static half, this is the runtime half.Why it is not just "seed it client-side"
Resolving these in the browser means evaluating CEL, and reproducing the server's clock / actor / timezone snapshot, in a second place. Two implementations of one contract is how they drift. So the fix is a design choice, not a patch, which is why it is filed rather than folded into #4068.
Rough options, for whoever picks this up:
requiredrule when the field declares a runtimedefaultValueand the form is in create mode. The field is not really "missing" — the producer guarantees a value at insert. Cheap and local; the control still reads oddly (a required marker over an empty input the user is allowed to skip).NOW(), the acting user forcurrent_user— and still omit the field from the payload so the server resolves it. Reads best; needs a display-only path that is not confusable with a real value, and needs the actor/clock, which the form does not have today.resolvedDefaultsalongsidefields), so the form seeds real values from the one authority. Cleanest contract, largest surface — an API change in the framework repo.required: true+ a runtimedefaultValueat publish/lint time.Not measured how many real declarations hit this:
defaultValue: 'NOW()'is common on platform objects, but those columns are typically notrequiredin the authored sense. Severity is for triage to grade — filing it plainly rather than sitting on it.Reproduce
packages/plugin-form/src/createDefaults.test.tsx(added in #4068) already renders the runtime-token case and asserts the control is empty; make that fieldrequired: trueand attempt a submit.Generated by Claude Code