lead_time_days, an integer) to products and wires it through every surface the dashboard has:
- edited on the product form, saved by the form’s own Save button
- a column in the products table
- a filter (“lead time > 7 days”) and a sort (“slowest first”) in the list toolbar
First: should this be a custom field instead?
Default to a custom field definition. It needs zero code — declare the field, and the product form renders and saves it. That covers most plugin data and everything merchants define themselves. Graduate to a real database column only when the attribute needs list operations or database guarantees:| You need… | Custom field | Real column |
|---|---|---|
| Editing on the product form | ✅ zero code | ✅ this recipe |
| Filtering / sorting the products list in Admin | ❌ | ✅ via Ransack |
| Merchants defining the field themselves at runtime | ✅ | ❌ |
1. Backend
Four small pieces — each one unlocks a specific frontend capability. In a host app these live inbackend/ (or api/); in a distributed plugin, in the Rails engine half.
The column (with an index — you’re adding it because it will be queried):
validates :lead_time_days, numericality: { greater_than_or_equal_to: 0, allow_nil: true }) — it’s the authoritative validation, and the dashboard renders its 422 message inline on the field automatically.
2. Frontend — one declaration
<form> of its own, no save button:
3. Verify it
API first — the fastest way to confirm the backend pieces:How the pieces map
| Frontend behavior | Made possible by |
|---|---|
| Form input hydrates + saves with the page | serializer attribute + permitted param + formFields/useHostForm |
| Table cell renders | serializer attribute + ColumnDef.render |
| Header sort works | sortable: true + Ransack allowlist |
| Filter panel entry works | filterable: true + filterType + Ransack allowlist |
| Inline validation message | the Rails validation (422 → mapped onto the field) |
ColumnDef extras worth knowing: ransackAttribute points the predicate somewhere other than the column key (the built-in sku column filters through master_sku), and displayable: false makes a filter-only field that never renders as a column.
Reference
- Custom form field recipe — the form layer in isolation, and the custom-field-definition path
- Tables — the full
ColumnDefand table registry API - Backend integration — serializers, permitted params, error mapping
- Decorators — the
prependpattern used for the controller

