Free cookie consent management tool by TermsFeed

Expressions

Overview

This page covers a multitude of general topics about expressions.

Note:  The Expression Editor allows you to easily write expressions. Learn more.

An expression is a statement composed of one or more literal values, operators, functions, and variables. Expressions are evaluated by the rules engine to produce a single value. The construction of Appian expressions will be immediately familiar to anyone experienced building formulas in spreadsheet software.

Use this article to learn how to create more sophisticated expressions and use them throughout your application. Expressions are a versatile tool that allow you to build powerful logic in your processes.

Expressions can access contextual data via variables like process variables, node inputs, constants, CDTs, and process properties. Beyond their returned value, expressions do not modify any data inside or outside the Appian system.

You can create an expression by typing it directly into an expression field or through the Expression Editor. Expression fields are marked by an Expression Editor icon.

Parts of an expression

Below is an expression with its various parts labeled.

Exp_callouts

  1. Literal Values: The values New Ticket and ` by ` are text literals and return in the expression output as written.
  2. Operator: The & operator represents text concatenation and combines text with data.
  3. Variables: The pv!ticketId variable returns the Ticket ID value, and the pp!initiator variable returns the user who started the process.
  4. Functions and Rules: The userDisplayName rule takes a user as a parameter and returns the user's first and last name.

The above expression could result in the following: New Ticket AN-9867 by John Smith

More information on using these different parts appears in the sections below.

Expression best practices

This section describes a recommended approach that follows Appian best practices.

Saving an expression as an expression rule

When you write an expression that can be applied in multiple places around your system, such as one that retrieves information on the most recent meeting event, it is a best practice to create a rule out of it.

An expression rule is a stored expression with a user-defined name, description, definition, and a central location that can be reused within any expression field. They are accessible through the Expression Editor or can be typed into an expression.

See also: Expression Rules, Create Constant, Save Selected Expression As…

Saving a literal value as a constant

Similar to expression rules, instead of defining the same literal value multiple times in your system, such as the number of days until a deadline, define the literal value as a constant.

See also: Constants

Literal values

A literal is a static value stated in the expression, such as 2, 3.14, or "hello".

The following types of literals are supported:

Text string

Expresses a series of text characters. It is stated as text entered between double quotation marks.

  • Example: "Hello World"

Integer number

An integer number is any whole number.

  • Example: 82 or -82

Decimal number

A decimal number possesses a decimal point and can express fractional numbers.

  • Example: 1.234

Special literals

Certain built-in expression values exist in order to concisely express concepts or conditions. Supported special literals include:

  • true for the Boolean value true.

  • false for the Boolean value false.

  • null for an empty (null) value that has no specific data type.

Special literals can be used within any variable as a default value and can be cast to any data type.

Arrays

An array is an ordered list of data items that can be selected by indices computed at run-time. Arrays can be empty or constructed with literal values, variables, or functions.

  • Only one-dimensional arrays can be specified.
  • Nested arrays are flattened to a one-dimensional array.
    • Example: {{1, 2}, {3, 4}} = {1, 2, 3, 4}
  • For multi-dimensional arrays, create a complex data type.

When entering an array as part of an expression, you must use braces ({}) to enclose it and separate items using commas (,) unless you are referencing a variable.

  • Example: {2, 3, 9, 1}

To add text in an array, enclose each item in quotation marks ("").

  • Example: {"a", "b", "c", "d"}

Items in a list that contain data queries may be evaluated in parallel to reduce the overall evaluation time.

Accessing array items at an index

In order to select one or more values from an array, use the index operator [] or index().

For example, with an index operator and an array of pv!multiple = {10, 20, 30}:

  • pv!Multiple[2] yields 20
  • pv!Multiple[{2, 3, 2}] yields {20, 30, 20}

  • Example with index() and array of {10, 20, 30}:

  • index({10, 20, 30}, 2, 1) yields 20

Usage considerations

  • All arrays are indexed starting at one (1).
  • An integer expression can be used in place of a single integer or array of integers.
  • Variables and constants with a number (integer) data type or text data type are also accepted by the index operator.

See also: Array Functions

Maps

A map is an ad-hoc data structure created inline in an expression using the a!map() function. In contrast, a custom data type is a reusable data structure with a specific name and predefined fields.

For example, to create a map with two fields called label and value, enter the following:

1
a!map(label: "Item", value: "Entry")

To create an array of maps, enter the following:

1
{a!map(label: "Item one", value: "Entry one"), a!map(label: "Item two", value: "Entry two")}

Fields within a map that contain data queries may be evaluated in parallel to reduce the overall evaluation time.

Dictionaries

A dictionary is very similar to a map; it is an ad-hoc data structure mapping fields to values. However, values indexed out of a dictionary often need to be cast to their respective type before use. For this reason, maps should generally be used instead of dictionaries.

To create a dictionary with two fields called label and value, enter the following:

1
{label: "Item", value: "Entry"}

Date and time

Data types Date, Time, and Date and Time are stored as numbers.

  • Date is an integer (days since the epoch date).
  • Time is an integer (milliseconds since midnight).
  • Date and Time is a decimal (fractional days since the epoch date).

For date and time values, the local value (based on the designer time zone) is not stored. Instead, it is stored in Greenwich Mean Time (GMT) and then converted to the user's time zone when displayed on the screen.

To advance either Date or Date and Time by a day, add 1 to the value.

To advance Time by a minute, add 60*1000 to the value. Adding 1 to Time only advances it by a millisecond.

Example Input Yields
date(2012, 4, 30) - date(2012, 4, 25) 5 days
datetime(2012, 4, 25, 12) - datetime(2012, 4, 25, 10) 0.0833 days -or- 2.0 hours
date(2012, 4, 25) + 5 4/30/2012

Operators

There are many operators that can be used in expressions to perform data manipulation. The operators provided are divided into two different categories.

Arithmetic operators

To Perform... Use Example
Addition + 10+8 yields 18
Subtraction/Negation - 10-8 yields 2 - OR - if value is 97, then -value is -97
Multiplication * 2*5 yields 10
Division / 10/5 yields 2
Exponentiation ^ 2^8 yields 256
Percentage (divide by 100) % 97% yields 0.97

Using arithmetic operators with arrays and tointeger()**

Example Input Yields
{1, 2, 3} * 10 {10, 20, 30}
{1, 2, 3} + {1, 2, 3} + {1, 2, 3} {3, 6, 9}
{1, 1, 1, 1, 1} + {1, 2} {2, 3, 2, 3, 2}
tointeger({}) + 1 1

BEST PRACTICE: Apply arithmetic to lists directly, rather than through MNI, to make your process model look cleaner and work faster.

Note:  If operating on two arrays that are not the same length, the shorter list will be repeatedly extended until it is the same length as the longer list. This applies to Date and Date/Time values as well: pv!list_of_dates+1 returns the next day for the entire list.

Comparison operators

To Perform... Use Example
Less than < 10<2 yields false
Greater Than > 10>2 yields true
Less Than or Equal <= 10<=2 yields false
Greater Than or Equal >= 10>=2 yields true
Not Equal to <> 10<>2 yields true
Equal to = 10=2 yields false

Using comparison operators with arrays, functions, and text inputs

  • Array elements are compared item by item.
  • When comparison operators are used on an array, multiple true or false results are returned.
  • If two arrays are being compared and have different lengths, the shorter array is repeated to match the length of the longer array. See below for examples.
  • If you want a single comparison of one list versus another, use the exact function.
  • If you need a text comparison that DOES NOT require case-insensitiviy, consider using the exact() function rather than the = operator to improve performance.
  • To test any element in the compared list, enclose the comparison using the or() function.
Example Input Yields
1={1, 2, 3} {true, false, false}
{1, 2, 3}<>{4, 2, 6} {true,false,true}
exact({1, 2, 3},{1, 2, 3}) true
1=tointeger({}) false
0=tointeger({}) true
"Hello"="HELLO" true (case-insensitive)
exact("Hello","HELLO") false (case-sensitive)
{1,2} = {1,2,1,2} {true, true, true, true}
{"a","b"} = {"a","a","b","b"} {true, false, false, true}

Note:  In the case of comparing an array of text values to a scalar text value, the array elements will not be compared item by item with the scalar text value. In order to compare item by item, you must wrap the scalar value in a list. For example, use {"a", "b", "c"} = {"c"} instead of {"a", "b", "c"} = "c" to compare each element of the array to the value "c". This only applies to the text data type.

Operator precedence

The precedence of operators evaluated in an expression follows the standard Order of Operations:

  1. Operator expressions inside parentheses
  2. Exponentiation
  3. Multiplication and Division
  4. Addition and Subtraction

So, if an expression includes two or more operators, the operator higher on the list is applied first, then the second highest, and so on. In order to ensure an operation occurs before another, enclose it within parentheses.

Variables

A variable identifies the data to use when evaluating an expression. It uses syntax similar to Microsoft Excel sheet!cell syntax, where the Appian data domain is the sheet and a named variable is the cell syntax. Appian variables are always of a specific data type.

The variables you can use in an expression depend on the context in which the expression is designed and evaluated. They are listed in the Data tab of the Expression Editor and include Appian variables and user-defined process variables.

For a full listing or variables delivered with Appian, see Process and Report Data.

In the following example, the expression returns the value of a process variable:

Pv_callouts

  1. domain: Optional if the desired domain is the default domain for the execution context. When a domain isn't specified, it is inferred based on the context in which the expression is evaluated.
  2. name: Name you assign (or assigned by Appian) to your variable when you create it. It is not case sensitive. If the variable name contains characters other than letters/numbers/underscores or begins with an underscore, the domain and variable name must be enclosed in single quotes. For example, 'pv!variable.a-name'.

Variables of complex and custom data types use the dot operator to access field values. You can also use the index() function.

Let's say you have a custom data type "Person" with the following structure:

1
2
3
4
5
Person
 |- firstName (Text)
 |- lastName (Text)
 |- homeAddress (Address)
   |- city (Text)

You create a variable (process variable, node input, or local variable) called "personA" of type Person. To access the "firstName" value of the variable, enter any of the following:

  • index(pv!personA, "firstName", "")
  • pv!personA["firstName"]
  • pv!personA.firstName

Note:  Appian recommends using index() to access the index value of a CDT in cases where the variable may have a null value, or the field name is not available. The index() function allows you to assign a default value through its default value parameter. See also: index() function.

The dot notation is useful when accessing nested fields of custom data type, provided that the nested field is not null.

To access the "city" value of the "personA" variable, enter either of the following:

  • index(pv!personA, "homeAddress", "city", "")
  • pv!personA.homeAddress.city

Local variables

Local variables differ in that they are defined and set within the expression and are only available inside the function that defines them.

The a!localVariables() function defines a local variable and immediately assigns it a value. For example, in the following expression, local!username is set to the current user's username and then used several times in the rest of the expression:

1
2
3
4
5
6
7
8
a!localVariables(
  local!username: loggedInUser(),
  if(
    len(local!username) > 10,
    local!username & " is a long username!",
    local!username & " seems like a fairly short username!"
  )
)

Using local variables can avoid duplicating logic, make expressions easier to read, and avoid needlessly evaluating a part of the expression multiple times. They can also be used to store a user's input as they interact with the interface.

Local variable definitions that contain data queries may be evaluated in parallel to reduce the overall evaluation time.

Function variables

Some functions allow variables to be used in one or more of their inputs. These variables are in the fv! domain and are only available when configuring that function's input. They are auto-suggested in the Expression Editor and listed in the documentation for relevant functions.

For example, the a!foreach() looping function has several variables available in its expression parameter, such as fv!item:

1
=a!forEach(items: { 1, 2, 3 }, expression: fv!item + 1)

Because it is a function variable provided by the a!foreach() function, fv!item cannot be used outside of a!foreach(). In this case, it is only provided for the expression parameter, so it cannot be used in the items parameter either. As a!forEach() iterates through the items parameter, it evaluates the expression multiple times, each time providing the current item value to fv!item.

Text concatenation

When writing an expression that includes text and data, use an ampersand (&) to string (or concatenate) the information together.

For example, use the following expression to display a salutation when using process variables for the title and name data:

1
="Dear " & pv!title & " " & pv!name & ","

To return the following value:

1
Dear Mr. John Smith,

Comments

Comments are a way for designers to leave notes in the expression to explain what the expression does.

You start a comment with /* and end it with */. Any content between these two symbols is ignored when the expression evaluates.

For example:

1
"Dear "& pv!title /* pv!title should contain either "Mr" or "Mrs" */ & pv!name &","

Includes a comment but still evaluates to the following:

1
"Dear "& pv!title & pv!name &","

Functions, rules, and data types

Appian functions, custom function plug-ins, rules, and data types are stored within the Appian system.

You can use Appian functions, custom function plug-ins, and rules in expressions to perform operations using arguments you pass to them. The expression then returns a result based on your arguments.

You can use data types in expressions to construct a complex data type value by creating a type constructor.

Arguments or fields within a type constructor that contain data queries may be evaluated in parallel to reduce the overall evaluation time.

Both options of passing arguments and creating type constructors are explained in detail below.

See also: Appian Functions, Expression Rules, Data Types

Passing arguments

Functions and rules are defined by their logic and parameters. These parameters accept arguments that determine how the function will evaluate.

For example, the user() function retrieves the first name of a user using a process variable and a literal text as arguments through the following syntax:

Function_callouts

  1. domain: Defines where the operation definition is stored. When not specified in the expression, the name is searched for in rules, then Appian Functions, then custom functions.
  2. name: Given name of the Appian function, custom function, rule, or data type to use.
  3. arguments: Values supplied to the function parameters. Supported arguments are literal values, arrays, variables, functions, rules, data types, and expressions. The Expression Editor lists the expected argument types within the function description.

Passing arguments can be done by position or keyword.

By position

Passing arguments by position is required for Appian functions and custom function plug-ins and is best for rules that take three or fewer arguments.

To pass arguments by position, enter the values in order.

For example, the syntax for the joinarray() function is the following:

joinarray( array, [separator] )

To pass values using positional arguments, enter the following:

1
=joinarray({1,2,3,4},"|")

This evaluates with array as {1,2,3,4} and separator as |, returning 1|2|3|4.

Required Arguments

You must enter values for every required argument in a function or rule or the expression results in an error. For rules, all inputs are required when passing by position.

Optional Arguments

To pass a value for an optional argument, enter values for all arguments defined before the parameter you're entering a value for, even if they are also optional. If not, the expression may apply the argument to the wrong parameter and result in an error or undesired results.

Optional arguments are surrounded by brackets [] in the function documentation.

For example, the toxml() function has four parameters, three of which are optional.

toxml( value, [format], [name], [namespace] )

To use the default for [format] and [namespace], but specify a value for [name], you must also configure the [format] parameter.

For example:

=toxml(pv!somePersonNameCDT, false(), "person") evaluates with the following:

  • value = pv!somePersonNameCDT
  • [format] = false()
  • [name] = "person"
  • [namespace] = default value

Whereas, =toxml(pv!somePersonNameCDT, "person") would evaluate with the following:

  • value = pv!somePersonNameCDT
  • [format] = "person"
  • [name] = default value
  • [namespace] = default value

Unlimited Arguments

Some functions take an unlimited number of arguments, such as sum(). This is denoted by an ellipsis in their function description with the Expression Editor.

For example:

sum( addend, … )

By keyword

Passing arguments by keyword is only supported in system functions and rules. It is a best practice to pass arguments by keyword when a function or rule takes more than one argument.

See also: System Functions

To pass arguments by keyword, specify the name of the parameter, followed by a colon, then the argument value.

Appian recommends entering a line break after each keyword argument.

For example:

A rule called feedMessageForNewCase returns the feed message for a case management application using the following definition:

1
="Priority " & ri!priority & ": " & ri!caseSummary & " [#" & ri!caseId & "]"

It includes the inputs priority, caseSummary, and caseId.

To pass arguments by keyword, you could enter the following:

1
2
3
4
5
=rule!feedMessageForNewCase(
  priority: pv!priority,
  caseSummary: pv!summary,
  caseId: pv!id
)

To evaluate into the following:

Priority 1: Basic users cannot connect to server [#100005]

Keywords do not need to be in the order the arguments are defined.

For example, the following still results in the text above:

1
2
3
4
5
=rule!feedMessageForNewCase(
  caseId: pv!id,
  caseSummary: pv!summary,
  priority: pv!priority
)

Optional Arguments

All arguments are optional when passing by keyword. If you do not pass an argument for a parameter, the parameter receives a null value of the parameter type.

For example:

1
2
3
4
=rule!feedMessageForNewCase(
  caseId: pv!id,
  caseSummary: pv!summary
)

Evaluates with the following:

  • [caseId] = pv!id
  • [caseSummary] = pv!summary
  • [priority] = null

Keyword Requirements

The keyword (or rule input name) for a parameter is defined by the designer. They are searched first by the case-sensitive name, then case-insensitive, such that both rules below will evaluate:

1
2
3
4
5
6
7
8
9
=rule!feedMessageForNewCase(
  caseId: pv!id,
  caseSummary: pv!summary
)

=rule!feedMessageForNewCase(
  CASEId: pv!id,
  CASESummary: pv!summary
)

If a keyword is not matched with a parameter name, the argument is ignored and the parameter receives a null value.

When specifying the keyword, use single quotes around it if it contains characters other than letters/numbers/underscores or begins with an underscore (similar to namespaces).

For example:

1
2
3
=rule!person(
  '_firstName': "John"
)

Note:  Passing arguments by keyword is not supported when creating rules for the Web Content Channel, process reports, or events. If used, it will cause the process or task to pause by exception.

Passing functions, rules, and data types as arguments

Appian functions, custom functions, rules, and data types are supported as arguments.

The syntax for passing them is similar to passing variables by inserting the object's domain and pointing to its name similar to the following:

  • fn!sum
  • rule!myrule
  • type!Person

Functions

Most functions can be passed as arguments using the sntax above. For example, passing the sum() function to the reduce() function:

1
=reduce(fn!sum, 0, {1, 2, 3}) yields 6

Note:  The following Appian functions cannot be passed as an argument:

  • a!localVariables() function, such as apply(a!localVariables . . . )
  • load() function, such as apply(fn!load . . . )
  • with() function, such as apply(fn!with . . . )
  • a!save() function, such as apply(a!save . . . )

Rules

All rules can be passed as arguments using the syntax above. For example, passing a rule rule!isnumbereven to the any() function:

1
=any(rule!isnumbereven,{-1,0,1,2}) yields true

If you pass a rule, interface, or decision to an expression and it is later deleted, the expression will still evaluate. If you inspect an import package, however, and it contains expressions that require the deleted rules, the rules will not be listed as missing precedents.

See also: Deleting Expression Rules

Data types

Passing a data type for a type comparison:

1
=if(typeof(ri!input) = type!User, user(ri!input, "email"), group(ri!input, "groupName"))

When using a data type in an expression, data type names are case-sensitive. Specifying the namespace of the data type improves readability but is optional when the name of the data type is unique.

If the data type name is not unique, the system returns a validation error indicating the namespace must be specified. When specifying the namespace, use single quotes around the domain and data type name if the namespace contains characters other than letters/numbers/underscores or begins with an underscore.

For example:

1
 'type!{http://www.appian.com/ae/types/2009}User'

If you pass a data type to an expression and it is later deleted, the expression will still evaluate. If you inspect an import package, however, and it contains expressions that require the deleted data types, the data types will not be listed as missing precedents.

See also: Deleting Data Types

Constructing data type values

To construct values for complex system and custom data types in an expression, create a type constructor. Type constructors accept an argument for each data type field and return a value of the specified data type.

Creating a type constructor is similar to passing arguments to functions except the domain is required for the type constructor to evaluate and data type names are case-sensitive.

For example:

If a Person CDT has the following structure:

1
2
3
4
5
6
7
8
Person
 |- firstName (Text)
 |- lastName (Text)

=type!Person(
  firstName: "John",
  lastName: "Smith"
)

returns [firstName=John, lastName=Smith]

Entering the namespace of the data type is optional. If the name of the data type is unique, the namespace is looked up when the expression is saved and shown when the expression is viewed again.

For example, enter the following rule and save it:

1
2
3
4
=type!Person(
  firstName: "John",
  lastName: "Smith"
)

When you view it again, it shows up as the following:

1
2
3
4
='type!{https://cdt.example.com/suite/types/}Person'(
  firstName: "John",
  lastName: "Smith"
)

If the data type name is not unique, the system prompts you to enter the fully qualified name including namespace when saving the expression. Remember to use single quotes around the full name since the namespace contains special characters.

Tip:  Appian recommends using keyword parameters with type constructors as shown in the above example to ease CDT change management.

See above: Passing Arguments

Deleted data types

When you save an expression that uses a data type value, and the data type is subsequently deleted, the expression continues to reference the deleted data type.

For example, if the data type Person is deleted, the expression in the examples above will show up as the following:

1
2
3
4
='type!{https://cdt.example.com/suite/types/}Person^1'(
  firstName: "John",
  lastName: "Smith"
)

The above is not the case, however, for all expressions. Expressions in third-party credentials and the following objects will not change upon type deletion because they always reference the latest version of a type:

  • Expression rules
  • Interfaces
  • Record types
  • Reports
  • Web APIs

See also: Delete Data Types

Optional arguments

All arguments are optional in a type constructor. Fields that are not assigned a value are set to null.

For example:

1
2
3
4
=type!PagingInfo(
  startIndex: 1,
  batchSize: 2
)

returns [startIndex=1, batchSize=2, sort=]

Complex arguments

For fields that are themselves a complex data type, type constructors can be used to define their values.

For example:

When a Person CDT has the following structure:

1
2
3
4
5
6
7
8
9
10
Person
    |- firstName (Text)
    |- lastName (Text)
    |- address (Address)
        |- street (Text)
        |- city (Text)

=type!Person(
  firstName: "John"
)

returns [firstName=John, lastName=, address=]

1
=isnull(type!Person(firstName: "John").address)

returns true

1
2
3
4
5
6
7
=type!Person(
  firstName: "John",
  address: type!Address(
    street: "123 Abc St",
    city: "Reston"
  )
)

returns [firstName=John, lastName=, address=[street=123 Abc St, city=Reston]]

Best Practice: For readability when creating saving it as a rule, enter line breaks after each argument when passing by keyword.

Extra arguments

If you enter keywords that don't match any fields in the data type, the keywords are ignored.

Note:  Type constructors are not supported when creating rules for the Web Content Channel, process reports, or events. If used, it will cause the process or task to pause by exception.

Parallel evaluation of expressions

In order to evaluate expressions faster, Appian automatically evaluates queries in parallel. So instead of waiting for each query to evaluate one after another, independent queries will be evaluated at the same time if there are additional resources available, reducing the overall time it takes to evaluate the expression. This includes database queries, web service calls, custom plugins, and even Appian functions that query data stored within Appian.

Queries are evaluated in parallel in any place where there's no way for one part of the expression to affect another part, such as:

  • Items in a list
  • Parameters of a function or rule
  • Definitions of local variables
  • Fields within a type constructor or dictionary
  • Iterations of a looping function
  • Rows of a read-only grid

Of course, any parts of the expression that depend on a query will only be evaluated after that query is complete, thus ensuring that the result will be the same regardless of whether the expression is evaluated serially or in parallel.

You may see this reflected in the performance view results for a particular interface. See the performance view page for more information on how to interpret performance results when an interface is evaluated in parallel.

Example

Let's look an example to illustrate how it would be evaluated in parallel. Say you want to get the list of all account managers who don't currently have an active customer account. You may use an expression like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a!localVariables(
  local!customers: {
    rule!getAllDomesticCustomers(),
    rule!getAllInternationalCustomers()
  },
  local!accountManagers: rule!getAccountManagersByRegionAndVertical(
    regions: rule!getAllRegions(),
    verticals: rule!getAllVerticals()
  ),
  local!AMsWithoutAccounts: difference(local!accountManagers, local!customers.manager),
  a!forEach(
    items: local!AMsWithoutAccounts,
    expression: rule!displayUserName(fv!item)
  )
)

where rule!displayUserName calls the user function, which is considered a query because it retrieves user information from Appian. When every part of this expression is evaluated serially, it looks something like this:

However, when there are resources available to evaluate queries in parallel, the evaluation would look like this:

Diagram of how the example expression evaluates in parallel

  1. local!customers and local!allAccountManagers evaluate in parallel because independent local variables are evaluated in parallel when they contain a query.
  2. rule!getAllDomesticCustomers and rule!getAllInternationalCustomers evaluate in parallel because they are separate items in a list and both are queries.
  3. local!AMsWithoutAccounts is not evaluated in parallel because it depends on both local!customers and local!allAccountManagers.
  4. a!forEach iterations evaluate in parallel after evaluating local!AMsWithoutAccounts because rule!displayUserName calls the user function, which is considered a query.

All of this happens without making any changes to the overall expression; Appian can automatically detect which parts of the expression are independent and evaluate them in parallel to decrease the overall evaluation time.

Functions and side effects

Appian expressions have the following properties:

  • The order in which the component parts of an expression are evaluated is not guaranteed.
  • The number of times the components parts of an expression are evaluated is not guaranteed.
  • The number of times the expression as a whole is evaluated is not guaranteed.

Consider the following simple expression:

1
=user(pp!initiator, "firstName") & " " & user(pp!initiator, "lastName")

One might expect that evaluation will proceed from left to right, and thus the user() function returning the first name will be evaluated first, returning the first name, followed by the literal single space and concluding with the second user() function returning the last name. However, the expression's value is the same if evaluation proceeds from right to left. It's even the same if the middle happens first and then the two user() functions are evaluated.

This is only true if expression functions only return values and do not have side effects. A side effect is any other change in the system besides the value a function returns. None of the functions shipped with Appian have side effects. Plug-in developers must ensure their functions conform to this restriction to avoid non-deterministic behavior due to expression evaluation optimizations and compensations.

The following list of cases where functions are not run when one might expect is intended to be illustrative, not exhaustive:

  • When a transient error is encountered running an expression, the platform may immediately evaluate the expression a second time.
  • When users interact with interfaces, parts of the interface expression may be evaluated multiple times.
  • When called multiple times with the same parameters in the same expression, plug-in functions and functions that call external services are only called once and the value is reused.
  • When the if() function is used in an expression, the parameter that is not selected is not evaluated.
  • When an expression contains multiple queries, parts of the expression may be evaluated in parallel.

Although configured using functions, smart services work differently. In interfaces and in certain web APIs, smart services are guaranteed to be executed exactly once. In any other expression, they are not executed. This is because executing a smart service's associated function does not by itself execute the smart service. The function merely returns the desired smart service configuration, and that in turn is executed by the framework if and only if it is in the right place (writer functions work the same way).

Advanced evaluation

The advanced evaluation options below apply mainly to creating dynamic behavior in user interfaces. They work, however, anywhere in the system, except analytics reports and process event nodes.

Partial evaluation of rules and type constructors

With partial evaluation, you can evaluate arguments in a rule or type constructor and defer complete evaluation of the function until the remaining arguments are available. In computer science publications, this is commonly called partial application or partial function application.

For example, when creating a Tempo report using expressions, you may want to create multiple filters the user can interact with in order to narrow down results in a grid, such as filtering items to those updated between two dates.

In this case, you would set up the expression with two date components (such as startDate and endDate ) each passing its value to a rule or data type that requires multiple arguments. The startDate and endDate inputs would save their values into a query that expects both a start date and an end date to filter by.

Normally, if all required arguments are not passed, the expression would not evaluate and the interface would not render.

In order to have your interface render and later accept arguments to the remaining parameters, you need to set up your function for partial evaluation.

To construct a partial function, create the expression as usual and leave an underscore character for arguments to be evaluated later.

Passing arguments by position

Deferring one argument: index(_, {1, 1, 3}, 0)

Deferring two arguments: index(_, _, 0)

Passing arguments by keyword

Deferring one argument: a!pagingInfo(startIndex:_, batchSize:10)

Deferring two arguments: a!pagingInfo(startIndex:_, batchSize:_)

Add the arguments to be evaluated later on to the end of the function and enclose them in parentheses as shown in the examples below:

Filling blank arguments by position

Arguments are applied to the underscores in the order they are listed from left to right.

Partial function that takes one argument:

  • index(_, {1, 1, 3}, 0)({10, 20, 30})

  • a!pagingInfo(startIndex: _, batchSize: 10)(1)

Partial function that takes two arguments:

  • index(_, _, 0)({10, 20, 30}, {1, 1, 3})

  • a!pagingInfo(startIndex: _, batchSize: _)(1, 10)

Filling blank arguments by keyword

Arguments are applied by matching up their keywords. They can only be passed by keyword to partial functions that are defined by keyword.

For example, a!pagingInfo(1, _)(batchSize: 10) is not allowed.

Partial function that takes one argument: a!pagingInfo(startIndex: _, batchSize: 10)(startIndex: 1)

Partial function that takes two arguments: a!pagingInfo(startIndex: _, batchSize: _)(startIndex: 1, batchSize: 10)

When the blank arguments are filled for the examples above, the functions will evaluate as follows:

Passing Arguments by Position: index({10, 20, 30}, {1, 1, 3}, 0)

Passing Arguments by Keyword: a!pagingInfo(startIndex: 1, batchSize: 10)

Arguments that make up a partial function are evaluated immediately. For example, in concat(now(), _), now() returns the time at which the expression is evaluated to return a partial function.

Functions with unlimited parameters

Functions that take an unlimited number of inputs, such as sum(), product(), and difference(), can also be partially evaluated by using the underscore syntax.

Inputs are applied for each underscore in order, and leftovers are added at the end as shown in the examples below:

1
2
3
4
5
6
7
8
9
sum(1, _)(2, 3, 4) returns sum(1, 2, 3, 4)

sum(1, _)(pv!array) returns sum(1, 2, 3, 4) where pv!array is {2, 3, 4}

sum(_, 2)(1, 3, 4) returns sum(1, 2, 3, 4)

sum(_, 2, _)(1, 3, 4) => sum(1, 2, 3, 4)

myrule(_)(1, 2) returns myrule(1, 2)

Best Practice: Appian recommends including the exact number of underscores when the number of arguments is known even though multiple arguments can be passed to a partial function when it is marked with only one underscore. This helps with the readability and maintainability of the expression.

For example, in the following expressions:

  • Accepted: myrule(_, 2, _)(1, 3, 4, 5) which equates to myrule(1, 2, 3, 4, 5)
  • RECOMMENDED: myrule(_, 2, _, _, _)(1, 3, 4, 5) which equates to myrule(1, 2, 3, 4, 5)

Note:  Rules created for the Web Content Channel, operators, and the following functions do not support partial evaluation:

  • a!localVariables() function, such as a!localVariables(local!a:20, _)
  • Operators, such as "Hello"=_

For a function recipe that showcases a looping function that uses partial evaluation, see also: Boolean Results

Indirectly evaluating arguments

Rules can execute a function, rule, data type, or partial function passed as an argument indirectly by way of a rule input. For example, a rule that formats data for a Grid component can indirectly evaluate a rule input function based on the PagingInfo value passed to the rule each time a user interacts with the grid.

See also: Grid Tutorial

To do this, create a rule with an input of type Any Type and define the rule as =ri!inputName() where inputName is the name of your input based on what it will represent. For example gridDataFn.

You can then pass 0, 1, or more parameters to this rule depending on the number of parameters that the function, rule, or data type entered as the inputName expects.

For example, if you have a rule called myrule with the inputs ri!inputName (Any Type) and ri!input (Any Type), and it is defined by =ri!inputName(ri!input) the following expressions evaluate as shown:

myrule( fn!sum, {1, 2, 3}) returns 6

myrule( fn!average, {1, 2, 3}) returns 2

myrule( fn!sum( _, 4), {1, 2, 3}) returns 10

1
2
3
4
5
myrule(
  a!pagingInfo(
    startIndex: _, batchSize: 10),
  1
)

returns [startIndex=1, batchSize=10, sort=]

This functionality is especially useful when defining reusable rules that encapsulate the logic for a interface.

For example, an expression rule is set up to configure a grid component in a UI, and the rows in the grid represent vehicle data. The rule defines the grid columns, label, instructions, etc., and takes as its input a reference to the source of the data. This data will sometimes come from process variables or from an external database through queries. The rule that returns the grid component with vehicle data will then be called as follows:

If the data comes from process variables:

1
=showVehicleGrid( todatasubset( pv!vehicles, _))

If the data comes from a query:

1
=showVehicleGrid( getVehiclesForClient( pv!clientId, _))

If the data comes from a rule that calls a query:

1
=showVehicleGrid( determineVehiclesToShow( _, pv!someInput1, pv!someInput2))

In the above examples, the rule showVehicleGrid takes a partial function that expects one parameter. showVehicleGrid then applies a PagingInfo value to the partial function to return one page of data in a data subset.

Note:  Rules created for the Web Content Channel do not support indirect evaluation.

Permissions used during evaluation

In order for an expression to execute and logic to evaluate when the system attempts to derive information at runtime, the initiator must have sufficient user rights.

For example, if an expression is initiated by a user who does not have sufficient rights to access a resource requested by the expression, it will encounter an exception that halts the evaluation.

When creating an expression for a process model, the designer or task owner may require the rights instead.

See also: User Contexts for Expressions in Process

Testing expressions

The validate button in the Expression Editor only checks for the following:

  • Invalid Function, Rule, and Referenced Literal Objects
  • Failed Casts
  • Duplicate Keywords
  • Invalid Parameters
  • Incorrect Number of Required Arguments

To test the logic of your expressions, create it through the expression rule designer.

See also: Testing an Expression Rule

Feedback