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:
cai workflow list --limit 100 --jsonNote 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:
cai flow list --workflow "<workflowId>" --jsonNote the desired flow’s data.items[].id as <flowId>, then inspect it:
cai flow get "<flowId>" --workflow "<workflowId>" --jsonNote the destination’s data.nodes[].id as <nodeId>, then read its context and operations:
cai expr context "<nodeId>" --workflow "<workflowId>" --methods --jsoncai expr ops --workflow "<workflowId>" --type list.custom.invoice --jsonMCP 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.
Choose an operation by type
Section titled “Choose an operation by type”What can text values do?
Section titled “What can text values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
lowercase | :lowercase | .toLowerCase() | text | text | None |
uppercase | :uppercase | .toUpperCase() | text | text | None |
capitalized_words | :capitalized words | .capitalizedWords().capitalize() | text | text | None |
trimmed | :trimmed | .trim() | text | text | None |
slugify | :slugify | .slugify() | text | text | None |
number_of_characters | :number of characters | .length | text | number | None |
converted_to_number | :converted to number | .toNumber() | text | number | None |
converted_to_date | :converted to date | .toDate([format[, timezone]]) | text | date | Format and timezone are text literals. |
converted_to_list | :converted to list | .toList() | text | list.text | None |
format_json_encode | :formatted as JSON-safe | .jsonEncode() | text | text | None |
defaulting_to | :defaulting to | value ?? fallback | text | text | Matching text literal |
equals | is | value === other | text | boolean | Value or expression |
not_equals | is not | value !== other | text | boolean | Value or expression |
contains | contains | .includes(value) | text | boolean | Value or expression |
not_contains | doesn't contain | .notIncludes(value) | text | boolean | Value or expression |
is_empty | is empty | .isEmpty() | text | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | text | boolean | None |
split_by | :split by | .split(value) | text | list.text | Value or expression |
append | :append | .concat(value).append(value) | text | text | Value or expression |
truncated_to | :truncated to | .truncatedTo(length) | text | text | Number literal or expression. |
find_replace | :find/replace | .replace(find, replacement).replaceAll(find, replacement) | text | text | Two text values or expressions. |
What can number values do?
Section titled “What can number values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
floor | :floor | .floor() | number | number | None |
ceiling | :ceiling | .ceiling() | number | number | None |
absolute | :absolute | Math.abs(value) | number | number | None |
rounded_to | :rounded to | .roundedTo([places]) | number | number | Number literal; omitted means 0. |
converted_to_text | :converted to text | .toText() | number | text | None |
converted_to_list | :converted to list | .toList() | number | list.number | None |
plus | + | value + other | number | number | Value or expression |
defaulting_to | :defaulting to | value ?? fallback | number | number | Matching number literal |
minus | - | value - other | number | number | Value or expression |
times | * | value * other | number | number | Value or expression |
divided_by | / | value / other | number | number | Value or expression |
greater_than | > | value > other | number | boolean | Value or expression |
greater_than_or_equal_to | ≥ | value >= other | number | boolean | Value or expression |
less_than | < | value < other | number | boolean | Value or expression |
less_than_or_equal_to | ≤ | value <= other | number | boolean | Value or expression |
equals | is | value === other | number | boolean | Value or expression |
not_equals | is not | value !== other | number | boolean | Value or expression |
is_empty | is empty | .isEmpty() | number | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | number | boolean | None |
What can boolean values do?
Section titled “What can boolean values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
converted_to_text | :converted to text | .toText() | boolean | text | None |
is_true | is true | .isTrue() | boolean | boolean | None |
is_false | is false | .isFalse() | boolean | boolean | None |
format_boolean | :formatted as text | condition ? text : text | boolean | text | Text branches; other mixed branch types produce text. |
format_boolean_number | :formatted as number | condition ? number : number | boolean | number | Two number branches. |
format_json_encode | :formatted as JSON-safe | .jsonEncode() | boolean | text | None |
converted_to_list | :converted to list | .toList() | boolean | list.boolean | None |
defaulting_to | :defaulting to | value ?? fallback | boolean | boolean | Matching boolean literal |
What can date values do?
Section titled “What can date values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
formatted_as | :formatted as | .format(format[, timezone]) | date | text | Literal format and optional literal timezone. |
converted_to_list | :converted to list | .toList() | date | list.date | None |
plus_seconds | + seconds: | .plusSeconds(n) | date | date | Number literal |
plus_minutes | + minutes: | .plusMinutes(n) | date | date | Number literal |
plus_hours | + hours: | .plusHours(n) | date | date | Number literal |
plus_days | + days: | .plusDays(n) | date | date | Number literal |
plus_months | + months: | .plusMonths(n) | date | date | Number literal |
plus_years | + years: | .plusYears(n) | date | date | Number literal |
greater_than | > | value > other | date | boolean | Value or expression |
less_than | < | value < other | date | boolean | Value or expression |
is_empty | is empty | .isEmpty() | date | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | date | boolean | None |
What can list.T values do?
Section titled “What can list.T values do?”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.
| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
count | :count | .length | list.T | number | None |
stringify | :stringify | .stringify() | list.T | text | None |
is_empty | is empty | .isEmpty() | list.T | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | list.T | boolean | None |
sum | :sum | .sum() | list.number | number | None |
average | :average | .average() | list.number | number | None |
first_item | :first item | .first() | list.T | T | None |
last_item | :last item | .last() | list.T | T | None |
item_number | :item # | .item(n) | list.T | T | Number literal |
list_from | :items from # | .itemsFrom(n) | list.T | list.T | Number literal |
list_until | :items until # | .itemsUntil(n) | list.T | list.T | Number literal |
merge_with | :merge with | .concat(other) | list.T | list.T | List expression |
filtered | :filtered | .filter(predicate[, options]) | list.T | list.T | Field predicate; optional ignoreEmptyConstraints boolean. |
all_match | :all match | .every(predicate) | list.T | boolean | Exactly one predicate argument: one condition or flat &&/|| conditions; cannot mix operators. |
each_<fieldId> | :each item's <field> | .map(row => row.field) | list.T | list.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.T | text | Expression body with current item; optional separator. |
list_contains | contains | .includes(value) | list.T | boolean | Value or expression |
join_with | :join with | .join(value) | list.T | text | Value or expression |
sorted | :sorted | .sortBy([field[, "asc" / "desc"]]).sorted([field[, "asc" / "desc"]]) | list.T | list.T | Literal field and direction; ascending by default. |
What can custom.X values do?
Section titled “What can custom.X values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
is_empty | is empty | .isEmpty() | custom.X | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | custom.X | boolean | None |
stringify | :stringify | .stringify() | custom.X | text | None |
What can dataRecord.custom.X values do?
Section titled “What can dataRecord.custom.X values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
stringify | :stringify | .stringify() | dataRecord.custom.X | text | None |
is_empty | is empty | .isEmpty() | dataRecord.custom.X | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | dataRecord.custom.X | boolean | None |
What can file values do?
Section titled “What can file values do?”| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
stringify | :stringify | .stringify() | file | text | None |
is_empty | is empty | .isEmpty() | file | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | file | boolean | None |
What can output.T values do?
Section titled “What can output.T values do?”Take .data before applying operations for T. Compiler acceptance of direct inner operations does not make them work at runtime.
| Operation | App label | Expression spelling | Input type | Result type | Arguments |
|---|---|---|---|---|---|
is_empty | is empty | .isEmpty() | output.T | boolean | None |
is_not_empty | is not empty | .isNotEmpty() | output.T | boolean | None |
Which other spellings compile?
Section titled “Which other spellings compile?”| Input type | Expression spelling | Result type | Meaning |
|---|---|---|---|
number | Math.floor(value) | number | Same operation as the number method. |
number | Math.ceil(value) | number | Same operation as the number method. |
text | Number(value) | number | Text-to-number conversion. |
text | .slice(0, length) | text | Only a zero start is accepted. |
text | text + value | text | Text concatenation. |
boolean | !value | boolean | Tests whether the value is false. |
boolean | condition ? boolean : boolean | boolean | Both branches remain boolean. |
list.T | [n] | T | Number literal; zero selects the first item. |
list.T | .some(predicate) | boolean | Filter, then test for a nonempty result. |
list.T | .find(predicate) | T | Filter, then select the first result. |
Which fields can you read?
Section titled “Which fields can you read?”| Input type | App label | Expression spelling | Result type |
|---|---|---|---|
custom.X | Field display name | .fieldName / ["exact_field_id"] | Shape form of declared field type |
dataRecord.custom.X | Field display name | .fieldName / ["exact_field_id"] | Declared field type |
file | id | .id | text |
file | fileName | .fileName | text |
file | fileType | .fileType | text |
file | mimeType | .mimeType | text |
file | sizeBytes | .sizeBytes | number |
file | url | .url | text |
output.T | data | .data | T |
output.T | error | .error | text |
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 type | Operation | App label | Result type | Expression availability |
|---|---|---|---|---|
custom.X | converted_to_list | :converted to list | list.custom.X | No assistant expression form for wrapping this whole value. |
dataRecord.custom.X | converted_to_list | :converted to list | list.dataRecord.custom.X | No assistant expression form for wrapping this whole value. |
file | converted_to_list | :converted to list | list.file | No assistant expression form for wrapping this whole value. |
output.T | converted_to_list | :converted to list | list.output.T | App 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 type | Expression spelling | Runtime 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?”| Constraint | App label | Picker field types | Predicate spelling | Comparison value |
|---|---|---|---|---|
equals | = | All simple fields | row.field === value | Matching field type |
not_equals | <> | All simple fields | row.field !== value | Matching field type |
contains | contains | string | row.field.includes(value) | Matching field type |
not_contains | doesn't contain | string | !row.field.includes(value) | Matching field type |
is_empty | is empty | All simple fields | row.field.isEmpty() | None |
is_not_empty | isn't empty | All simple fields | row.field.isNotEmpty() | None |
is_in | is in | text, number, string, integer | Engine only; no predicate spelling | Candidate list |
is_not_in | is not in | text, number, string, integer | Engine only; no predicate spelling | Candidate list |
greater_than | > | number, integer | row.field > value | Matching field type |
less_than | < | number, integer | row.field < value | Matching field type |
greater_than_or_equal_to | >= | number, integer | row.field >= value | Matching field type |
less_than_or_equal_to | <= | number, integer | row.field <= value | Matching field type |
contains_item | Not offered by the simple filter picker | — | Engine only; no predicate spelling | One list item |
not_contains_item | Not offered by the simple filter picker | — | Engine only; no predicate spelling | One list item |
empty | Advanced condition | Current item | Engine only; no general predicate spelling | Boolean expression |
Which date formats are presets?
Section titled “Which date formats are presets?”| 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 |
Build one digest from several invoices
Section titled “Build one digest from several invoices”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).
Predict an empty list’s result
Section titled “Predict an empty list’s result”Operation on [] | Result |
|---|---|
.length, .sum() on a number list | 0 |
.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.
Keep dates and wrappers explicit
Section titled “Keep dates and wrappers explicit”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.
Recognize the language boundary
Section titled “Recognize the language boundary”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.