Create form ≠ edit form
The new-record form asks 5 fields; the full edit form shows 40 grouped into sections. Derive both from one flat field set; only hand-shape the create form when layout or flow genuinely diverges.
Create form ≠ edit form
Scenario
The form for creating a record should ask only a handful of fields (the essentials). The form for editing an existing record shows the full record, grouped into sections. How do I model this without maintaining two field lists that drift apart?
Recommended solution
Do not author two forms. Author one field set with enough intent that both forms derive from it — then override only when you must.
1. Put the intent on the fields
The create-form subset is not an arbitrary pick; it is derivable from signals that already live on each field:
| Field signal | Effect on the create form |
|---|---|
required: true | must appear on create |
readonly / formula / rollup / autonumber / system-stamped | never on create (you can't set it) |
defaultValue present | can be omitted from create (it self-fills) |
hidden | off everywhere by default |
group | which section the field belongs to (semantic, travels with the model) |
| declaration order | the order you write fields in is the default order everywhere — there is no field.order |
So a sensible create form is: editable, required-or-core fields, in declaration order — and it falls out of the object. This is ADR-0047's guardrail: omission is correct — emit nothing extra and you still get a complete, correct form.
2. The default (edit) form derives from field.group
The full edit form materialises each field.group into a section. You can omit it entirely and let the platform derive an equivalent grouped form. When you do write it, list fields as bare strings so each one inherits its type / validation / FLS / default from the object — the form carries layout only, never data semantics.
3. Hand-shape the create form only when layout or flow diverges
The escape hatch is a named form view bound to the create entry point. Author it as a sparse override — mostly a list of field names — not a from-scratch restatement:
import { defineView } from '@objectstack/spec';
const data = { provider: 'object' as const, object: 'showcase_contact' };
export const ContactViews = defineView({
list: {
type: 'grid', data,
columns: [{ field: 'name' }, { field: 'email' }, { field: 'company' }, { field: 'stage' }],
// Bind the "+ Add record" entry point to the slim create form:
addRecord: { enabled: true, mode: 'form', formView: 'create' },
},
// Full edit form — grouped by field.group; bare strings inherit field defs.
form: {
type: 'simple', data,
sections: [
{ name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] },
{ name: 'work', label: 'Work', columns: 2, fields: ['company', 'title'] },
{ name: 'status', label: 'Status', columns: 2, fields: ['stage', 'lead_score'] },
{ name: 'notes', label: 'Notes', columns: 1, fields: ['notes'] },
],
},
formViews: {
// Sparse create override: only the core fields, one section. Omits
// lead_score (readonly), stage (defaulted), notes (edit-time only).
create: {
type: 'simple', data, title: 'New contact',
sections: [
{ label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] },
],
},
},
});The binding is addRecord.mode: 'form' + addRecord.formView: 'create' (AddRecordConfigSchema). No formViews.create → the create entry derives a default. Present → it wins for create.
Why
Three layers, each derive + only store differences — never "re-list all 40 fields":
1. Derived default derive(object, 'create' | 'edit') ← free, no authoring
2. Author override formViews.create (sparse patch) ← this recipe; only on real divergence
3. Tenant override org overlay delta (ADR-0005) ← a single org wants its own formWelding two independent full forms is the Salesforce page-layout tax: add a required field, forget the create form → runtime "missing required field" on create; rename a field → silent drift. Keeping data semantics on the object (never on the form) means a form can only ever drift on which fields appear — a flat name list that reference-integrity diagnostics catch as a hard failure in the AI loop (ADR-0047 §3.5, ADR-0033). That guardrail is what makes the escape hatch safe to hand to an AI author.
The create fields are a prefix/subset of the edit fields in the same order and groups, so "quick-create 4 fields → save → land on the full record" stays visually continuous. Derivation preserves that for free; two hand-authored forms break it.
When to actually reach for the override
| Your real need | Use |
|---|---|
| Create asks fewer fields (required + a few core) | Pure derivation — don't hand-write |
| Create groups differently but is just a smaller subset | Usually still derivation (a 5-field form needs no sections) |
| Create is a wizard / multi-step, has create-only copy, conditional reveal, bespoke layout | Hand-write formViews.create |
Rule of thumb: "different field subset" → derive. "different layout or flow" → override. Writing a full create form just to drop a few fields walks straight back into the two-artifact drift trap.
Runnable example
- Object:
examples/app-showcase/src/objects/contact.object.ts— flat, grouped, intent-tagged field set. - Views:
examples/app-showcase/src/views/contact.view.ts— full edit form + sparseformViews.create+addRecordbinding.
Anti-patterns
- Two full hand-authored field lists (
contact_create_form+contact_edit_form). Guaranteed drift; the create form silently misses new required fields. - Restating field type / validation / options on the form. Data semantics live on the object only; the form chooses which fields and where.
- Reaching for
formViews.createto drop fields. That's derivation's job — reserve the override for genuine layout/flow divergence.
See also
- Decision record: ADR-0047 (object UI run modes — derive-default + override).
- Field grouping & order — the three different meanings of "group".
- Reference: View / FormView schema, Field schema.