ObjectStackObjectStack

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?

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 signalEffect on the create form
required: truemust appear on create
readonly / formula / rollup / autonumber / system-stampednever on create (you can't set it)
defaultValue presentcan be omitted from create (it self-fills)
hiddenoff everywhere by default
groupwhich section the field belongs to (semantic, travels with the model)
declaration orderthe 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 form

Welding 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 needUse
Create asks fewer fields (required + a few core)Pure derivation — don't hand-write
Create groups differently but is just a smaller subsetUsually still derivation (a 5-field form needs no sections)
Create is a wizard / multi-step, has create-only copy, conditional reveal, bespoke layoutHand-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

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.create to drop fields. That's derivation's job — reserve the override for genuine layout/flow divergence.

See also

On this page