Expressions & References
Expressions appear on the right-hand side of every step input, let binding,
emit field, and check condition. This page covers how you name things
(bindings and references), how to reach into structured values (field access,
indexing, safe access), literals, string interpolation, operators, lambdas, and
the built-in functions.
Bindings: the -> operator
-> captures the output of a step and gives it a name. That name is a
binding.
do "Search the web" with web_search
query = topic
-> results -- declares a binding called "results"A binding is:
- named β
resultsrefers to the stepβs full output object. - immutable β once declared it cannot be reassigned (use a new name).
- typed β its type comes from the toolβs output schema.
- scoped top-down β visible to every step that comes after it.
Binding is optional. A step with no -> is fire-and-forget; its output is
discarded.
do "Log event" with echo
message = "job_started"
-- no ->, output is discardedNames must be unique β two steps cannot both -> results. (The one exception is
decide branches, where exactly one branch runs, so multiple branches may bind
the same name; see Control Flow.)
References and lookup order
Writing a bare name like topic is a reference. The compiler resolves it in
this order:
1. Loop / pipe variable (inside `each`/`pipe`: the item, and index)
2. let bindings & step bindings (-> results, let x = ...)
3. Script inputs (the input block: topic, depth, ...)
4. Built-in constants (true, false, null)If the name is not found at any level, it is a compile error (with a βdid you meanβ¦?β suggestion for likely typos).
Field access and indexing
Reach into a bindingβs structure with .field and [index]:
search_output.results -- a field (the list of results)
search_output.results[0] -- index into the list (first item)
search_output.results[0].title -- chained field access
search_output.total -- another fieldBecause the compiler knows the bindingβs type, it validates every field access against the schema. Accessing a field that does not exist is a compile error listing the available fields.
Safe access: ? and ??
Fields that the schema marks optional (nullable) cannot be accessed
directly β the compiler forces you to handle the null case with ? or ??. The
same operators work on untyped object values.
cached = result.cached_at? -- returns null if missing, no error
author = article.author ?? "Unknown" -- supplies a default if null
score = result.relevance ?? 0.0? short-circuits through chains: if any level is null, the whole expression is
null.
nested = response.metadata?.source?.name ?? "unknown"?β nullable access; the expression isnullwhen the field is missing.??β default operator; supplies the right-hand value when the left is null.
Required fields are accessed directly β the compiler guarantees they exist.
Literals
let s = "a string"
let n = 42
let f = 3.14
let b = true
let nada = null
let list = [0.5, 0.3, 0.8, 0.1]
let obj = { source: "web", chunk: 0 }
let nested = { analysis: analysis.text, models: ["kimi-k2.5", "moonshot-v1-auto"] }String literals support escapes (\n, \t, \", \\, and \{ / \} to
write a literal brace). Triple-quoted strings ("""...""") allow multi-line
text without escaping newlines β handy for prompts and inline code:
do "normalize" with python
code = """
def handler(event, context):
return { "out": event.get("text", "").strip().lower() }
"""
-> resultString interpolation
Inside a string, {expr} is replaced with the value of the expression. This is
the workhorse for building prompts, queries, and messages.
do "Deep research" with web_search
query = "{topic} in-depth technical analysis"
-> findings
emit "Progress"
status = "Found {count(findings.results)} results for {topic}"Any expression works inside the braces β field access, function calls,
arithmetic. To write a literal {, escape it as \{.
Operators
Comparison
==, !=, >, <, >=, <= compare two values and produce a boolean.
check "Reachable?" probe.status < 400
on pass: continue
on fail: fail "URL returned error status"Boolean logic
and, or, and not (also written !) combine boolean expressions.
check "Is this urgent?"
mood.urgency == "critical" or intent.category == "outage"
on pass: goto "Escalate to human"
on fail: continuedecide "Encrypt output?" with rules
when encrypt_output == true and encryption_key != "" -> "Encrypt"
otherwise -> "No Encryption"Arithmetic
+, -, *, /, % work on numbers (and - is also unary negation).
let counter = counter + 1
let value = value * 2
let filter_rate = round((passed_filter / total_docs) * 100 * 10) / 10Lambdas and method calls
Higher-order built-ins (map, filter, sort_by, β¦) take a lambda:
param => body, or (a, b) => body for multiple parameters.
let cosine_scores = map(raw_scores, item => item.similarity)
let above_cutoff = filter(scored_docs, doc => doc.similarity > score_cutoff)
let sorted = sort_by(above_cutoff, doc => doc.similarity)Method-call syntax object.method(args) is also available and desugars to a
function call with the object as the first argument β list.map(f) is the same
as map(list, f):
text = extracted_pages.map(p => p.message).join("\n")Built-in functions
Scriptum ships a standard library of pure functions. Common ones:
-- Collections
count(list) -- number of items
first(list) / last(list) -- ends of a list
slice(list, start, end) -- sub-list
join(list, separator) -- list of text β text
flatten(list_of_lists) -- flatten one level
map(list, fn) / filter(list, fn)
sort_by(list, fn) / reverse(list)
group_by(list, fn) / enumerate(list)
-- Objects
keys(object) / values(object)
merge(object, object) -- shallow merge
-- Text
length(text) -- character count
contains(text, substring)
starts_with(text, prefix) / ends_with(text, suffix)
uppercase(text) / lowercase(text) / trim(text)
-- Conversion & inspection
to_text(value) / to_number(text)
type_of(value) -- "text", "number", "list", "object", ...
is_null(value)
now() -- current timestamp
-- Environment
env(NAME) -- read a declared env varNumeric and vector math is also available for data pipelines β for example
round, pow, sqrt, avg, max, argmax, softmax, dot_product,
cosine_similarity, l2_norm, and to_fixed(value, digits):
let mean_score = avg(all_cosines)
let std_dev = sqrt(avg(map(all_cosines, s => pow(s - mean_score, 2))))
let best_idx = argmax(all_cosines)
let probs = softmax(logits)Putting it together
This excerpt scores documents against a query vector using arithmetic,
higher-order functions, and lambdas β no LLM involved. (From
examples/model_pipeline.scriptum.)
let query_vector = [0.5, 0.3, 0.8, 0.1]
each vec, i in doc_vectors
let similarity = cosine_similarity(query_vector, vec)
emit "Score"
doc_index = i
similarity = similarity
-> raw_scores
let cosine_scores = map(raw_scores, item => item.similarity)
let above_cutoff = filter(scored_docs, doc => doc.similarity > score_cutoff)
let sorted = reverse(sort_by(above_cutoff, doc => doc.similarity))
let final_results = slice(sorted, 0, top_k)Next: how these expressions feed into steps, starting with Core Action Steps.