Overview
Custom fields let an admin attach workspace-specific metadata to any of Vigilo's core entities — changes, incidents, tasks, assets, and projects — without modifying the schema or shipping a release. A field is declared once (the FieldDefinition), populated per record (the FieldValue), and tracked over time (the FieldValueHistory). Field-level read/write permissions let admins expose internal-only fields without leaking them to broader audiences, and computed fields run safe-eval expressions over other fields and entity data.
Custom fields live under Settings → General → Custom fields. The configured fields render on the create dialog, the edit dialog (where applicable), and the entity's detail panel — automatically wired up for each of the five supported entity types. Edit dialogs pre-fill with the entity's saved values; new entities persist their custom field values immediately after creation.
Why it exists
Every customer has a small handful of fields they want on every change, every incident, every asset that don't fit the universal model: a compliance code, a customer ID, a cost-centre, a vendor name. Adding these fields to the core schema would bloat the product and force every customer to see them; storing them as freeform tags loses the type information. The FieldDefinition / FieldValue split gives each workspace its own custom schema with type safety, history, permissions, and computed expressions, while keeping the shared core schema lean.
Key concepts
FieldDefinition
FieldDefinition (workspace, entity_type, key, label, field_type, is_required, default_value, options (json), formula (string, nullable), readable_by (json list of roles), writable_by (json list of roles), display_order, is_active).
- entity_type — one of
change | incident | task | asset | project. Selects which entity's create / edit / detail surfaces render the field. The five supported types correspond to the most common per-workspace metadata needs; new entity types can be added in a future release. - key — slug-form unique identifier (
cost_centre,vendor_id). Used in API payloads. - label — human-readable form label.
- field_type — see Supported types below.
- options — for dropdowns, the choice list
[{value, label}]. - formula — for computed fields, an expression in the safe-eval DSL.
- readable_by / writable_by — arrays of role names. Empty = visible to all members.
FieldValue
FieldValue (field_definition, entity_type, entity_id, value (json)). One row per (field, entity). The value column is JSONB so type-specific serialisation is handled at write time (numbers as numbers, dates as ISO-8601 strings, dropdowns as the option value). Reading a field renders the value through the type's React component (number → formatted number, date → date picker, dropdown → label lookup).
FieldValueHistory (audit)
Every write to a FieldValue appends a row to FieldValueHistory (WB.33: field_value, old_value, new_value, changed_by, changed_at, change_reason). The entity detail page exposes a Field history drawer that renders the timeline per-field — who changed what, when, and why. Required by SOC 2 evidence collection.
Computed formulas (safe-eval)
Set formula to declare the field as computed — its value is derived from other fields on the entity (and a small set of safe globals: now(), today(), basic arithmetic, string ops) rather than written by a user. Example: a "days_open" field on incident with formula (now() - opened_at).days.
The safe-eval sandbox forbids attribute access (__class__, __import__), function definitions, imports, and any non-allowlisted name. Formulas are evaluated on read (cached per request) so updates are immediate.
Computed fields have writable_by=[] enforced; the API rejects any write attempt with a 400 computed_field_readonly error.
Field-level permissions
readable_by and writable_by accept a list of role names (or custom-role names). The API and UI both gate per-field:
- If the caller's role is not in
readable_by, the field is omitted from the API response and hidden from the UI entirely (not "visible but greyed"). - If the caller's role is not in
writable_by, the field renders read-only with a tooltip.
This enables sensitive-field patterns: a vendor_secret_code field readable only by owner, a compliance_notes field writable only by approver and above.
Supported types
- text — single-line text input.
max_lengthconfigurable inoptions.max_length. - textarea — multi-line, no markdown, max 4 000 chars.
- number — float or int (controlled by
options.integer). - boolean — checkbox.
- date — ISO-8601 date (no time component).
- datetime — ISO-8601 datetime, UTC stored, local rendered.
- dropdown — single-select from
options.choices. - multi_select — multi-select from
options.choices. - url — URL input with validation.
- user_ref — picker bound to current workspace members.
- datepicker_with_holidays — example plugin-supplied type that overlays a regional holiday calendar; useful for "planned cutover date" fields where the picker should warn on holidays. Demonstrates how plugins can extend the supported-type list via the FieldType SDK.
Common workflows
Define a new field
- Settings → Custom fields → New field.
- Pick entity type (e.g.
change_request), enter key (cost_centre) and label. - Pick type (
dropdown), enter options (["EUR-OPS", "US-OPS", "APAC-OPS"]). - Set required, default value, permissions, display order.
- Click Save. The field appears on every change detail page in its order slot.
Backfill existing records
For non-required fields with a default value, existing records render the default until written. For required fields, the system surfaces a banner on existing records — "Required field cost_centre missing" — and blocks state transitions (e.g. submit) until populated.
Add a computed field
- Define a field of type
number, setformula = "(now() - opened_at).total_seconds() / 3600". - Save. The field renders on every incident with the live computed value.
- Attempting to PATCH the field via API returns
400 computed_field_readonly.
View field history
- Open the entity detail page, scroll to the Custom fields section.
- Click the clock icon next to a field. The drawer slides in with the full FieldValueHistory timeline.
Permissions
- Owners and admins can create, edit, and deactivate field definitions.
- All members can read fields they're in
readable_byfor, and write fields they're inwritable_byfor. Empty list means "everyone". - Engineers and above can read FieldValueHistory; viewers cannot.
Troubleshooting
Field does not appear on the entity page — Likely the user's role is not in readable_by. Check the field definition. If the list is empty, the field is universally visible — clear browser cache.
Formula throws "name 'foo' is not defined" — The formula references a field key that doesn't exist on the entity. Double-check the key matches.
Field history is empty after multiple writes — Confirm the writes went through the API (PATCH /api/v1/{entity}/{id}/) and not via direct DB. Direct DB updates do not append history.
Plugin-supplied type ("datepicker_with_holidays") missing from picker — The plugin must be enabled for the workspace under Settings → Integrations → Plugins and the React bundle loaded. Reload the page after enabling.
Required field blocks state transition — Populate the field. If the field was added after the entity was created, the API returns 400 required_field_missing on transition; users must update the field first.
Computed field shows stale value — Computed fields are evaluated on read with a request-scoped cache; navigate away and back to recompute. Server-side caches do not persist across requests.