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 entityentityKind- The type of entity (e.g. Order, Contact, Other). See Entity TypesdisplayName- Localized labels for the UIfields- The data fields that make up the entityquickSearch- 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 Kind | Description | Use Case |
|---|---|---|
| Order | A customer order or shipment of any order type | Tracking shipments, deliveries, warehouse operations |
| Contact | Customers, carriers, vendors, drivers, employees, etc. | Managing customer data, vendor details |
| OrderEntity | A party on an order (shipper, consignee, carrier role) | Order parties and addresses |
| AccountingTransaction | Invoices, bills, credit memos | Financial documents |
| Job | A job grouping orders | Consolidations, fulfillment, projects |
| Commodity | Goods, pieces and containers | Inventory, packing, containers |
| Calendar | A calendar | Scheduling |
| CalendarEvent | An event on a calendar | Appointments, dock slots |
| CalendarAvailabilityBlock | An availability block on a calendar | Working hours, blackout periods |
| AuditChangeEntry | An audit change record | Change history views |
| Other | User-defined entity for custom data needs | Industry-specific data models |
How to Define an Entityโ
- In your App Module YAML file, add an
entitiessection - Define each entity with a name, entity kind, and fields
- For custom fields, set
isCustomField: true - 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
Quick Searchโ
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โ
| Attribute | Required | Description |
|---|---|---|
enabled | Yes | Turns quick search on or off for the entity. A disabled block is not validated, so it can be left incomplete. |
filter | No | Lucene filter that narrows the entity kind's table to this entity, e.g. orderType: ParcelShipment or contactType: Customer. Checked when the module is saved. |
permission | No | Permission a user needs to see the group. Reserved: not enforced yet. |
matchFields | Yes (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). |
select | Yes (at least 1) | Fields fetched for the result templates. Allowed values depend on entityKind; any customValues.<key> may also be selected. |
groupLabel | No | Localized tab and group label. Defaults to the entity's displayName. |
icon | Yes | Tabler icon class as used by the web app, e.g. tabler-plane, tabler-package, tabler-building-store. |
order | No | Position of the tab and group; lower numbers come first. Default 100. |
display.title | Yes | Template for the result title. |
display.subtitle | No | Template for the second line. |
display.badge | No | Template shown as a status chip. |
open | Yes | Exactly 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 Kind | matchFields | select |
|---|---|---|
| Order | orderNumber, trackingNumber | orderNumber, trackingNumber, orderType, created, billToContact.name, employeeContact.name, salespersonContact.name, orderStatus.orderStatusName |
| Contact | name, accountNumber | name, accountNumber, contactType, created, contactStatus.statusName |
| AccountingTransaction | transactionNumber | transactionNumber, accountingTransactionType, accountingTransactionStatus, transactionDate, dueDate, created, applyToContact.name |
| Job | jobNumber | jobNumber, description, created, customer.name, employee.name, jobStatus.jobStatusName |
| Commodity | description, serialNumber, trackingNumbers.trackingNumber | description, 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
filterand open action as the entity's list grid, so a search result behaves like clicking its row. - Keep
selectto the fields your templates use. - Give each group a distinct
iconand a sensibleorder, 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 Type | Description | Example Use |
|---|---|---|
| text | Stores string data | Names, descriptions |
| number | Numeric values | Quantities, measurements |
| date | Date/time values | Ship dates, deadlines |
| boolean | True/False values | Active, enabled, visible |
| list | List of options | Status codes |
| entityName | References another entity | Linking 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 fielddisplayName: Localized label for the fieldfieldType: Type of data the field holdsisCustomField: 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 to0.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 filteringfilterByProperty: Optional property name to use for filtering instead of the field name. Alias forfilter.fieldName- useful when you don't need a custom filter component.isInactive: Whether the field is inactivefilter: # Filter Component definitioncomponent: Component to use for filteringfieldName: Optional property name to use for filtering instead of the field name (takes precedence overfilterByProperty).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