Skip to main content

Entities

Introductionโ€‹

Entities in CXTMS define the core data structures used throughout the system. They represent key business objects like orders, contacts, and accounting transactions. App Modules can define new custom entity types or extend existing ones to add additional fields and functionality.

When to Define Custom Entitiesโ€‹

  • You need to store and manage data that doesn't fit into existing entity types
  • You want to add custom fields to extend existing entities like Orders or Contacts
  • You're building a new module that requires its own data model

Key Conceptsโ€‹

Entity Definitionโ€‹

An entity definition includes:

  • name - Unique identifier for the entity
  • entityKind - The type of entity (e.g. Order, Contact, Other). See Entity Types
  • displayName - Localized labels for the UI
  • fields - The data fields that make up the entity
  • quickSearch - Optional. Shows the entity in the global quick search. See Quick Search

Custom Fieldsโ€‹

Custom fields allow you to extend entities with additional data points. They can be added to both custom and existing entity types.

Entity Extensionsโ€‹

Existing entity types like Order can be extended to add new fields without creating an entirely new entity.

Entity Field Visibilityโ€‹

Entity-field queries exclude fields that belong to soft-deleted app modules. If a module is deleted or disabled, its custom field metadata stops appearing in entity field lists even when the underlying rows remain for audit or recovery.

Entity Typesโ€‹

entityKind accepts these values:

Entity KindDescriptionUse Case
OrderA customer order or shipment of any order typeTracking shipments, deliveries, warehouse operations
ContactCustomers, carriers, vendors, drivers, employees, etc.Managing customer data, vendor details
OrderEntityA party on an order (shipper, consignee, carrier role)Order parties and addresses
AccountingTransactionInvoices, bills, credit memosFinancial documents
JobA job grouping ordersConsolidations, fulfillment, projects
CommodityGoods, pieces and containersInventory, packing, containers
CalendarA calendarScheduling
CalendarEventAn event on a calendarAppointments, dock slots
CalendarAvailabilityBlockAn availability block on a calendarWorking hours, blackout periods
AuditChangeEntryAn audit change recordChange history views
OtherUser-defined entity for custom data needsIndustry-specific data models

How to Define an Entityโ€‹

  1. In your App Module YAML file, add an entities section
  2. Define each entity with a name, entity kind, and fields
  3. For custom fields, set isCustomField: true
  4. To extend an existing entity, set extension: true

Example:

entities:
- name: "ParcelShipment"
entityKind: Order
extension: true
displayName:
en-US: "Parcel Shipment"
fields:
- name: "trackingNumber"
displayName:
en-US: "Tracking Number"
fieldType: "text"
isCustomField: true

Introductionโ€‹

The global quick search (Ctrl+K or Ctrl+/ in the web app) lets users jump to a record by typing its identifier: an order or tracking number, a transaction number, a customer name or account number, a job number, or a commodity serial or tracking number. Any entity can take part by declaring a quickSearch block. Each searchable entity becomes its own tab and result group in the search box, and results from every enabled entity are searched at once.

YAML Structureโ€‹

entities:
- name: AirShipment
entityKind: Order # required for quick search; selects the searchable table
displayName:
en-US: Air Shipment
quickSearch:
enabled: true
filter: "orderType: AirShipmentOrder" # Lucene filter, same syntax as grid filters
permission: AirShipments/Read # reserved; not enforced yet
matchFields: [orderNumber, trackingNumber]
select: [orderNumber, trackingNumber, billToContact.name, orderStatus.orderStatusName]
groupLabel:
en-US: Air Shipments # defaults to displayName
icon: tabler-plane
order: 30
display:
title: "{{ orderNumber }}"
subtitle: "{{ trackingNumber }} ยท {{ billToContact.name }}"
badge: "{{ orderStatus.orderStatusName }}"
open:
navigate: "consolidatedShipments/AirShipmentOrder/{{ orderId }}"
fields:
- ...

Attribute Descriptionโ€‹

AttributeRequiredDescription
enabledYesTurns quick search on or off for the entity. A disabled block is not validated, so it can be left incomplete.
filterNoLucene filter that narrows the entity kind's table to this entity, e.g. orderType: ParcelShipment or contactType: Customer. Checked when the module is saved.
permissionNoPermission a user needs to see the group. Reserved: not enforced yet.
matchFieldsYes (at least 1)Identifier fields the typed text is matched against. Matching is case-insensitive: exact matches rank first, then prefix, then contains (contains from 3 characters). Allowed values depend on entityKind (see below).
selectYes (at least 1)Fields fetched for the result templates. Allowed values depend on entityKind; any customValues.<key> may also be selected.
groupLabelNoLocalized tab and group label. Defaults to the entity's displayName.
iconYesTabler icon class as used by the web app, e.g. tabler-plane, tabler-package, tabler-building-store.
orderNoPosition of the tab and group; lower numbers come first. Default 100.
display.titleYesTemplate for the result title.
display.subtitleNoTemplate for the second line.
display.badgeNoTemplate shown as a status chip.
openYesExactly one of navigate (a route path template, org-relative like a grid's onRowClick.navigate) or dialog (a component name such as Orders/UpdateOrder, opened in a dialog).

Templates use the usual {{ path }} syntax and may only reference fields listed in select, plus the record key (orderId, contactId, accountingTransactionId, jobId, commodityId). A dialog opened from quick search receives the record key under its own name, organizationId, and the current user, the same as a dialog opened from a grid row.

Searchable Entity Kindsโ€‹

Entity KindmatchFieldsselect
OrderorderNumber, trackingNumberorderNumber, trackingNumber, orderType, created, billToContact.name, employeeContact.name, salespersonContact.name, orderStatus.orderStatusName
Contactname, accountNumbername, accountNumber, contactType, created, contactStatus.statusName
AccountingTransactiontransactionNumbertransactionNumber, accountingTransactionType, accountingTransactionStatus, transactionDate, dueDate, created, applyToContact.name
JobjobNumberjobNumber, description, created, customer.name, employee.name, jobStatus.jobStatusName
Commoditydescription, serialNumber, trackingNumbers.trackingNumberdescription, serialNumber, created, billToContact.name, commodityStatus.statusName

Other entity kinds can't be searched yet. Custom values can be selected and shown but not matched. The CLI keeps these lists in .cx-schema/quick-search-kinds.json.

Validationโ€‹

npx cxtms <module.yaml> checks a quickSearch block's shape (schema_violation) and its rules (invalid_quick_search): a supported entityKind, allowed matchFields and select values, a title and icon, exactly one open action, and template paths limited to select. The backend applies the same rules when a module is saved, created, imported or updated, and also rejects an invalid filter.

Overriding or Hiding a Core Entityโ€‹

An app module can change how a core entity is searched by redeclaring the entity with its own full quickSearch block. When several modules declare a quickSearch block for the same entity name, the most recently saved block wins. To hide a core group entirely, redeclare it with enabled: false:

entities:
- name: PickupOrder
entityKind: Order
quickSearch:
enabled: false

A module that redeclares an entity only to add fields, without a quickSearch block, doesn't affect its quick search.

Examplesโ€‹

A contact entity that opens its update dialog:

- name: Customer
entityKind: Contact
quickSearch:
enabled: true
filter: "contactType: Customer"
matchFields: [name, accountNumber]
select: [name, accountNumber, contactStatus.statusName]
groupLabel:
en-US: Customers
icon: tabler-building-store
order: 70
display:
title: "{{ name }}"
subtitle: "{{ accountNumber }}"
badge: "{{ contactStatus.statusName }}"
open:
dialog: Customers/UpdateCustomer

An app overriding core's Ocean Shipments group so results open its own dialog instead of navigating:

- name: OceanShipment
entityKind: Order
quickSearch:
enabled: true
filter: "orderType: OceanShipmentOrder"
matchFields: [orderNumber, trackingNumber]
select: [orderNumber, trackingNumber, billToContact.name, orderStatus.orderStatusName]
groupLabel:
en-US: Ocean Shipments
icon: tabler-ship
order: 31
display:
title: "{{ orderNumber }}"
subtitle: "{{ trackingNumber }} ยท {{ billToContact.name }}"
badge: "{{ orderStatus.orderStatusName }}"
open:
dialog: OceanShipments/UpdateOceanShipment

Best Practicesโ€‹

  • Match on identifiers users actually type: numbers and codes, not free text.
  • Use the same filter and open action as the entity's list grid, so a search result behaves like clicking its row.
  • Keep select to the fields your templates use.
  • Give each group a distinct icon and a sensible order, so tabs read in the order users expect.
  • Hide core groups your app doesn't use with enabled: false, rather than leaving empty tabs.

Field Typesโ€‹

Field TypeDescriptionExample Use
textStores string dataNames, descriptions
numberNumeric valuesQuantities, measurements
dateDate/time valuesShip dates, deadlines
booleanTrue/False valuesActive, enabled, visible
listList of optionsStatus codes
entityNameReferences another entityLinking to another entity

Fields Definitionโ€‹

fields:
- name: "originCountry"
displayName:
en-US: "Origin Country"
fieldType: "Country"
isCustomField: true
priority: 10
props:
filterComponent: Countries/Select
allowOrderBy: false
allowFilter: false

Key Attributes:

  • name: Unique identifier for the field
  • displayName: Localized label for the field
  • fieldType: Type of data the field holds
  • isCustomField: Whether the field is a custom field. If fieldName is not system field, then isCustomField is required.
  • priority: Numeric precedence for module-level field overrides. When multiple app modules define the same entity field, the active field with the highest priority wins. Defaults to 0.
  • props: Additional properties for the field

Common Propsโ€‹

  • showAs: How the field is displayed in the Grid and Form (component)
  • allowOrderBy: Whether the field can be used for ordering (default: true)
  • orderByProperty: Optional property name to use for sorting instead of the field name. Useful when displaying a formatted value but sorting by an underlying field.
  • allowFilter: Whether the field can be used for filtering
  • filterByProperty: Optional property name to use for filtering instead of the field name. Alias for filter.fieldName - useful when you don't need a custom filter component.
  • isInactive: Whether the field is inactive
  • filter: # Filter Component definition
    • component: Component to use for filtering
    • fieldName: Optional property name to use for filtering instead of the field name (takes precedence over filterByProperty).
    • props: Additional properties for the filter component

Virtual Fields / GraphQL Resolversโ€‹

Virtual fields are fields that are not stored in the database, but are calculated at runtime using a GraphQL resolver.

fields:
- name: "getSummary" # name of the field
displayName: "Summary"
fieldType: "text"
isCustomField: true
props:
allowOrderBy: false
allowFilter: false

Best Practicesโ€‹

  • Use clear, descriptive names for entities and fields
  • Leverage existing entities where possible before creating custom ones
  • Add custom fields to extend entities rather than duplicating data
  • Use entity references to create relationships between data
  • Include localized display names for multi-language support