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
| Function | Result |
|---|---|
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:
| Context | Available names | Expected result |
|---|---|---|
| Step Predicate | Resolved Step inputs under their invocation names, including consts | boolean |
| Required or optional Attribute match | One candidate as value | boolean |
| Input mapping | The selected input under mapping.name, or its Attribute name when no name is supplied | Transformed input value |
| Output mapping | The entire service response under the output Attribute’s outer name | Value to store for that output Attribute |
| Space selector | Stable Step metadata as value | boolean |
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.