User Defined Fields (UDF)
You can create your own fields in most Kompass modules: Clients, Contacts, Projects, Proposal Items, Tasks, Invoices, Payments, Expenses, Leave, Equipment, Suppliers and Employees. In the Projects module, you can find the extra fields in the Overview tab, beneath the map.

User defined fields (UDF) can only be created in the Admin site. Normally, your Kompass Champion has access to the Admin site.
To create your fields, you first need to create a UDF schema for the module. Head to the relevant UDF schema (for example Project UDF Schema) (1) and click Add UDF schema (2).

Add a name and then set the schema.

Please note that once you have create your UDF schema, you will have to allocate it to your org. Head to your org page and select the schema at the relevant module.
Understanding the Schema Definition
You can see the basic structure of a UDF schema below:
{
"type": "object",
"properties": {
"field_name": {
"type": [
"string",
"null"
],
"label": "Field Label",
"default": null,
"x-order": 1
}
}
}
A few things to know:
- Fields are displayed in the order set by
x-order(lowest first). Fields withoutx-orderare displayed last. - A schema only accepts the fields it defines. You can't add values for fields that aren't in it.
- If you remove a field from a schema, or move a record to a different schema, any values stored for fields that no longer exist are dropped the next time the record is saved. The old values remain in the record's history.
Field Properties
These standard properties can be used on any field:
| Property | Description | Example |
|---|---|---|
type |
Field data type (see Field Types) | "string" , ["string", "null"] |
label |
Display label shown in the UI | "Company Name" |
default |
Default value when the field is empty | null , "" , 0 , false |
enum |
List of allowed values (creates a dropdown) | ["Active", "Inactive", "Pending"] |
There are also custom extensions, all starting with x- :
| Property | Description | Applies to | Example |
|---|---|---|---|
x-order |
Display order (lowest first) | All fields | 1 , 2 , 3 |
x-cols |
Field width in a 12-column grid, 1–12 (default 4) | All fields | 3 , 6 , 12 |
x-widget |
Override the default widget (see Widgets) | Dates, entity references, dividers, calculated fields | "date" , "client" |
x-depends-on |
Parent field; the field clears when the parent changes | Required for contact , quote_item , task |
"client_id" , "project_id" |
x-module |
Module context for tags | Required for the tags widget |
"clients" , "projects" |
x-multi-line |
Turns a text field into a text area | String fields (required if x-rows is set) |
true , false |
x-rows |
Number of text area rows | String fields (requires x-multi-line: true ) |
3 , 5 , 10 |
x-decimal-places |
Fixed decimal precision, 0–10 | Number fields | 2 , 3 , 0 |
x-format |
How a number is displayed, e.g. as currency | Number fields | "currency" |
x-enum-by |
Narrows a dropdown based on another field's value | Fields with enum |
see below |
x-default-from |
Starts a new record with a value taken from a related record | Task, project and Proposal Item schemas | "project.deadline" |
x-min-from |
Earliest date or lowest number allowed, taken from a related record or another field | Date and number fields on task, project and Proposal Item schemas | see below |
x-max-from |
Latest date or highest number allowed, taken from a related record or another field | Date and number fields on task, project and Proposal Item schemas | "project.deadline" |
x-enum-string-partial |
Lets searches on a dropdown match part of a value | String fields with enum |
true |
x-template |
Jinja2 template for a calculated value | Required for the calculated widget |
"{{ quote_item.price_total }}" |
x-realtime |
Recompute a calculated field live while editing (default false ) |
calculated widget, Proposal Items only |
true |
Field Width (x-cols )
Fields are laid out on a 12-column grid. By default each field takes 4 columns, so three fields sit on each row. Use x-cols to change a field's width:
x-cols |
Result |
|---|---|
3 |
Four fields per row |
4 (default) |
Three fields per row |
6 |
Two fields per row |
12 |
Full width |
{
"type": [
"number",
"null"
],
"label": "Target Multiplier",
"x-cols": 3,
"default": null
}
x-cols only affects layout and works on any field. Dividers are always full width.
Field Types
1. Text Field
This is a standard string field used for capturing text, such as notes or simple descriptions. You can allow it to be left empty by setting the type to ["string", "null"] .
- Example use: A "Plot" description.
- Code definition:
"plot": {
"type": "string",
"label": "Plot",
"x-order": 1,
"default": null
}
- Code definition (with null allowed):
"plot": {
"type": [
"string",
"null"
],
"label": "Plot",
"x-order": 1,
"default": null
}
2. Checkbox (Yes/No)
This is a boolean field that appears as a simple checkbox. It's ideal for tracking true/false or yes/no information.
- Example use: A "RAMS Sent" checkbox.
- Code definition:
"rams": {
"type": "boolean",
"label": "RAMS Sent",
"default": false
}
Note: Checkboxes can't be empty (null ). Always use false as the default.
3. Dropdown List
This field allows users to select a single option from a predefined list. Use the enum property to list the available options. Including null in the enum list makes the field optional.
- Example use: A "State Area" field.
- Code definition:
"section": {
"enum": [
"Central",
"East",
"North",
"South",
"West",
null
],
"type": [
"string",
"null"
],
"label": "State Area",
"x-order": 2,
"default": null
}
To make the list change depending on another field, see Dropdowns Filtered by Another Field.
4. Date Field
This field provides a calendar picker. By setting the format to date , you ensure the input is handled as a date.
- Example use: An "Archive Date".
- Code definition:
"archive_date": {
"type": [
"string",
"null"
],
"format": "date",
"label": "Archive Date",
"x-order": 3,
"x-widget": "date",
"default": null
}
5. Number Field
This field is used for entering numerical data only. You can set a default value, such as 0 .
- Example use: An "Acres" field.
- Code definition:
"acres": {
"type": [
"number",
"null"
],
"label": "Acres",
"x-order": 4,
"default": 0
}
6. Decimal Field (fixed precision)
Add x-decimal-places to a number field to fix how many decimal places it holds.
"unit_price": {
"type": [
"number",
"null"
],
"label": "Unit Price",
"default": null,
"x-decimal-places": 2
}
x-decimal-placestakes a whole number from 0 to 10.- Values are rounded as they're typed and again when saved.
- Read-only views show the fixed precision, so
1.2with 2 decimal places shows as1.20. x-decimal-places: 0enforces whole numbers while keeping thenumbertype.
7. Formatted Number (x-format )
x-decimal-places controls precision. x-format controls presentation: thousands separators, currency symbols and percentages. With both set, 1234.5 can show as £1,234.50 .
"agreed_fee": {
"type": [
"number",
"null"
],
"label": "Agreed Fee",
"default": null,
"x-decimal-places": 2,
"x-format": "currency"
}
Shorthand formats. The simplest option is one of three words, which follow your org's locale (so the currency symbol and separators match your region):
x-format |
Displays as |
|---|---|
"currency" |
Your locale's currency, e.g. £1,234.50 |
"decimal" |
A number with thousands separators, e.g. 1,234.5 |
"percent" |
A percentage (see the note below) |
Custom formats. For anything else, give x-format an object of options instead:
// Always euros, regardless of your locale
"x-format": { "style": "currency", "currency": "EUR" }
// Thousands separators, no decimals
"x-format": { "style": "decimal", "maximumFractionDigits": 0 }
// Compact: 1.2m
"x-format": { "style": "decimal", "notation": "compact" }
Supported options: style , currency , currencyDisplay , unit , unitDisplay , notation , compactDisplay , signDisplay , useGrouping , minimumIntegerDigits , minimumFractionDigits , maximumFractionDigits . Any other option is rejected when you save the schema.
Percentages: watch the ×100. "percent" multiplies the stored value by 100, so it suits a field that stores 0.15 for 15%. If your field stores 15 , use { "style": "unit", "unit": "percent" } instead, which adds the % sign without multiplying.
Good to know:
x-formatonly works on number fields.- It only changes how the value is displayed. The value is still stored as a plain number, so searching, filtering and calculated fields are unaffected.
- Formatting shows in read-only views. In edit mode you type the raw number. Calculated fields are always read-only, so they always show formatted.
- With
"currency", the number of decimals shown comes from the currency (two for GBP), not fromx-decimal-places.
Making Fields Required/Optional
Required Fields
To make a field mandatory, list it in the schema's required array:
{
"type": "object",
"properties": {
"company_name": {
"type": "string",
"label": "Company Name"
}
},
"required": [
"company_name"
]
}
Optional Fields (Nullable)
To allow empty values, use an array type that includes "null" :
{
"type": [
"string",
"null"
],
"label": "Optional Field",
"default": null
}
Examples:
// Optional string
{
"type": [
"string",
"null"
],
"label": "Middle Name",
"default": null
}
// Optional integer
{
"type": [
"integer",
"null"
],
"label": "Employee Number",
"default": null
}
// Optional number
{
"type": [
"number",
"null"
],
"label": "Discount Rate",
"default": null
}
UI behaviour:
- Fields with
"null"in the type array show a clear button (×) in edit mode - Clearing the field sets the value to
null - Empty text in nullable fields is automatically converted to
null
Widgets
Widgets determine how fields are rendered in the UI. Most widgets are selected automatically based on the field type. The x-widget property is only needed for dates, dividers, calculated fields and entity selectors.
Automatic Widget Selection
| Type | Condition | Rendered as |
|---|---|---|
string |
Has enum |
Dropdown |
string |
No enum |
Text field |
integer |
- | Number field |
number |
- | Number field |
boolean |
- | Checkbox |
Text field options:
x-multi-line: turns the field into a text area (true/false). Iftrueandx-rowsisn't set, it defaults to 1 row.x-rows: number of rows in the text area (requiresx-multi-line: true).
Examples:
// Simple text field
{
"type": "string",
"label": "Name"
}
// Text area
{
"type": "string",
"label": "Description",
"x-multi-line": true,
"x-rows": 5
}
// Dropdown (enum)
{
"type": "string",
"label": "Status",
"enum": [
"Active",
"Inactive"
]
}
// Number field
{
"type": "integer",
"label": "Count"
}
// Checkbox
{
"type": "boolean",
"label": "Enabled"
}
Special Widgets (via x-widget)
Entity selectors must be set explicitly and use the integer type (or array for tags).
Date Widget
{
"type": [
"string",
"null"
],
"label": "Due Date",
"x-widget": "date",
"default": null
}
Divider Widget
Renders a horizontal divider line across the full width. Useful for visually grouping fields.
{
"type": "null",
"x-widget": "divider",
"default": null
}
With a text label:
{
"type": "null",
"x-widget": "divider",
"label": "Personal Information",
"default": null
}
Note: Dividers don't store data. Use the label property to display text in the middle of the line.
Client Widget
{
"type": [
"integer",
"null"
],
"label": "Client",
"x-widget": "client",
"default": null
}
Contact Widget
Requires: x-depends-on (the field holding the client ID)
{
"type": [
"integer",
"null"
],
"label": "Contact",
"x-widget": "contact",
"x-depends-on": "client_id",
"default": null
}
Filters contacts by the selected client.
Project Widget
{
"type": [
"integer",
"null"
],
"label": "Project",
"x-widget": "project",
"default": null
}
Proposal Item Widget
Requires: x-depends-on (the field holding the project ID)
{
"type": [
"integer",
"null"
],
"label": "Proposal Item",
"x-widget": "quote_item",
"x-depends-on": "project_id",
"default": null
}
Filters Proposal Items by the selected project.
Task Widget
Requires: x-depends-on (the field holding the project ID or Proposal Item ID)
{
"type": [
"integer",
"null"
],
"label": "Task",
"x-widget": "task",
"x-depends-on": "project_id",
"default": null
}
Filters tasks by the selected project or Proposal Item. The x-depends-on value can be either "project_id" or "quote_item_id" .
Supplier Widget
{
"type": [
"integer",
"null"
],
"label": "Supplier",
"x-widget": "supplier",
"default": null
}
User Widget
{
"type": [
"integer",
"null"
],
"label": "Assigned To",
"x-widget": "user",
"default": null
}
Tags Widget
{
"type": [
"array",
"null"
],
"label": "Tags",
"x-widget": "tags",
"x-module": "clients",
"default": null
}
Options:
x-module: module context for tags (e.g."clients","projects","tasks")
Using Entity Fields in Document Templates
Entity selectors (client , contact , project , quote_item , supplier , task , user ) only store the selected record's ID. That's not much use on a printed document, so document templates also receive a udf_obj alongside udf , with the same field names mapped to the selected record:
{
"udf": {"qi_managed_by_id": 3},
"udf_obj": {"qi_managed_by_id": {"id": 3, "name": "Somebody Smith"}}
}
Each record has an id and a name ; users also have an email . In a template, write:
{{ udf_obj.qi_managed_by_id.name }}
If the field is empty, or the record has been deleted, the value is empty. udf_obj is available in project and invoice document templates, including the nested project, client, billing client and Proposal Item lists.
Date fields also arrive in document templates as proper dates, so you can format them, for example {{ udf.archive_date.strftime("%d/%m/%Y") }} .
Calculated Fields
A calculated field displays a read-only value that Kompass works out for you from a formula, rather than something a user types in. Use one for figures derived from the record's own data — an effective multiplier, a net revenue total, a variance against target — that should always stay in step with the underlying numbers.
Calculated fields are available on Projects and Proposal Items, where the record carries the financial data a formula can draw on.
How they work
A calculated field uses the calculated widget and carries a Jinja2 template in the x-template property. When the record is saved, Kompass renders that template against the record's data and stores the result. The user sees the value but can't edit it — anything a user (or an import) tries to submit for a calculated field is ignored.
{
"effective_multiplier": {
"type": [
"number",
"null"
],
"label": "Effective Multiplier",
"x-widget": "calculated",
"x-template": "{{ (quote_item.price_total - quote_item.costs_budget_non_staff) / quote_item.costs_budget_staff }}",
"x-decimal-places": 2,
"x-realtime": true,
"default": null
}
}
Rules for a calculated field
A calculated field has a few requirements, all enforced when you save the schema:
x-widgetmust becalculatedandx-templatemust be a non-empty, valid Jinja2 template.- The type must be
numberorstring, and it must include"null", with adefaultofnull. A calculation can fail — a division by zero, or referenced data that isn't there yet on a draft — and when it does, Kompass storesnullrather than an error. - A calculated field cannot be listed in
required. The value is computed by the server, never submitted, so a required calculated field would fail every save. - Number results are rounded to
x-decimal-places(if set). Text results are used exactly as rendered; a result that is only blank space is stored asnull. - Add
x-formatto display a number result as currency, a percentage and so on.
When they recalculate
- Proposal Items: whenever the Proposal Item is saved.
- Projects: whenever the project is saved, and whenever one of its Proposal Items changes, so a project total can never fall behind the items it's built from.
Live preview: x-realtime (Proposal Items only)
By default, a calculated field recomputes only when the record is saved.
On a Proposal Item, add "x-realtime": true to have the field recompute live as the user types, before saving — useful for a figure that depends only on fields on the edit form in front of them (price, quantity, and so on).
Leave x-realtime off for figures that depend on data outside the edit form — invoices, payments, or other Proposal Items. Those can't be worked out from the unsaved form alone, so they refresh on save.
Not on projects. x-realtime is rejected on project schemas. A project's figures roll up from its Proposal Items, which aren't part of the project's edit form, so a live preview would show zeros and then jump on save. Project calculated fields compute on save.
Whether realtime or not, every calculated field is recomputed and stored on save.
What you can reference on a Proposal Item
On a Proposal Item, the template can reference a quote_item object, including:
| Figure | What it is |
|---|---|
price_total |
The item's total price |
costs_budget_total |
Total budgeted cost for one unit |
costs_planned |
Total budgeted cost for the whole item |
costs_budget_staff / costs_budget_non_staff |
Staff and non-staff cost for the whole item (together they make costs_planned ) |
costs_budget_staff_raw / costs_budget_non_staff_raw |
The same split for one unit (together they make costs_budget_total ) |
costs_multiplier |
The exact factor the per-unit costs are scaled by |
hours_budget / days_budget |
Budgeted staff hours and days for the whole item |
hours_budget_raw / days_budget_raw |
The same for one unit |
gross_profit , gross_margin |
Profit and margin |
value , invoiced_net , paid_net , work_in_progress |
Valuation, invoicing and payment figures |
It also carries breakdowns of cost and price by staff/non-staff, by cost category and by nominal account (for example price_by_staff , price_by_category , costs_budget_by_category , costs_by_nominal_account ), and a costs list with one row per cost line.
Note: On a Proposal Item, the parent project is available but is descriptive only — it carries id , name , details , notes , client , billing_client , contact , requested_by , quoted_by , managed_by and tags , but deliberately not the project's financial totals. For financial calculations on a Proposal Item, always use the item's own quote_item.* values.
What you can reference on a Project
On a Project, the template can reference a project object that does carry rolled-up figures: price_computed (the project's total price), costs_budget_staff , costs_budget_non_staff , days_budget and price_total_by_staff . For example, the project version of the effective multiplier is:
{
"type": [
"number",
"null"
],
"label": "Effective Multiplier",
"x-widget": "calculated",
"x-template": "{{ (project.price_computed - project.costs_budget_non_staff) / project.costs_budget_staff }}",
"x-decimal-places": 2,
"default": null
}
Which Proposal Items count in these totals? Before the project is accepted, every item that isn't optional or hidden from the proposal. Once the project is accepted, only the accepted items. So an optional item nobody bought adds neither cost nor price, and after acceptance the figures narrow to what was actually sold.
The project also has a quote_items list with each item's own figures. This list is not filtered, so a template that loops over it includes optional and excluded items too.
Referencing other fields
- Another UDF field: use
udf.other_field, so one calculated field can build on another. Kompass automatically works out the right order to calculate them in. (A circular reference — two fields that depend on each other — can't be resolved and computes tonull.) - The previously-saved values: when editing an existing record, the template can compare the current value against what was saved.
savedmirrors the record's own values as they were before this edit (guard it with{% if saved %}, as it'snullfor a brand-new record), andchangesis a list of the fields that differ from the saved version. For example, this gives the price change made in the current edit:
{{ quote_item.price_total - saved.quote_item.price_total }}
Two limits apply to changes : on a project it never includes UDF values (use saved.udf to compare those), and in the live preview it only covers fields on the edit form.
A note on the staff / non-staff split
Staff costs (costs_budget_staff ) are the budgeted cost of the cost lines flagged as Staff; non-staff costs are the rest. Both cover the whole item, scaled by the same quantity price_total is priced from, so they can sit either side of a division with price_total without a unit mismatch. Use the _raw pair if you want the per-unit figures instead. (See the cost options article for the Staff flag.)
The staff, by-category and by-nominal-account price splits are notional: a Proposal Item has a single price, which Kompass apportions across each dimension in proportion to its share of cost.
Tip: Kompass's figures are exact decimals. To use a fixed decimal number in a formula, convert it with str2dec , for example quote_item.costs_budget_staff - quote_item.days_budget * ('3.35' | str2dec) . A bare 3.35 won't calculate against them.
Limitations
- Calculated fields are read-only and are not searchable or filterable.
- A failed calculation (bad data, division by zero, or a formula error) stores
null— the field simply shows blank until the data supports a result.
Field Dependencies
Fields can depend on other fields using x-depends-on . When the parent field changes, dependent fields are automatically cleared.
Example: Client → Contact
{
"type": "object",
"properties": {
"client_id": {
"type": [
"integer",
"null"
],
"label": "Client",
"x-widget": "client",
"x-order": 1,
"default": null
},
"contact_id": {
"type": [
"integer",
"null"
],
"label": "Contact",
"x-widget": "contact",
"x-depends-on": "client_id",
"x-order": 2,
"default": null
}
}
}
Behaviour:
- The contact selector is filtered by the selected client
- When the client changes, the contact field is cleared
- Dependency chains are supported (A → B → C)
Example: Project → Proposal Item → Task
{
"type": "object",
"properties": {
"project_id": {
"type": [
"integer",
"null"
],
"label": "Project",
"x-widget": "project",
"x-order": 1,
"default": null
},
"quote_item_id": {
"type": [
"integer",
"null"
],
"label": "Proposal Item",
"x-widget": "quote_item",
"x-depends-on": "project_id",
"x-order": 2,
"default": null
},
"task_id": {
"type": [
"integer",
"null"
],
"label": "Task",
"x-widget": "task",
"x-depends-on": "quote_item_id",
"x-order": 3,
"default": null
}
}
}
Dropdowns Filtered by Another Field
x-enum-by limits a dropdown to the options that make sense for another field's current value. For example, a Sub-category list that changes depending on the project's Category:
"sub_category": {
"type": [
"string",
"null"
],
"label": "Sub-category",
"enum": [
null,
"Extension",
"Loft conversion",
"Office fit-out",
"Retail fit-out"
],
"x-enum-by": {
"field": "category.id",
"options": {
"12": ["Extension", "Loft conversion"],
"13": ["Office fit-out", "Retail fit-out"]
}
},
"default": null
}
Here, a project in category 12 is offered "Extension" and "Loft conversion"; a project in category 13 is offered the two fit-out options.
How to set it up:
| Part | What it does |
|---|---|
field |
The field the list depends on. Either another field in the same schema (e.g. "region" ), or, on project schemas only, a project field such as "category.id" , "client.id" or "managed_by.id" . |
options |
Maps each value of that field to the options offered. Write the keys as text, so category ID 12 is "12" . |
enum |
Still holds the full list. Every option listed under options must also appear in enum . |
How it behaves:
- When the parent field is empty, or its value has no entry in
options, the wholeenumlist is offered. - While editing, the dropdown follows the parent immediately. Change the category and the sub-category list changes with it; a value the new category doesn't offer is cleared.
- Saving a value that isn't offered for the parent's current value is rejected.
Check your field name carefully. If field is misspelt, Kompass can't find the parent, so it quietly offers the full list instead of raising an error. After saving the schema, test it on a record to confirm the list narrows as expected.
Tip: To find a category's ID, open the category in the Admin site; the number is in the page address.
Values from Related Records
Some fields work best when they start from, or stay within, a value held on a related record. For example, a task's Due date can start as the project's deadline and must not fall after it:
"due_date": {
"type": [
"string",
"null"
],
"label": "Due date",
"x-widget": "date",
"x-default-from": "project.deadline",
"x-max-from": "project.deadline",
"default": null
}
A new task's due date is filled in with the project's deadline. Staff can change it to an earlier date, but a date after the deadline is rejected.
What each setting does:
| Setting | What it does |
|---|---|
x-default-from |
The starting value for a new record. It is filled in once, when the record is created, and is an ordinary field after that. |
x-min-from |
The earliest date, or lowest number, the field accepts. |
x-max-from |
The latest date, or highest number, the field accepts. |
What they can refer to:
| Schema | Value |
|---|---|
| Task | "project.deadline" (the deadline of the task's project) |
| Proposal Item | "project.deadline" (the deadline of the item's project) |
| Project | "deadline" (the project's own deadline) |
x-min-from and x-max-from can also name another field in the same schema, so one field can limit another:
"budget_max": {
"type": [
"number",
"null"
],
"label": "Maximum budget",
"default": null
},
"budget_agreed": {
"type": [
"number",
"null"
],
"label": "Agreed budget",
"x-max-from": "budget_max",
"default": null
}
If you need a value from somewhere else on the record, contact us and we will check whether it can be used.
How x-default-from behaves:
- It only applies to new records. The form fills the field in as soon as it opens, so you can see the value and change it before saving.
- On a new task, if you pick a different Proposal Item before touching the field, the value follows the new project. Once you have changed the field yourself, it is left alone.
- After the record is saved, the field is not updated again. Moving the project deadline later does not change the due date of existing tasks.
- If the related value is empty, the field starts empty.
- Records created through the API also get the value if the field is left empty, as do tasks and Proposal Items created by a CSV import and repeating task series. Projects created by a CSV import, and records created by an integration sync such as QuickBooks Online or Microsoft Dynamics 365, start with the field empty.
- It cannot be combined with a
defaultother thannull, and it cannot be used on a required field, a checkbox, a calculated field or a divider.
How x-min-from and x-max-from behave:
- They work on date fields (the
datewidget) and on number fields. - The limit itself is allowed: a due date on the deadline day is accepted.
- A value outside the limit is rejected with a message giving the limit. Kompass never changes the value for you.
- If the field or the limit is empty, nothing is checked.
- The check runs when a record is created and whenever the field itself is changed. If the project deadline is later moved earlier, existing tasks can still be saved for other changes; their due date is only checked again when someone edits it.
Check the value you refer to carefully. If it is misspelt, Kompass can't find it, so the field simply starts empty and no limit applies; there is no error. After saving the schema, create a test record to confirm the field fills in and the limit works.
Complete Example
{
"type": "object",
"properties": {
"status": {
"type": "string",
"label": "Status",
"enum": [
"New",
"In Progress",
"Completed",
"On Hold"
],
"default": "New",
"x-order": 1
},
"client_id": {
"type": [
"integer",
"null"
],
"label": "Related Client",
"x-widget": "client",
"x-order": 2,
"default": null
},
"contact_id": {
"type": [
"integer",
"null"
],
"label": "Primary Contact",
"x-widget": "contact",
"x-depends-on": "client_id",
"x-order": 3,
"default": null
},
"priority": {
"type": "integer",
"label": "Priority Level",
"default": 3,
"x-order": 4
},
"budget": {
"type": [
"number",
"null"
],
"label": "Budget",
"default": null,
"x-decimal-places": 2,
"x-format": "currency",
"x-order": 5
},
"due_date": {
"type": [
"string",
"null"
],
"label": "Due Date",
"x-widget": "date",
"x-order": 6,
"default": null
},
"is_billable": {
"type": "boolean",
"label": "Billable",
"default": true,
"x-order": 7
},
"notes": {
"type": [
"string",
"null"
],
"label": "Notes",
"x-multi-line": true,
"x-rows": 5,
"x-cols": 12,
"x-order": 8,
"default": null
},
"assigned_to": {
"type": [
"integer",
"null"
],
"label": "Assigned To",
"x-widget": "user",
"x-order": 9,
"default": null
},
"tags": {
"type": [
"array",
"null"
],
"label": "Tags",
"x-widget": "tags",
"x-module": "projects",
"x-order": 10,
"default": null
}
},
"required": [
"status",
"priority",
"is_billable"
]
}
Validation
Validation on save
All UDF data is checked against the schema when it's saved:
- Type checking (string, integer, number, boolean)
- Required fields
- Dropdown values, including
x-enum-byrestrictions - Date and number limits (
x-min-from,x-max-from) - Field dependencies
Validation while editing
The form also checks values as you type:
- Required fields show an error if empty
- Number fields only accept numbers
- Integer fields only accept whole numbers
- Decimal fields (
x-decimal-places) round to the set precision - Dates and numbers outside their
x-min-from/x-max-fromlimit show an error - Nullable fields can be cleared
Validation Errors
Validation errors are returned in this format:
{
"udf": {
"field_name": "Error message"
}
}
Searching by UDF
UDF fields can be searched from the power search box on Projects, Proposal Items, Invoices and Contacts, using the keyword udf- followed by the field name, with underscores replaced by hyphens. For example, a field called risk_level is searched as udf-risk-level .
Tip: Hover over a UDF field to see its exact search keyword.
- Text fields match part of the text.
- Dropdowns match the exact value. Add
"x-enum-string-partial": trueto a dropdown to let searches match part of a value instead, soudf-status:progfinds "In Progress". - Number and date fields search by range.
Dividers, calculated fields, entity selectors and tags can't be searched this way.
For the full search syntax, see The Power Search Box.
Restricting who sees a field
Individual user-defined fields can be hidden from, or made read-only for, chosen permission groups. This is set up separately from the schema, as a field permission rule naming the field as Model.udf.field_name (for example Project.udf.effective_multiplier ). A user who may not read the field does not see an empty box: the field is left out of the record and out of the form. Gate a divider field the same way to hide a whole section. See Field permissions for how to write the rules.
Best Practices
- Always specify
x-order: ensures consistent field ordering - Use null for optional fields: include
"null"in the type array - Set sensible defaults: provide default values for all fields
- Use dependencies wisely: create logical field relationships
- Keep labels concise: use clear, short labels (2–4 words)
- Give long text room: use
x-cols: 12withx-multi-linefor notes - Test validation: check required/optional field behaviour, and that
x-enum-bylists narrow as expected - Consider field width: fields show three per row by default; use
x-colsto change it
Common Patterns
Optional Reference Field
{
"type": [
"integer",
"null"
],
"label": "Related Entity",
"x-widget": "client",
"default": null
}
Required Dropdown
{
"type": "string",
"label": "Category",
"enum": [
"A",
"B",
"C"
],
"default": "A"
}
Dependent Selectors
{
"parent_id": {
"type": [
"integer",
"null"
],
"x-widget": "client",
"x-order": 1
},
"child_id": {
"type": [
"integer",
"null"
],
"x-widget": "contact",
"x-depends-on": "parent_id",
"x-order": 2
}
}
Large Text Field
{
"type": [
"string",
"null"
],
"label": "Description",
"x-multi-line": true,
"x-rows": 8,
"x-cols": 12,
"default": null
}
Currency Amount
{
"type": [
"number",
"null"
],
"label": "Agreed Fee",
"x-decimal-places": 2,
"x-format": "currency",
"default": null
}