# Accessing BO Descriptors
reviewed: 10 March 2025
Writing BO-Savvy Code
We have learned the term self-describing business objects. It is the self-describing feature that makes it possible to develop highly generic components. These components are sometimes referred to a being BO-savvy. They can work with any self-describing business object; one generic component that can be used by countless number of different business objects.
| Component | BO | Result |
|---|---|---|
List |
Client |
List of Clients |
Order |
List of Orders |
|
PrintRequest |
List of PrintRequests |
|
Form |
Flight |
Form to view / edit details of Flights |
Transaction |
Form to view / edit details of Transactions |
|
Visit |
Form to view / edit details of Visits |
The boDesc Function Handler
The boDesc function handler is designed to read and set BO descriptor values.
We start by looking at the functions to read value; see the paragraph on massaging descriptors for information about the functions to set values.
The following example shows two similar functions; one to get the label of the BO and one to get the label of a specific attribute.
logError( boDesc.getLabel( [_me] ) )
logError( boDesc.getAttrLabel( [_me], 'id' ) )
These functions will return the values as can be found in the entity script file:
<@bo
label: 'Client' // Returned by boDesc.getLabel( [_me] )
table: 'client'
primaryKey: 'id'
sequence: 'client'
auditable: auditing.Auditable
deleteRule: deleteRule.PerRelation
sequenceStep: 2
attributes: <
id: <@bo/attribute
label: 'Id' // Returned by boDesc.getAttrLabel( [_me], 'id' )
column: 'id'
dataType: dataType.Automatic
length: 9
locked: true()
>
Most functions are pretty straightforward and map more or less directly on the BO descriptor settings.
A number of functions are worth a separate mention.
boDesc.inGroup, boDesc.anyInGroup
Test whether one or more attributes exist in a group. Often used in _eventAction methods.
if and(
eq( [timing], eventActionTiming.Pre ),
in( [type], eventActionType.Insert, eventActionType.Update ),
// Is accessCode in the attribute group?
boDesc.inGroup( [_me], 'accessCode', [group] ),
ne( left( bo.attr( [_me], 'accessCode' ), 1 ), '+' )
)
raise exceptionType.soft, 'Access code must start with +'
end-if
Private Descriptors boDesc.create, boDesc.isPrivate
Descriptors are cached for performance reasons. When you do a bo.create( 'client' ), CaseMaster will check whether the descriptor for client is already in the cache. When it is, the descriptor is retrieved from cache, otherwise client.cms is loaded for the first time, parsed and a new descriptor is created from it which is then added to the cache.
It can sometimes be useful to have a private descriptor; one that is not added to the cache and is thus never shared between different instantiated business objects.
A common scenario is where you massage the descriptor (see that paragraph) and you do not want your temporary descriptor overrides to be shared.
// Get a private descriptor
set( 'privateDescriptor', boDesc.create( 'client' ) )
// Massage the private descriptor here
set( 'bo', bo.create( [privateDescriptor] ) )
The following example shows how to get a handle to the cached descriptor:
set( 'descriptorFromCache', boDesc.get( bo.create( 'client' ) ) )
Descriptor Alias, BO Alias: getAlias and setAlias
An alias of a descriptor or business object can be used to ensure that CaseMaster generates correct SQL in joins (see here) or when resolving foreign keys (see here).
The scenarios are typically highly advanced and often originate from the same entity included in a join more than once.
You can set the alias on the entity by including the alias: 'something' tag in the descriptor .cms file. This means the alias is always set.
You can also set the alias using the boDesc.setAlias() function. See the paragraph on massaging descriptors.
Where possible, you can also set the alias on a business object instead of its' descriptor. The performance will be better as business objects are thread safe by default. Use the bo.setAlias() function for this.
See also:
- The
foreignKeyAliasfor foreign key attributes (see here) - The alias in sub-selects in where clauses (see here)
boDesc.serialize
Use the boDesc.serialize() function to dump the .cms code for the <@bo> qualifier of a descriptor.
// Can work on an entity name or business object
logError( boDesc.serialize( 'client' ) )
logError( boDesc.serialize( bo.create( 'client' ) ) )
The iterator.ofAttribute() Function
The iterator.ofAttribute() function returns an iterator with information about attributes in a group of a business object.
The function takes 3 parameters; a business object or entity name, an attribute group and the variable name that you can refer to inside the iterate - end-iterate construct.
// Use with entity name
iterate iterator.ofAttribute( 'client', 'description', 'attr' )
response.write( [attr] )
response.write( '<br>' )
end-iterate
// Use with BO
set( 'cl', bo.create( 'client' ) )
iterate iterator.ofAttribute( [cl], 'description', 'attr' )
response.write( [attr] )
response.write( '<br>' )
end-iterate
The iterator consists of zero or more property bags that have the following format:
<
name: "firstname"
bodescriptor: "client"
>
Often, you (ab)use the feature of a property bag that when you treat it as a string it simply returns the first value. See our example above.
Putting it Together

The following code shows how the boDesc function handler and the iterator.ofAttributes() function can be used to create a generic routine to dump values in a HTML table:
// 1. Change client to user, user/session or any other valid entity name
// set( 'entity', 'user' )
// set( 'entity', 'user/session' )
set( 'entity', 'client' )
// 2. Open container, table and thead
response.write( '<div class="container">')
response.write( '<table class="table table-sm table-striped table-bordered">')
response.write( '<thead class="bg-primary text-light">')
// 3. Iterate over the attributes in the descriptor attribute group
iterate iterator.ofAttribute( [entity], 'description', 'attr' )
// 4. Write td with the label of the attribute
response.write( formatString( '<td>{0}</td>', boDesc.getAttrLabel( [entity], [attr] ) ) )
end-iterate
// 5. Close the thead and open the tbody
response.write( '</thead>')
response.write( '<tbody>')
// 6. Iterate over all rows of the entity
iterate iterator.ofEntity( < < name: 'BO' entity: [entity] > > )
// 7. Open tr
response.write( '<tr>')
// 8. Iterate over the attributes in the descriptor attribute group; note that we could also have used [entity] instead of [BO]
iterate iterator.ofAttribute( [BO], 'description', 'attr' )
// 9. Write td with the value of the attribute
response.write( formatString( '<td>{0}</td>', bo.attrFormatted( [BO], [attr] ) ) )
end-iterate
// 9. Close tr
response.write( '</tr>')
end-iterate
// 10. Close the tbody, table and container div
response.write( '</tbody>')
response.write( '</table>')
response.write( '</div>')
Massaging Descriptors
Sometimes it can be useful to change the behaviour of a business object at runtime. Examples:
- An attribute that is optional needs to be mandatory on a specific form
- In a specific list the column header for an attribute needs a different label
This can be done by massaging the descriptor. You can safely set / update descriptor settings using the boDesc function handler.
There is a (small) performance penalty as CaseMaster will create a private clone of the descriptor so the alterations are not shared.
// Massage the descriptor so the client is locked
boDesc.setAttrLocked( [order], group: 'client', value: true() )
See the CaseMaster Playground for all functions in the boDesc handler.
Notes on the boDesc Function Handler
- Most functions in the
boDeschandler can be applied to a BO object or a descriptor object - Some can also apply to an entity name, but none of them allow you to set anything
- When applied to a BO object, the changes only apply to that BO, not to other BOs of the same entity
- When applied to a descriptor, the changes apply to all BOs created from that descriptor after the change has been applied
BO and Attribute Enhancers
Enhancers are designed to enhance the behaviour a business object or attribute. Often, enhancing is used to enhance the look & feel on an HTML page.
There are two enhancers for a BO:
| Enhancer | Usage |
|---|---|
| noAudits | States whether or not audits should be written |
| noTouch | States whether or not audit attributes should be written |
Both are explained in detail here.
The following shows how this could be included in an entity '.cms' file although it is more often that these enhancers are set at runtime.
resource main
<@bo
label: 'Client'
table: 'client'
primaryKey: 'id'
sequence: 'client'
auditable: auditing.Auditable
deleteRule: deleteRule.PerRelation
sequenceStep: 2
enhancers: <@bo/enhancers
noAudits: true()
>
As explained, the BO enhancers are typically set at runtime. Setting an enhancer at runtime will also force the creating of a private descriptor.
set( 'cl', bo.create( 'client' ) )
boDesc.setEnhancer( [cl], 'noAudits', true() )
There are many more enhancers for attributes. A full list can be found by reading the source of the qualifier <@bo/attribute/property/enhancers>.
| Enhancer | Usage | Example |
|---|---|---|
| disabled | States whether or not an attributes is disabled | false() |
| noLabel | States whether or not an attributes label should be visible | false() |
| labelClass | Specifies one or more class names for the attributes form label | 'expressionClass' |
| labelTooltip | Specifies the tooltip for the attributes form label | 'Enter a positive number' |
| inputClass | Specifies one or more class names for the attributes form input | 'mandatoryClass' |
| inputTooltip | Specifies the tooltip for the attributes form input | 'Enter a positive number' |
| inputDir | Specifies the text direction of the attributes form input | ltr |
| inputPattern | Specifies a regular expression that the attributes form input value is checked against on form submission | '999[a-z]' |
| inputTitle | Specifies extra information about the attributes form input, most often shown as a tooltip | 'Enter a positive number' |
| inputStep | Specifies the interval between legal numbers in an input element | 0.1 |
| inputType | Specifies a type override to allow presenting an input in a nonstandard way | 'url' |
| lockedClass | Specifies one or more class names for the attributes locked input | 'lockedClass' |
| lockedTooltip | Specifies the tooltip for the attributes locked form value | 'You do not have the access rights to edit the amount' |
| headerClass | Specifies one or more class names for the attributes table header | 'headerClass' |
| headerTooltip | Specifies the tooltip for the attributes table header | 'Click to sort' |
| cellClass | Specifies one or more class names for the attributes table cell | 'cellClass' |
| cellLink | Specifies one or more class names for the attributes table cell | See further |
| cellTooltip | Specifies the tooltip for the attributes table cell | 'Click to view document' |
| prependInput | Specifies a collection of elements to prepend to the attributes from input | - |
| appendInput | Specifies a collection of elements to append to the attributes from input | See further |
| preInput | Specifies a collection of elements to render before the attributes from input | - |
| postInput | Specifies a collection of elements to render after the attributes from input | See further |
| multiLineRows | Specifies the number of rows to display for multiline text inputs | 12 |
| selectSorter | Specifies the sorter JavaScript callback for option list select inputs | alphabetical to sort alphabetical |
| foreignKeyRows | Specifies the number of rows to fetch for each foreign Key select input request | 50 |
The following is a real-life example of the cellLink enhancer:
_listDocument: <@bo/attribute/dynamic
dynamicValue: $if(
isNotNull( bo.attr( [_me], 'document' ) ),
'<span class= "btn btn-outline-primary btn-sm">View document...</span>',
null()
)
htmlSafe: true()
enhancers: <@bo/attribute/property/enhancers
cellLink: <@page/table/link
url: <@url
address: $script.call('web/router/service:resolveUrl', 'document/view/')
untrustedQs: <
context: $encrypt( bo.attr( [_me], 'document' ) )
>
>
target: 'tab'
>
>
>
The following is a real-life of the appendInput enhancer:
appendInput: <@page/input/group/append
excluded: $or(
// The the business object has not been loaded (i.e. create instead of edit)
ne(bo.attrPersistStatus([__bo], boDesc.getPK([__bo])), attrPersistStatus.Loaded),
// The attribute is disabled (i.e. can not edit)
boDesc.attrIsDisabled([__bo], [__attribute])
)
content: <@page/button
class: 'btn-light border'
icon: <@page/icon name: 'language'>
target: <@page/link/target/modal
size: 'xl'
title: $getTranslation('multilanguage/translations')
>
url: <@url
address: 'application/multilanguage/edit:main'
persistQs: false(),
qs: <
pk: $bo.pk([__bo])
bo: $boDesc.name([__bo])
attribute: $[__attribute]
control: $boDesc.getControlName([__bo], [__attribute])
>
>
>
>
The following is a real-life of the postInput enhancer:
group: <@bo/attribute/memo
label: <@text
EN: "Group"
>,
datatype: datatype.String
enhancers: <@bo/enhancers
postInput: <@page/text/small
class: 'text-muted',
content: 'Relevant for update only'
>
>
>
<End of document>