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 without x-order  are 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-places  takes 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.2  with 2 decimal places shows as 1.20 .
  • x-decimal-places: 0  enforces whole numbers while keeping the number  type.

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-format  only 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 from x-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 ). If true  and x-rows  isn't set, it defaults to 1 row.
  • x-rows : number of rows in the text area (requires x-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-widget  must be calculated  and x-template  must be a non-empty, valid Jinja2 template.
  • The type must be number  or string , and it must include "null" , with a default  of null . A calculation can fail — a division by zero, or referenced data that isn't there yet on a draft — and when it does, Kompass stores null  rather 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 as null .
  • Add x-format  to 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 to null .)
  • The previously-saved values: when editing an existing record, the template can compare the current value against what was saved. saved  mirrors the record's own values as they were before this edit (guard it with {% if saved %} , as it's null  for a brand-new record), and changes  is 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
    }
  }
}

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 whole enum  list 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.

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 default other than null , 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 date widget) 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-by  restrictions
  • 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-from limit 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": true  to a dropdown to let searches match part of a value instead, so udf-status:prog  finds "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

  1. Always specify x-order : ensures consistent field ordering
  2. Use null for optional fields: include "null"  in the type array
  3. Set sensible defaults: provide default values for all fields
  4. Use dependencies wisely: create logical field relationships
  5. Keep labels concise: use clear, short labels (2–4 words)
  6. Give long text room: use x-cols: 12  with x-multi-line  for notes
  7. Test validation: check required/optional field behaviour, and that x-enum-by  lists narrow as expected
  8. Consider field width: fields show three per row by default; use x-cols  to 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
}
Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us