Argyll Expressions

Write conditions and transformations with JavaScript-style expressions, collection operations, and conditional selection.

Use Argyll Expressions for Step Predicates, Attribute matches and mappings, and Space selectors. If you know JavaScript, paths, arithmetic, objects, arrays, and boolean expressions will look familiar. The main additions are case, collection operations introduced by |, and value membership with in.

An expression computes one value from its inputs. Use Argyll Expressions for conditions and transformations, and Lua for executable Script Steps.

Familiar expressions

Read values by their input names:

customer.address.country
response["content-type"]
orders[0].id
customer?.address?.country ?? "CH"

Arithmetic and boolean operators use familiar precedence: multiplication before addition, comparisons before equality, and && before ||. Use parentheses to make grouping explicit, especially when mixing ?? with boolean operators.

usage.input_tokens * input_price + usage.output_tokens * output_price
customer_tier == "premium" && order.total >= 100

Construct objects and arrays to shape a mapped result. Object keys can be identifiers or double-quoted strings, and trailing commas are allowed:

{
    customer_id: customer.id,
    country: customer?.address?.country ?? "CH",
    channels: ["email", "sms"],
}

Filter and map collections

Use | filter name: condition to retain array elements and | map name: expression to transform them. The name refers to the current element:

orders | filter order: order.status == "paid"
orders | map order: {id: order.id, total: order.total}

To map the result of a filter, put the complete filter expression in parentheses:

(orders | filter order: order.total > 10) | map order: order.id

Here, map receives the filtered array. The expression after each : is a complete expression. Parentheses mark where it ends before another operation applies to the result. Each collection operation binds its name to the current element while evaluating its expression.

Bindings can read inputs and outer bindings. Nested collections work the same way:

customers | map customer: {
    id: customer.id,
    paid_order_ids: (customer.orders | filter order: order.status == "paid") | map order: order.id,
}

Both operations require arrays and return arrays, even when the result has zero or one element. They preserve element order. Filter conditions must return booleans. For an optional array, supply a fallback before applying the operation:

(response?.items ?? []) | map item: item.id

| introduces a collection operation. || means boolean OR. Use . to read properties, including properties named map or filter, as in response.map and response.filter.

Test collections with any and all

Use | any name: condition to test whether at least one element matches, or | all name: condition to require every element to match:

value.tags | any tag: startsWith(tag, "market:")
results | all result: result.status == "approved"

Both require arrays and boolean conditions. They return booleans and stop as soon as the result is known. On an empty array, any returns false and all returns true. Neither evaluates its condition for an empty array.

The same grouping rules apply. To reject empty input and require every check to pass:

length(checks) > 0 && (checks | all check: check.passed)

Choose a value with case

Use case for conditional selection:

case {
    quality_score >= 0.9: "accept",
    quality_score >= 0.5: "review",
    "reject",
}

Each conditional arm has the form condition: result. Commas separate arms, and the final bare expression is the required fallback. There must be at least one conditional arm. Conditions must be booleans. Conditions are evaluated in order until one is true, then its result is evaluated. When every condition is false, the fallback is evaluated.

Results can be any expression, including objects, arrays, or another case. For example, a mapping can choose a model from the request’s requirements:

case {
    request.requires_reasoning: {model: "reasoning", max_tokens: 4000},
    {model: "fast", max_tokens: 1000},
}

Test array membership with in

in asks whether an array contains a value:

payment_method in ["card", "bank_transfer"]

For a Space selector, membership makes tag tests direct:

"domain:inventory" in value.tags || "domain:orders" in value.tags

Membership compares the left value with the elements of the array on the right. It uses the same equality rules as ==, so objects and arrays are compared by their contents. An empty array returns false. Use includes(text, part) to test a substring and key in keys(object) to test for an object key.

Values and types

Strict booleans

Predicates, case conditions, filters, any, all, and the operators &&, ||, and ! require booleans. Use a comparison to turn a number, string, object, or array into a condition. To test whether an array is nonempty, use:

length(orders) > 0

Use ?? for a missing or null fallback. It preserves 0, false, and "".

Equality compares types and contents

Use == and !=. Values of different types compare unequal, so 1 == "1" is false. Objects and arrays compare structurally: {id: 1} == {id: 1} is true. Addition accepts two numbers or concatenates two strings. Use number(value) and string(value) for explicit conversions.

Strings use double-quoted JSON syntax.

Optional access handles absence

Ordinary access fails on an absent property, missing intermediate value, or out-of-range index. Mark each access that may be absent with ?. or ?.[]:

response?.choices?.[0]?.message?.content ?? ""

Optional access returns a missing value for an absent member or a null or missing receiver. An incorrect receiver or index type raises an error. ?? handles both missing values and explicit JSON null, which are otherwise distinct.

Object construction omits fields whose values are missing. Arrays retain missing elements, which cannot be serialized to JSON. Use a fallback when a mapped array must contain serializable values:

records | map record: record?.email ?? null

Functions

FunctionResult
length(value)Number of array elements, object members, or Unicode code points in a string
abs(number)Absolute value
min(number, ...)Minimum of one or more numbers
max(number, ...)Maximum of one or more numbers
sum(array)Sum of numeric elements, or zero for an empty array
round(number)Nearest integer, with halfway values rounded away from zero
floor(number)Round toward negative infinity
ceil(number)Round toward positive infinity
startsWith(text, prefix)Whether text starts with the prefix
endsWith(text, suffix)Whether text ends with the suffix
includes(text, part)Whether text contains the substring
lower(text)Unicode lowercase form
upper(text)Unicode uppercase form
trim(text)Remove surrounding Unicode whitespace
split(text, separator)Split text on a literal separator into an array of strings
join(array, separator)Join an array of strings with the separator
number(value)Convert a number or a string containing a JSON number to a finite number
string(value)String form of a string, finite number, boolean, or null
keys(object)Property names in lexicographic order
values(object)Values in the same property name order as keys

Call the functions above by name, as in length(items) and startsWith(text, prefix). Use value in items for array membership and includes(text, part) for substrings. String tests are case-sensitive, and an empty search string matches. Case conversion uses locale-independent Unicode character mappings.

For an Attribute match, normalize a payment status before comparing it:

lower(trim(value?.status ?? "")) == "approved"

To combine service results, convert decimal cost strings explicitly and sum them:

sum(results | map result: number(result.cost))

number allows surrounding whitespace but requires the remaining string to follow JSON number syntax. It rejects empty strings, leading zeros such as "01", hexadecimal notation, booleans, null, and non-finite results. string converts null to "null" and booleans to "true" or "false". It rejects missing values, arrays, and objects. Invalid conversions raise an error.

sum requires finite numeric elements and fails if the running total overflows. round rounds halfway values away from zero, so round(-1.5) returns -2 and round(1.5) returns 2.

split preserves empty fields. An empty separator splits into Unicode code points, and splitting empty text with an empty separator returns an empty array. join requires strings and returns an empty string for an empty array. For example:

join(split("inventory,orders", ","), " / ")

This returns "inventory / orders". keys and values produce arrays in lexicographic property name order, including numeric-looking names.

Names available in Argyll

The same syntax runs in several contexts, but each supplies different variables:

ContextAvailable namesExpected result
Step PredicateResolved Step inputs under their invocation names, including constsboolean
Required or optional Attribute matchOne candidate as valueboolean
Input mappingThe selected input under mapping.name, or its Attribute name when no name is suppliedTransformed input value
Output mappingThe entire service response under the output Attribute’s outer nameValue to store for that output Attribute
Space selectorStable Step metadata as valueboolean

A match receives one variable, value, containing its candidate. For example, matching an approved payment result uses value?.status == "approved". A Space selector receives Step metadata as value, so it can use value.type == "service" or test membership in value.tags.

Transform an input

For an input Attribute named price_dollars with mapping.name set to amount, the expression reads the selected price as amount. Convert dollars to cents with:

amount * 100

An input of 12.5 becomes "amount": 1250 in the service request.

Extract an output

For an output mapping, the output Attribute’s name refers to the whole raw response. Given a service response of {"user": {"id": "u-123", "name": "Ada"}}, this mapping stores the nested user object as user_data:

user_data.user

An input mapping failure passes through the original value. An output mapping failure omits that output. Use optional access and explicit fallbacks when absence is expected. A top-level missing result is a mapping failure, while explicit null is a valid result.

Configure a Predicate

Set the language to argyll and place the expression in script. This Predicate runs a Step only for premium customers whose order meets a minimum total:

customer_tier == "premium" && order.total >= 100

In a Step definition, the configuration is:

{
  "predicate": {
    "language": "argyll",
    "script": "customer_tier == \"premium\" && order.total >= 100"
  }
}

Both inputs must be declared by the Step. The same language and script fields are used for Attribute matches, mappings, and Space selectors.

See Attributes for invocation mapping, Predicates for execution and match behavior, and Spaces for selector metadata. The Argyll Expressions language reference includes the full operator precedence and language rules.