Building truly reusable components means finding the right compromise between hard-coded logic, parametric adjustment and customisation. In this post, I want to share my way of building customisable components.
Background
To understand how this works, we have to start with a specific feature of the Appian expression language. It is a functional language with expression rules as first class citizens. This means, that you can pass references to expression rules just like any other value to a rule input of the type Any or a local variable and then execute them. Find the documentation here.
Let’s do a quick example. My first rule is this:
ReferencedRule(name(text))
"Hello " & ri!name & "!"
Then I can write code like below:
a!localVariables(
local!rule: rule!SSH_ReferencedRule,
local!rule(name: "Stefan")
)
Take a close look at line 2. By omitting the brackets, I assign a reference to the local variable instead of calling it. I can then execute the reference stored in the local by treating it like a normal rule.
Customisable Components
With this technique at hand, you can build a framework component like a grid, and then let the designer pass an expression to display each item. Let’s have a look at an example. This needs three separate expressions.
EXP_SegmentList
As I want to put a grid onto the screen, I somehow need to turn a list of items into a two-dimensional matrix. I use the following expression out of my toolbox to turn a list of items into a list of lists of items with a defined length. It transforms a one-dimensional list (1, 2, 3, 4, 5) into a two-dimensional ((1, 2, 3), (4, 5)). I added some additional features to allow a 90° rotation ((1, 4), (2, 5), (3)) and padding to have a uniform number of items in each column or row.
It uses the following rule inputs:
- items: Any Type
- segmentSize: Integer
- rotate: Boolean
- enablePadding: Boolean
if(
or(
a!isNullOrEmpty(ri!items),
a!isNullOrEmpty(ri!segmentSize)
),
ri!items,
a!localVariables(
local!numSegments: ceiling(count(ri!items) / ri!segmentSize),
if(
ri!rotate,
/* WITH ROTATION: segmentSize means the number of segments */
if(
ri!enablePadding,
/* WITH PADDING: Adds type casted NULL values to the last segment to fill up to the size of the segment */
a!forEach(
items: enumerate(ri!segmentSize),
expression: index(
ri!items,
1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
+ fv!item /* Start number for current segment */
/* Fix segment size */
+ enumerate(local!numSegments)
* ri!segmentSize,
cast(runtimetypeof(ri!items[1]), null)
)
),
/* WITHOUT PADDING: Only add left over items into the last segment. The last segment might contain less items */
a!forEach(
items: enumerate(ri!segmentSize),
expression: reject(
a!isNullOrEmpty(_),
index(
ri!items,
1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
+ fv!item /* Start number for current segment */
/* Adjust the size of the segment to either the required size or for the last segment the left over items */
+ enumerate(local!numSegments)
* ri!segmentSize,
null
)
)
)
),
/* WITHOUT ROTATION: segmentSize means the number of items per segment */
if(
ri!enablePadding,
/* WITH PADDING: Adds type casted NULL values to the last segment to fill up to the size of the segment */
a!forEach(
items: enumerate(local!numSegments),
expression: index(
ri!items,
1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
+ (fv!item * ri!segmentSize) /* Start number for current segment */
/* Fixe segment size */
+ enumerate(ri!segmentSize),
cast(runtimetypeof(ri!items[1]), null)
)
),
/* WITHOUT PADDING: Only add left over items into the last segment. The last segment might contain less items */
a!forEach(
items: enumerate(local!numSegments),
expression: index(
ri!items,
1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
+ (fv!item * ri!segmentSize) /* Start number for current segment */
/* Adjust the size of the segment to either the required size or for the last segment the left over items */
+ enumerate(min(ri!segmentSize, count(ri!items) - (fv!item * ri!segmentSize))),
null
)
)
)
)
)
)
EXP_RenderGridItem
This expression is responsible for rendering an individual item from the list. In my example, these items only have a single field “name”. There is no restriction to the complexity or source of the items you want to use. Just make sure that your expression works with your data structure.
The rule inputs are:
- item: Any Type (This can also be your CDT or Record type, or a map)
a!cardLayout(
showWhen: a!isNotNullOrEmpty(ri!item),
contents: {
a!richTextDisplayField(
labelPosition: "COLLAPSED",
value: ri!item.name
)
},
height: "AUTO",
style: "NONE",
marginBelow: "STANDARD",
showBorder: false,
showShadow: true
)
EXP_Grid
This is my framework component, which uses a foreach() to create a columns layout for the horizontal dimension. In each column, the inner foreach() calls the contentExpression to display the actual contents in a vertical orientation.
It needs the following rule inputs:
- items: Any Type
- contentExpression: Any Type
- numColumns: Integer
a!columnsLayout(
columns: a!forEach(
items: rule!PCO_SegmentList(
items: ri!items,
segmentSize: ri!numColumns,
rotate: true,
enablePadding: true
),
expression: a!columnLayout(
contents: a!forEach(
items: fv!item,
expression: ri!contentExpression(
item: fv!item
)
)
)
)
)
If you like, you can add a default rendering style and make the contentExpression optional. Below, a simple example:
expression: if(
a!isNullOrEmpty(ri!contentExpression),
a!richTextDisplayField(
label: "Default",
value: tostring(fv!item)
),
ri!contentExpression(
item: fv!item
)
)
Putting the Pieces Together
Let’s do a quick recap. You now have an expression that can render a two-dimensional matrix of items onto the screen. And you have another expression which knows how to display an individual item. Putting it together should look like this:
rule!EXP_Grid(
items: {
{name: "One"},
{name: "Two"},
{name: "Three"},
{name: "Four"},
{name: "Five"},
},
contentExpression: rule!EXP_ContentExpression,
numColumns: 2
)

Summary
I hope I could give you some insights into advanced coding techniques using the Appian expression language and inspiration for building your own customisable components.
I am very curious about the ideas you will implement with this method. Let me know in a comment.

Hi, please let me know your thoughts on below use case on exp rules as first class citizens.
#rule 1 – validate_user_email
Rule input- user – type -> usercdt
Exp rule: logic to validate user email address and return true or false
#rule 2 – validate_user_pincode
Rule input- user – type -> usercdt
Exp rule: logic to validate user pincode and return true or false
#rule3 – filter_cdt
Rule inputs:
1. function – type -> any type
2. userarray – type -> usercdt multiple
Exp rule:
a!foreach(
items: ri!userarray,
expression: function(fv!item)
# reject nulls if required.
#main rule
local!filter_cdt:rule!filter_cdt,
local!filter_cdt(rule!validate_user_email, userarray),
local!filter_cdt(rule!validate_user_pincode, userarray),
local!filter_cdt(rule!check_if_active, userarray), # example
Please let me know if i implemented it correctly.
– VAMSI KRISHNA
Hi Vamsi,
I suggest to use the filter() function to filter any kind of lists. This function takes a predicate function which is just the reference to another function or expression. So in your use case it would look something like this:
filter(
rule!validate_user_email,
userarray
)
One more thing. In Appian we typically use camelCasing and not underscore_casing. Then, I try to name validation expression more like “isUserEmailValid”. This also indicates that the return value is a boolean.
A foreach() creates a list with the same number of items as the input list. Trying to use foreach() for filtering only works because Appian merges empty lists in most cases. In some cases this breaks which might result in some hard to debug errors.
Stefan.
[…] code depends on the rule “PFX_SegmentList” that splits a list into segments. Find it here. You also need to install the regex plugin used to find valid addresses in the text […]
Stefan – It seems to me that Appian no longer allows (not sure since when) storing rule/function references in a local variable, but I could be wrong. If this is the case, is there a workaround to make this approach work again?
Hi Owen, storing rule references to locals works in expressions only. I am not sure whether it was ever supported in interfaces.
When you follow along the larger example, you will see that I do not store any reference to a local. And in my normal use cases, I never had the need to do so. What is your specific use case about?
Stefan – Thanks for the clarification! The specifics on how storing rule references to locals works in expressions only must’ve escaped my eyes. 🙁
Not sure if you’ll have time/appetite to respond to this, but my initial inquiry was regarding a broader problem I’m trying to solve. As for the specific use case:
1. I have about two dozen tasks in a workflow, and most of these tasks share similar functionalities (e.g., approve/reject decision, doc upload, simple confirmation, etc.).
2. I figured there’s an opportunity to create reusable task components to reduce the number of interface objects and to keep simplify the management/maintenance of these tasks as much as possible.
3. I’m thinking of having a wrapper interface object that receives a Task ID as an input to display different UI components with a future configurability (i.e., a new developer should be able to add/update/remove any of the task components with minimal code changes) in mind.
I’d really appreciate any tips, pointers, or guidance you can share on how to go about achieving the above objectives. Thanks, Stefan!
My experience with “similar” tasks is, that they tend to not be that similar, or become more diverse over time. When making such a design decision, we typically try to stick with it. When deciding to go with reusable interfaces, this leads to adding more and more options and logic over time. This makes them complex and complicated to maintain.
The least bad compromise is for me, to create just a few small reusable components I then use to build the larger interfaces. And I try to keep these components simple, clear and with a specific purpose in mind.