Skip to Content
We are live but in Staging πŸŽ‰
Scriptum LanguageExpressions & References

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 β€” results refers 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 discarded

Names 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 field

Because 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 is null when 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() } """ -> result

String 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: continue
decide "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) / 10

Lambdas 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 var

Numeric 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.