Expression Language
Expression Language
MashinTalk’s expression language is a pure, functional sublanguage for computation inside compute steps, conditions, field defaults, and inline expressions. It follows JavaScript conventions with some additions (pipeline operator, pattern matching, comprehensions, optional chaining). The expression language has no I/O primitives by construction; all I/O happens through governed step types.
When to use
Expressions appear in:
computesteps: the entire body is an expressiondecidestep conditions: thewhenclause is an expressionfilterin read actions: expressions that filter store queriesdefaultvalues: default values on fields can be expressions- String interpolation:
${expr}inside string literals
Operators
Operators are listed from highest precedence (binds tightest) to lowest.
| Precedence | Operators | Notes |
|---|---|---|
| Postfix | . ?. () [] | Property access, optional chaining, function calls, indexing |
| Unary | - !/not ~ typeof | Negation, logical not, bitwise NOT, type inspection |
| Exponent | ** | Right-associative |
| Multiply | * / % | Multiplication, division, modulo |
| Add | + - | Addition, subtraction, string concatenation |
| Shift | << >> | Bitwise left/right shift |
| Range | .. | Optional by for step: 1..10 by 2 |
| Pipeline | |> | Inserts left side as first argument to right side |
| Comparison | < > <= >= | Also: is less than, is greater than, is at most, is at least |
| Membership | in | Collection membership test |
| Equality | == != | Also: is, is not |
| Bitwise AND | & | |
| Bitwise XOR | ^ | |
| Bitwise OR | | | |
| Logical AND | && / and | Short-circuit. Returns left if falsy, right if truthy. |
| Logical OR | || / or | Short-circuit. Returns left if truthy, right if falsy. |
| Nullish | ?? | Returns left if not nil, right if nil |
| Ternary | ? : | Conditional expression |
Natural-language operators
Every symbolic comparison and logical operator has an English alternative that compiles to the same AST. Both forms can be mixed freely.
// These are equivalent:when score >= 0.9 && category != "spam"when score is at least 0.9 and category is not "spam"Truthy/falsy semantics
|| and && use value-returning semantics, not strict boolean. Only nil and false are falsy. 0, "", and [] are all truthy (unlike JavaScript). Use ?? for nil-only coalescing:
value || "default" // returns value if truthy (not nil/false), else "default"value ?? "default" // returns value if not nil (even if value is false)Literals
42 // integer3.14 // decimal"hello" // string"Hello ${name}" // template string with interpolationtrue, false // booleannull // null (compiles to nil)[1, 2, 3] // list{name: "test", x: 1} // object{[key]: "value"} // computed property nameString interpolation
Use ${} inside string literals to embed expressions:
"Hello ${input.name}, you have ${steps.count.total} items""Status: ${status == "active" ? "Active" : "Inactive"}""Score: ${Math.round(score * 100)}%"Any expression can appear inside ${}. The result is converted to a string.
Context access
| Reference | What it accesses | Example |
|---|---|---|
input.<field> | Machine input fields | input.query, input.name |
steps.<name>.<field> | Output from a previous step | steps.classify.category |
steps.<name> | Entire output of a previous step | steps.classify |
state.<field> | Machine state (reactive machines) | state.counter |
context.<field> | Execution context values | context.user_id |
Let bindings
Bind intermediate values to names within a step. Let bindings are scoped to the current step.
compute transform let count = input.items.length let total = input.items.reduce((sum, x) => sum + x.price, 0) let average = total / count {count: count, total: total, average: average}Arrow functions
x => x * 2 // single parameter(a, b) => a + b // multiple parameters() => 42 // no parameters(x, y = 0) => x + y // default parametersx => { let y = x + 1 // block body (last expression is return value) y * 2 }Arrow functions are used with array methods (.map, .filter, .reduce) and anywhere a callback is expected.
Destructuring
// In let bindingslet {name, age} = personlet [first, second] = itemslet [head, ...tail] = list
// In arrow function parametersitems.map(({name, age}) => name + " (" + age + ")")pairs.map(([key, value]) => key + "=" + value)Array methods
Methods dispatched to the Elixir standard library at runtime.
| Method | Description | Example |
|---|---|---|
.map(fn) | Transform each element | items.map(x => x * 2) |
.filter(fn) | Keep elements where fn returns true | items.filter(x => x > 10) |
.reduce(fn, init) | Accumulate a single value | items.reduce((sum, x) => sum + x, 0) |
.find(fn) | First element matching fn | items.find(x => x.id == target) |
.some(fn) | True if any element matches | items.some(x => x.active) |
.every(fn) | True if all elements match | items.every(x => x.valid) |
.includes(val) | True if list contains value | tags.includes("urgent") |
.indexOf(val) | Index of first occurrence (-1 if not found) | items.indexOf("target") |
.sort() | Sort elements | scores.sort() |
.reverse() | Reverse element order | items.reverse() |
.join(sep) | Join elements into a string | words.join(", ") |
.length | Number of elements (property, not method) | items.length |
.flat() | Flatten one level of nesting | nested.flat() |
.flatMap(fn) | Map then flatten | items.flatMap(x => x.tags) |
.slice(start, end) | Extract a sub-list | items.slice(0, 5) |
String methods
| Method | Description | Example |
|---|---|---|
.toUpperCase() | Convert to uppercase | name.toUpperCase() |
.toLowerCase() | Convert to lowercase | email.toLowerCase() |
.trim() | Remove leading/trailing whitespace | input.trim() |
.split(sep) | Split into a list | csv.split(",") |
.startsWith(str) | True if string starts with str | url.startsWith("https") |
.endsWith(str) | True if string ends with str | file.endsWith(".pdf") |
.includes(str) | True if string contains str | body.includes("urgent") |
.replace(old, new) | Replace first occurrence | text.replace("old", "new") |
.replaceAll(old, new) | Replace all occurrences | text.replaceAll(" ", "_") |
.length | Number of characters (property) | name.length |
Object functions
| Function | Description | Example |
|---|---|---|
Object.keys(obj) | List of keys | Object.keys(metadata) |
Object.values(obj) | List of values | Object.values(scores) |
Object.entries(obj) | List of [key, value] pairs | Object.entries(config) |
Math functions
| Function | Description |
|---|---|
Math.abs(x) | Absolute value |
Math.ceil(x) | Round up to nearest integer |
Math.floor(x) | Round down to nearest integer |
Math.round(x) | Round to nearest integer |
Math.trunc(x) | Truncate decimal part |
Math.sqrt(x) | Square root |
Math.sign(x) | Sign of number (-1, 0, or 1) |
Math.sin(x), Math.cos(x), Math.tan(x) | Trigonometric functions |
Math.asin(x), Math.acos(x), Math.atan(x) | Inverse trigonometric |
Math.atan2(y, x) | Two-argument arctangent |
Math.exp(x) | e raised to the power x |
Math.log2(x), Math.log10(x) | Logarithms |
Math.random() | Random number between 0 and 1 |
Math.PI | Pi constant (3.14159…) |
Math.E | Euler’s number (2.71828…) |
JSON functions
| Function | Description | Example |
|---|---|---|
JSON.parse(str) | Parse a JSON string into a value | JSON.parse(input.payload) |
JSON.stringify(obj) | Convert a value to a JSON string | JSON.stringify(result) |
DateTime functions
| Function | Description | Example |
|---|---|---|
DateTime.now() | Current datetime (UTC) | DateTime.now() |
DateTime.today() | Current date (UTC) | DateTime.today() |
Result and Option types
Explicit error and null handling without exceptions.
Result functions
| Function | Description |
|---|---|
Result.ok(v) | Wrap a value as a success |
Result.error(reason) | Wrap a reason as a failure |
Result.is_ok(r) | True if result is ok |
Result.is_error(r) | True if result is an error |
Result.map(r, fn) | Transform the ok value |
Result.and_then(r, fn) | Chain operations that may fail |
Result.unwrap_or(r, default) | Extract value or use default |
Result.unwrap!(r) | Extract value or raise error |
Result.try_fn(fn) | Wrap a function that might throw |
Option functions
| Function | Description |
|---|---|
Option.some(v) | Wrap a value as present |
Option.none() | Represent absence |
Option.is_some(o) | True if value is present |
Option.is_none(o) | True if value is absent |
Option.map(o, fn) | Transform the inner value if present |
Option.and_then(o, fn) | Chain operations on optional values |
Option.unwrap_or(o, default) | Extract value or use default |
Option.from_nullable(v) | Convert a nullable value to an Option |
compute safe_parse let parsed = Result.try_fn(() => JSON.parse(input.text)) let items = Result.map(parsed, data => data.items) let safe = Result.unwrap_or(items, []) {items: safe}Comprehensions
// List comprehensions[x * 2 for x in items][x for x in items if x > 10][x + y for x in list1 for y in list2] // nested (cartesian product)
// Map comprehensions{k: v for {k, v} in entries}{k: v * 10 for {k, v} in prices if v > 0}Match expressions
Pattern matching with first-match semantics. The first arm whose pattern matches wins.
match input.action { "store" => handle_store(input.data) "query" => handle_query(input.query) _ => {error: "unknown action"}}Pattern types
| Pattern | Description | Example |
|---|---|---|
| Literal | Exact value match | "store", 42, true |
_ | Wildcard (matches anything) | _ => default_value |
| Variable | Binds the matched value | x => x * 2 |
| Object destructure | Matches object structure | {type: "billing", amount} => amount |
| Array destructure | Matches array structure | [first, ...rest] => first |
null | Matches nil | null => "no data" |
| Nested | Patterns inside patterns | {items: [first, ...rest]} => first |
match input.payload { {type: "billing", amount} => {team: "billing", value: amount} {type: "urgent", data} => {team: "escalation", context: data} [first, ...rest] => {team: first, overflow: rest} null => {team: "unassigned", error: "no payload"} _ => {team: "general"}}Spread operator
// Object spread (merge/override)let merged = {...base, key: "override", extra: true}
// Array spread (concatenation)let extended = [...list, newItem, ...moreItems]typeof operator
Returns a string naming the runtime type.
typeof "hello" // "string"typeof 42 // "number"typeof true // "boolean"typeof [1, 2] // "list"typeof {a: 1} // "map"typeof null // "null"typeof (x => x) // "function"Optional chaining
Safe navigation for nested property access. Returns null if any intermediate value is nil.
input.user?.address?.city // null if user or address is nilsteps.lookup?.result?.name // null if lookup failedPipeline operator
Inserts the left-hand value as the first argument to the right-hand function.
input.text |> trim() |> toLowerCase() |> split(" ") |> filter(word => word.length > 3)Control flow in expressions
// Ternarycondition ? true_value : false_value
// If/else (expression form)if (condition) { then_branch } else { else_branch }
// Nullish coalescingvalue ?? default_when_nullfail() function
Immediately halts the step with an error. Triggers on_failure handlers or flow-level error handling.
compute validate let valid = input.age > 0 && input.age < 150 if (!valid) fail("Invalid age: must be between 0 and 150") {validated: true}Examples
Data transformation pipeline
compute analyze let items = input.orders.filter(o => o.status == "completed") let total = items.reduce((sum, o) => sum + o.amount, 0) let avg = total / items.length let top = items.filter(o => o.amount > avg).map(o => o.id) { completed_count: items.length, total_revenue: total, average_order: avg, above_average_ids: top }String processing
compute format_name let parts = input.full_name.trim().split(" ") let first = parts[0] let last = parts.length > 1 ? parts[parts.length - 1] : "" { first_name: first, last_name: last, display: "${first} ${last.length > 0 ? last[0] + "." : ""}", initials: first[0].toUpperCase() + (last.length > 0 ? last[0].toUpperCase() : "") }Pattern matching with computed values
compute route let priority = input.amount > 10000 ? "high" : input.amount > 1000 ? "medium" : "low" match priority { "high" => {team: "senior", sla_hours: 4} "medium" => {team: "standard", sla_hours: 24} "low" => {team: "queue", sla_hours: 72} }Governance
The expression language is pure by construction. It has no I/O primitives, no network access, no file system access, and no database access. This purity is not enforced by a sandbox; the capability simply does not exist.
This means:
- Expressions never require governance approval
- Expressions never appear in permission checks
- Expressions always execute, regardless of trust level
- Expressions produce the same output for the same input (deterministic), except for
Math.random()andDateTime.now()
All I/O happens through governed step types (ask, recall, remember, launch, store actions). The expression language computes; machines effect.
See also
- compute - The primary step type for expressions
- decide - Branching logic using expression conditions
- Field Types - Type system for fields
- defines - Machine-level shared functions used in expressions