Skip to content

Expression Operations

Which operation turns an invoice list into a digest? Choose by the value’s type: filter structured rows, select one field with .map(), and join the text list. Each table gives the engine operation, app label, expression spelling an assistant writes, input and result types, and arguments.

In the app: select a node field, click Insert dynamic data, choose the source, then choose an offered operation. Its settings appear in the expression panel. The predicate table identifies comparisons offered by the simple filter picker.

With an assistant: these commands pass --workflow <workflowId> explicitly. Discover the existing Daily invoice digest workflow:

Terminal window
cai workflow list --limit 100 --json

Note its data.items[].id as <workflowId>. If it is absent and data.hasMore is true, find its ID in the app. Then list its flows:

Terminal window
cai flow list --workflow "<workflowId>" --json

Note the desired flow’s data.items[].id as <flowId>, then inspect it:

Terminal window
cai flow get "<flowId>" --workflow "<workflowId>" --json

Note the destination’s data.nodes[].id as <nodeId>, then read its context and operations:

Terminal window
cai expr context "<nodeId>" --workflow "<workflowId>" --methods --json
cai expr ops --workflow "<workflowId>" --type list.custom.invoice --json

MCP uses expr_context with nodeId and methods:true, and expr_operation_list with type:"list.custom.invoice"; both require workflowId as a positive-integer string. The catalog requires at least one node in the workflow.

In signatures, value is the receiver, T the list item or output type, and X a custom type name. Brackets mark optional arguments; arguments exclude the receiver. Follow the result type when chaining. Separate tables identify operations that need the app and compiler spellings with runtime limitations. Validate and inspect resolved values before relying on an expression.

OperationApp labelExpression spellingInput typeResult typeArguments
lowercase:lowercase.toLowerCase()texttextNone
uppercase:uppercase.toUpperCase()texttextNone
capitalized_words:capitalized words.capitalizedWords()
.capitalize()
texttextNone
trimmed:trimmed.trim()texttextNone
slugify:slugify.slugify()texttextNone
number_of_characters:number of characters.lengthtextnumberNone
converted_to_number:converted to number.toNumber()textnumberNone
converted_to_date:converted to date.toDate([format[, timezone]])textdateFormat and timezone are text literals.
converted_to_list:converted to list.toList()textlist.textNone
format_json_encode:formatted as JSON-safe.jsonEncode()texttextNone
defaulting_to:defaulting tovalue ?? fallbacktexttextMatching text literal
equalsisvalue === othertextbooleanValue or expression
not_equalsis notvalue !== othertextbooleanValue or expression
containscontains.includes(value)textbooleanValue or expression
not_containsdoesn't contain.notIncludes(value)textbooleanValue or expression
is_emptyis empty.isEmpty()textbooleanNone
is_not_emptyis not empty.isNotEmpty()textbooleanNone
split_by:split by.split(value)textlist.textValue or expression
append:append.concat(value)
.append(value)
texttextValue or expression
truncated_to:truncated to.truncatedTo(length)texttextNumber literal or expression.
find_replace:find/replace.replace(find, replacement)
.replaceAll(find, replacement)
texttextTwo text values or expressions.
OperationApp labelExpression spellingInput typeResult typeArguments
floor:floor.floor()numbernumberNone
ceiling:ceiling.ceiling()numbernumberNone
absolute:absoluteMath.abs(value)numbernumberNone
rounded_to:rounded to.roundedTo([places])numbernumberNumber literal; omitted means 0.
converted_to_text:converted to text.toText()numbertextNone
converted_to_list:converted to list.toList()numberlist.numberNone
plus+value + othernumbernumberValue or expression
defaulting_to:defaulting tovalue ?? fallbacknumbernumberMatching number literal
minus-value - othernumbernumberValue or expression
times*value * othernumbernumberValue or expression
divided_by/value / othernumbernumberValue or expression
greater_than>value > othernumberbooleanValue or expression
greater_than_or_equal_to≥value >= othernumberbooleanValue or expression
less_than<value < othernumberbooleanValue or expression
less_than_or_equal_to≤value <= othernumberbooleanValue or expression
equalsisvalue === othernumberbooleanValue or expression
not_equalsis notvalue !== othernumberbooleanValue or expression
is_emptyis empty.isEmpty()numberbooleanNone
is_not_emptyis not empty.isNotEmpty()numberbooleanNone
OperationApp labelExpression spellingInput typeResult typeArguments
converted_to_text:converted to text.toText()booleantextNone
is_trueis true.isTrue()booleanbooleanNone
is_falseis false.isFalse()booleanbooleanNone
format_boolean:formatted as textcondition ? text : textbooleantextText branches; other mixed branch types produce text.
format_boolean_number:formatted as numbercondition ? number : numberbooleannumberTwo number branches.
format_json_encode:formatted as JSON-safe.jsonEncode()booleantextNone
converted_to_list:converted to list.toList()booleanlist.booleanNone
defaulting_to:defaulting tovalue ?? fallbackbooleanbooleanMatching boolean literal
OperationApp labelExpression spellingInput typeResult typeArguments
formatted_as:formatted as.format(format[, timezone])datetextLiteral format and optional literal timezone.
converted_to_list:converted to list.toList()datelist.dateNone
plus_seconds+ seconds:.plusSeconds(n)datedateNumber literal
plus_minutes+ minutes:.plusMinutes(n)datedateNumber literal
plus_hours+ hours:.plusHours(n)datedateNumber literal
plus_days+ days:.plusDays(n)datedateNumber literal
plus_months+ months:.plusMonths(n)datedateNumber literal
plus_years+ years:.plusYears(n)datedateNumber literal
greater_than>value > otherdatebooleanValue or expression
less_than<value < otherdatebooleanValue or expression
is_emptyis empty.isEmpty()datebooleanNone
is_not_emptyis not empty.isNotEmpty()datebooleanNone

A missing list is null, not []: both .isEmpty() and .isNotEmpty() return false on null, and a field filter can fail. Require the input or make its producer return []; test null separately from an empty list.

OperationApp labelExpression spellingInput typeResult typeArguments
count:count.lengthlist.TnumberNone
stringify:stringify.stringify()list.TtextNone
is_emptyis empty.isEmpty()list.TbooleanNone
is_not_emptyis not empty.isNotEmpty()list.TbooleanNone
sum:sum.sum()list.numbernumberNone
average:average.average()list.numbernumberNone
first_item:first item.first()list.TTNone
last_item:last item.last()list.TTNone
item_number:item #.item(n)list.TTNumber literal
list_from:items from #.itemsFrom(n)list.Tlist.TNumber literal
list_until:items until #.itemsUntil(n)list.Tlist.TNumber literal
merge_with:merge with.concat(other)list.Tlist.TList expression
filtered:filtered.filter(predicate[, options])list.Tlist.TField predicate; optional ignoreEmptyConstraints boolean.
all_match:all match.every(predicate)list.TbooleanExactly one predicate argument: one condition or flat &&/|| conditions; cannot mix operators.
each_<fieldId>:each item's <field>.map(row => row.field)list.Tlist.fieldType (list fields keep their type)One known field; list fields flatten one level.
format_as_text:format as text.formatAsText(row => text[, separator])list.TtextExpression body with current item; optional separator.
list_containscontains.includes(value)list.TbooleanValue or expression
join_with:join with.join(value)list.TtextValue or expression
sorted:sorted.sortBy([field[, "asc" / "desc"]])
.sorted([field[, "asc" / "desc"]])
list.Tlist.TLiteral field and direction; ascending by default.
OperationApp labelExpression spellingInput typeResult typeArguments
is_emptyis empty.isEmpty()custom.XbooleanNone
is_not_emptyis not empty.isNotEmpty()custom.XbooleanNone
stringify:stringify.stringify()custom.XtextNone
OperationApp labelExpression spellingInput typeResult typeArguments
stringify:stringify.stringify()dataRecord.custom.XtextNone
is_emptyis empty.isEmpty()dataRecord.custom.XbooleanNone
is_not_emptyis not empty.isNotEmpty()dataRecord.custom.XbooleanNone
OperationApp labelExpression spellingInput typeResult typeArguments
stringify:stringify.stringify()filetextNone
is_emptyis empty.isEmpty()filebooleanNone
is_not_emptyis not empty.isNotEmpty()filebooleanNone

Take .data before applying operations for T. Compiler acceptance of direct inner operations does not make them work at runtime.

OperationApp labelExpression spellingInput typeResult typeArguments
is_emptyis empty.isEmpty()output.TbooleanNone
is_not_emptyis not empty.isNotEmpty()output.TbooleanNone
Input typeExpression spellingResult typeMeaning
numberMath.floor(value)numberSame operation as the number method.
numberMath.ceil(value)numberSame operation as the number method.
textNumber(value)numberText-to-number conversion.
text.slice(0, length)textOnly a zero start is accepted.
texttext + valuetextText concatenation.
boolean!valuebooleanTests whether the value is false.
booleancondition ? boolean : booleanbooleanBoth branches remain boolean.
list.T[n]TNumber literal; zero selects the first item.
list.T.some(predicate)booleanFilter, then test for a nonempty result.
list.T.find(predicate)TFilter, then select the first result.
Input typeApp labelExpression spellingResult type
custom.XField display name.fieldName / ["exact_field_id"]Shape form of declared field type
dataRecord.custom.XField display name.fieldName / ["exact_field_id"]Declared field type
fileid.idtext
filefileName.fileNametext
filefileType.fileTypetext
filemimeType.mimeTypetext
filesizeBytes.sizeBytesnumber
fileurl.urltext
output.Tdata.dataT
output.Terror.errortext

Dot access uses an exact field ID, its camelCase spelling, or an unambiguous display alias: row.invoiceId resolves invoice_id. Use ["exact_field_id"] when names are ambiguous.

Which operations need app or wrapper-specific handling?

Section titled “Which operations need app or wrapper-specific handling?”
Input typeOperationApp labelResult typeExpression availability
custom.Xconverted_to_list:converted to listlist.custom.XNo assistant expression form for wrapping this whole value.
dataRecord.custom.Xconverted_to_list:converted to listlist.dataRecord.custom.XNo assistant expression form for wrapping this whole value.
fileconverted_to_list:converted to listlist.fileNo assistant expression form for wrapping this whole value.
output.Tconverted_to_list:converted to listlist.output.TApp operation wraps the whole output. For primitive T, .toList() compiles but infers list.T instead; take .data first.

Which accepted spellings lack a matching type operation?

Section titled “Which accepted spellings lack a matching type operation?”
Input typeExpression spellingRuntime result
boolean.isEmpty()Accepted by the compiler; no matching boolean operation, so evaluation returns an empty result.
any.isEmpty()Generic compiler category, not an engine type; behavior depends on the resolved concrete type.
boolean.isNotEmpty()Accepted by the compiler; no matching boolean operation, so evaluation returns an empty result.
any.isNotEmpty()Generic compiler category, not an engine type; behavior depends on the resolved concrete type.

Which field comparisons can predicates use?

Section titled “Which field comparisons can predicates use?”
ConstraintApp labelPicker field typesPredicate spellingComparison value
equals=All simple fieldsrow.field === valueMatching field type
not_equals<>All simple fieldsrow.field !== valueMatching field type
containscontainsstringrow.field.includes(value)Matching field type
not_containsdoesn't containstring!row.field.includes(value)Matching field type
is_emptyis emptyAll simple fieldsrow.field.isEmpty()None
is_not_emptyisn't emptyAll simple fieldsrow.field.isNotEmpty()None
is_inis intext, number, string, integerEngine only; no predicate spellingCandidate list
is_not_inis not intext, number, string, integerEngine only; no predicate spellingCandidate list
greater_than>number, integerrow.field > valueMatching field type
less_than<number, integerrow.field < valueMatching field type
greater_than_or_equal_to>=number, integerrow.field >= valueMatching field type
less_than_or_equal_to<=number, integerrow.field <= valueMatching field type
contains_itemNot offered by the simple filter picker—Engine only; no predicate spellingOne list item
not_contains_itemNot offered by the simple filter picker—Engine only; no predicate spellingOne list item
emptyAdvanced conditionCurrent itemEngine only; no general predicate spellingBoolean expression
Preset
MM/DD/YY
MM/DD/YYYY
MMM D, YYYY
MMMM D, YYYY
ddd, MMM D, YYYY
ddd D MMMM, YYYY
dddd, MMMM D, YYYY
h:mm a
HH:mm
HH:mm:ss
YYYY-MM-DD
iso_date

Assume context advertises inputs.invoiceQueue with Invoice fields. For the three-row fixture in Write Workflow Expressions, select review rows and list their IDs:

const pending = inputs.invoiceQueue.filter(row => row.reviewNeeded === true);
`Review queue (${pending.length}): ${pending.map(row => row.invoiceId).join(", ")}`

.map() selects one known field from structured items per call; it cannot construct objects or apply arithmetic to each item. Chain selections for nested fields. List-valued fields flatten one level. The assistant expression language rejects primitive-list value predicates such as .filter(n => n > 1000). In the app, choose Filtered → Add advanced condition and compare the current item; with CLI or MCP, use a per-item flow.

Count with .length. Use .item(1) or [0] for the first item. .itemsFrom(n) includes position n; .itemsUntil(n) excludes it, so the first ten items are .itemsUntil(11).

Operation on []Result
.length, .sum() on a number list0
.average(), .first(), .last(), .find(predicate), .item(n)null
.some(predicate)false
.every(validPredicate)true
.join(), .formatAsText(template)""
.stringify()"[]"

Require a row as well as a passing predicate with inputs.invoiceQueue.length > 0 ? inputs.invoiceQueue.every(row => row.reviewNeeded === true) : false; scalar && is unsupported. Out-of-range .item(n) also returns null. Scalar .toList() wraps null as [null].

Decide what an empty predicate should mean

Section titled “Decide what an empty predicate should mean”

Empty-text equality matches empty fields; inequality passes nonempty and missing values; containment of empty text matches every text value. ignoreEmptyConstraints skips null, undefined, empty-text, and empty-list comparison values; it preserves 0, false, whitespace, and dates. In the app, open Filtered and turn on Ignore empty constraints. With an assistant, use the second .filter() argument:

workflowData("custom.invoice").filter(
row => row.status === flow.status,
{ ignoreEmptyConstraints: true }
)

Known-schema field typos fail compilation. Without a known item schema, a field predicate can compile and then be dropped by .filter() at runtime, retaining every row. Removing a field after compilation has the same consequence. .every() fails a missing-field constraint closed and evaluates its list once. Within field predicates, {} and [] count as empty, unlike standalone shape, record, and file presence checks.

Predicates allow flat && or flat ||, without mixing operators. On .filter(), .some(), and .find(), OR allows at most four branches and at most one ordered comparison. ignoreEmptyConstraints is accepted only on .filter(), and never with OR. Filtered OR results are grouped by branch, changing source order; .find() therefore selects by branch priority. .every() takes exactly one predicate argument, which can contain multiple flat conditions.

For a known list-valued field, .includes(singleElement) fails compilation because the comparison value must match the field’s list type. The engine’s list-membership constraints appear separately above.

Use .toDate() for strict ISO text, .toDate("epoch_seconds") for epoch seconds, or .toDate("epoch_millis") for epoch milliseconds. Invalid text returns null. Parsing and formatting default to UTC; an offset or Z embedded in parsed text takes precedence. Use Moment format tokens and an explicit timezone when the displayed day matters:

now.format("YYYY-MM-DD", "America/Chicago")

For output.T, take .data before applying operations for T. The compiler accepts some direct inner operations, but the runtime wrapper does not implement them, producing empty results; its list conversion wraps the whole output. On the wrapper itself, use .data, .error, .isEmpty(), or .isNotEmpty(). Copy the accessor from context. ?? accepts only a text, number, or boolean receiver and a literal fallback of the same type; dates, shapes, records, and lists reject it.

Programs allow const declarations and a final expression. There are no assignments, loops, new Date, regular expressions, optional chaining, arbitrary object/array construction, .reduce(), or JSON.stringify(). The .filter() options object is the object-literal exception. Use .jsonEncode() for complete JSON-safe text tokens and .stringify() for structured values.