TellEm Scripting
TellEm is the small expression language embedded in prompts via (>@tellem … <). Two sections below: a syntax introduction and a reference of the builtin functions available inside a prompt. The content mirrors app_modules/libs/tellem/README.md and REF.md — edit those files to update this page.
Introduction
TellEm Scripting Syntax
For contributor and agent implementation rules, see conventions.md.
For LLM/system-prompt style scripting guidance, see docs/cli/language.md.
For OpenCode deployment files, see opencode/README.md.
TellEm is a small expression language for querying/transformation-style scripts.
You can run literals and expressions directly, chain collection operations, bind variables with var, and call host-provided functions.
Quick Examples
2 + 3
var obj = {"a": 5, "b": 7}
obj.a
[2, 3, 4].map { it -> it + 1 }
var payload = {"a": 2, "b": 3}.sum { k, v -> v }
payload.render("summary")
Script Structure
- A script can contain one or more statements.
- Statements are evaluated in order.
- The final statement result is the script result.
Example:
var x = 5
var y = x * 2
y + 1
Literals
- Numbers:
1,42,3.14 - Strings:
- Double-quoted with escapes:
"line\nnext" - Single-quoted exact strings:
'raw text'
- Double-quoted with escapes:
- Booleans:
true,false - Null:
null - Arrays:
[1, 2, "x"] - Objects:
{"a": 1, "b": 2}(keys can be quoted strings or identifiers)
Operators
TellEm supports:
- Arithmetic:
+,-,*,/,% - String concatenation:
¤ - Comparison:
<,<=,>,>=,==,!= - Boolean:
&&,||,! - Unary minus:
-x - Grouping:
( ... )
Notes:
+concatenates if either side is a string (ornull), otherwise it does numeric addition.¤always concatenates as string (for example1 ¤ 2=>"12")."ab" * 3and3 * "ab"are valid (string repetition).
Operator precedence (high to low):
- Unary:
-,! *,/,%+,-¤<,<=,>,>===,!=&&||
Variables (var)
Define a variable:
var name = expression
Example:
var a = 10
var b = a + 5
b
Variables are immutable within the same scope (redefining the same name in that scope fails).
Package Imports (scope)
Use scope to set which installed packages are imported into the root namespace.
scope ioimports one packagescope io, stackimports several packagesscopeclears imports (no packages imported)
A new scope replaces the previous import list and returns TeVoid.
Qualified access using package#name always works regardless of imports.
Paths and Access
Use dotted paths and indexes to access nested values:
obj.a
obj.items[0]
$.docHits[0].relevantSegments[2]
[]accepts an expression inside (for examplearr[i + 1]).*can be used as wildcard in path/index contexts where JSONPath supports it.
CLI stdin Binding ($)
When running a script from -e or a script file, the CLI can preload stdin into $:
- If stdin has data, the CLI reads all of it before execution.
- It tries to parse stdin as JSON first; if parsing fails, stdin is stored as a plain string.
- The parsed value is assigned to the root variable
$.
Examples:
printf '{"docHits":[{"relevantSegments":["x","y","z"]}]}' | tellem -e '$.docHits[0].relevantSegments[2]'
# z
printf 'raw text' | tellem -e '$'
# raw text
Notes:
tellem -and... | tellem(with no script args) use stdin as script source, not as$input data.- Inside
when (expr) { ... },$is rebound toexprfor branch matching scope.
Collection Postfix Functions
These work on arrays and objects:
mapassociatesumfilterbestByfinddistinctByreduce(arrays only)containstake(arrays only)drop(arrays only)takeWhiledropWhilecountsortedBykeys(objects only)values(objects only)anyallnonefirstlast
Lambda Forms
- Value-only:
{ it -> ... }or{ v -> ... } - Key/value:
{ k, v -> ... }
For arrays, k is index and v is item.
For objects, k is key and v is value.
Examples
[2, 3].map { it -> it + 1 } // [3, 4]
["a", "b"].associate { i, v -> [v, i] } // {"a": 0, "b": 1}
[2, 3].sum() // 5
[2, 3].sum { i, v -> i + v } // 6
[2, 3, 4].count() // 3
[2, 3, 4].filter { i, v -> i > 0 } // [3, 4]
[2, 3, 4].bestBy { i, v -> i * 10 + v} // 4
[2, 3, 4].find { i, v -> v > 3 } // 4
[1, 2, 3, 4].distinctBy { i, v -> v % 2 } // [1, 2]
[1, 2, 3, 4].reduce { a, n -> a + n } // 10
[1, 2, 3].reduce { a + n } // 6 (defaults: a, n)
[1, 2, 3].contains(2) // true
"hello".contains("ell") // true
{"a": 1, "b": 2}.contains("a") // true
[1, 2, 3, 4].take(2) // [1, 2]
[1, 2, 3, 4].drop(2) // [3, 4]
[1, 2, 3, 0, 4].takeWhile { i, v -> v > 0 } // [1, 2, 3]
[1, 2, 0, 3, 4].dropWhile { i, v -> v > 0 } // [0, 3, 4]
[1, 2, 3, 4].count { i, v -> v % 2 == 0 } // 2
[3, 1, 2].sortedBy { i, v -> v } // [1, 2, 3]
{"a": 1, "b": 2}.keys() // ["a", "b"]
{"a": 1, "b": 2}.values() // [1, 2]
[2, 3, 4].any { i, v -> v > 3 } // true
[2, 3, 4].all { i, v -> v > 1 } // true
[2, 3, 4].none { i, v -> v > 10 } // true
[2, 3, 4].first() // 2
[2, 3, 4].first { i, v -> v > 2 } // 3
[2, 3, 4].last() // 4
[2, 3, 4].last { i, v -> v > 2 } // 4
Behavior notes:
countsupports.count()and.count { ... }sumsupports.sum()or.sum { ... }associateexpects lambda result[key, value]and supports arrays and objectsfirstandlastsupport.first()/.last()and predicate lambdasreduceonly supports arrays and returnsnullfor empty arrayscontainssupports arrays, strings, and object key lookuptakeanddroponly support arraystakeWhile,dropWhile,count, andsortedBysupport arrays and objectskeysandvaluesonly support objectsbestByreturnsnullfor empty inputfilterandfinduse truthy rules (null/false/0/""are falsey)
Function Calls and Host Callbacks
TellEm can call functions provided by the host through a getter callback.
Direct call:
render("x")
Postfix call:
obj.render("x")
For postfix calls, TellEm passes the source value as the first argument to the callback.
when Keyword
Pattern-style branching is available with when:
when (expr) {
1 -> "one"
2, 3 -> "two-or-three"
else -> "fallback"
}
Postfix form is also supported and requires .:
expr.when {
1 -> "one"
isNumber -> "numeric"
else -> "fallback"
}
- First matching branch wins.
else -> ...can be used as the fallback branch.- Postfix
whenmust be called with a preceding dot (expr.when { ... }). - Inside the
whenscope,$is bound to the value ofexpr. - Predicate helpers are available in
when:isNull,isString,isInteger,isFloat,isNumber,isBoolean,isArray,isObject,isUnknown. - If nothing matches, result is
null.
The expressionless form takes full boolean expressions as conditions and
matches against true, giving if/else and guard chains:
when (cond) { true -> "yes" false -> "no" }
when { x > 10 -> "big" x > 1 -> "some" else -> "none" }
Branch bodies may be plain values, {key: value} object literals, or
{ statement... } blocks whose last expression is the value. Curly
content starting as key: (or empty {}) is an object literal, exactly
as in statement position; anything else is a block.
Error Handling and Strictness
TellEm fails fast on tokenization/parsing syntax errors.
At runtime, unresolved values are represented with TeUnknown instead of throwing.
When a variable or function does not exist at runtime, TellEm returns an unknown value (TeUnknown) with a descriptive message.
By default, CLI output for TeUnknown is that description (for example: variable a is not defined).
Postfix operations on unknown values are no-ops and return the same unknown value.
Use .or { ... } to provide a fallback for unknown or null values, for example: a.or { 5 }.
JSON path resolution is implemented directly in TellEm. Non-existent paths return TeUnknown and can be safely continued.
Getter callbacks should return a TellEmDataType; for undefined functions, return a descriptive TeUnknown.
count is predefined and works both as postfix (arr.count(), obj.count()) and function call (count(arr), count(obj)).
prepend and append are predefined helpers and work with arrays and strings as function calls (prepend(arr, x), append(arr, x), prepend("b", "a"), append("a", "b")) or postfix (arr.prepend(x), arr.append(x), "b".prepend("a"), "a".append("b")).
String helpers are predefined and support both function and postfix forms: len, split, trim, lower, upper, capitalize, startsWith, endsWith, replace (replace-all), pad (pad-start), camel, unCamel, snake, unSnake, lines, and words.
Object key helpers are predefined and support both forms: pick and omit.
Numeric helpers are predefined and support both forms: abs, round, floor, ceil, clamp, min, max, and isNaN.
General helpers include type, not, postfix conditionals (ifTrue {}, ifFalse {}, ifNull {}, ifUnknown {}, ifNaN {}), and postfix lambdas also {} and let {}.
packages is predefined and returns the list of installed library package names.
scopes is predefined and returns the list of currently scoped (imported) package names.
package is predefined and returns built-in callable/value names.
String helper examples:
len("hello") // 5
"a,b,c".split(",") // ["a", "b", "c"]
trim(" hi ") // "hi"
lower("HeLLo") // "hello"
upper("HeLLo") // "HELLO"
capitalize("hELLo") // "Hello"
startsWith("hello", "he") // true
"hello".endsWith("lo") // true
replace("a-b-a", "a", "x") // "x-b-x"
pad("7", 3, "0") // "007"
camel("my HTTP server") // "myHTTPServer"
unCamel("parseJSONValue") // "parse JSON value"
snake("My HTTP Server") // "my_http_server"
unSnake("my_HTTP_server") // "my HTTP server"
lines("a\nb") // ["a", "b"]
" one two three ".words() // ["one", "two", "three"]
only({"a":1,"b":2}, "a") // {"a": 1}
{"a":1,"b":2}.omit("b") // {"a": 1}
type(1.2) // "float"
not(true) // false
abs(-5) // 5
clamp(10, 0, 5) // 5
isNaN(0.0/0.0) // true
false.ifFalse { "fallback" } // "fallback"
[1].drop(1).first().ifNull { 7 } // 7
a.ifUnknown { 9 } // 9 (when a is undefined)
a.ifUnknown { v -> "Error: " ¤ v } // "Error: variable a is not defined" (v is the message)
(0.0/0.0).ifNaN { 11 } // 11
2.let { v * 10 } // 20
The console installs a default io package with positional-parameter functions:
io#dir(pattern?)io#loadJson(name)io#load(name)io#save(data, name)io#mkdir(name)io#puts(value, ...)
io#dir (value form) is equivalent to io#dir().
io#puts prints markdown to stdout (primitives as plain text, arrays/objects as fenced JSON).
The console also installs a default stack package with positional-parameter functions:
stack#push(value)stack#pop()stack#swap()stack#rot()stack#dup()stack#over()stack#drop()
The console also installs a default sys package:
sys#run(command)sys#args
sys#run(command) executes a shell command and returns stdout as a string.
sys#args is a value containing CLI arguments after the script name (for REPL/default library options it is an empty list).
The console also installs a default math package:
- Values:
math#pi,math#e,math#tau - Functions:
math#sqrt,math#pow,math#exp,math#ln,math#log10,math#log,math#sin,math#cos,math#tan,math#asin,math#acos,math#atan,math#atan2
Notes:
- Trigonometric functions use radians.
- Numeric utility helpers like
abs,round,floor,ceil,clamp,min,max, andisNaNremain builtins (notmath#members).
Examples:
math#sqrt(9) // 3
math#pow(2, 10) // 1024
math#sin(math#pi / 2) // 1.0
math#ln(math#e) // 1.0
math#log(8, 2) // 3.0
You can still configure behavior with TellEmOptions:
import com.tellusr.tellem.TellEm
import com.tellusr.tellem.TellEmOptions
val options = TellEmOptions(
allowGetterCallback = true
)
val op = TellEm.compile("var x = 1 x", options)
You can also pass options through TellEmProgram:
val program = TellEmProgram("$.missing", options)
Precompiled Libraries (Alternative to Getter Callback)
If you want to avoid runtime callback lookup, register libraries before compilation.
Libraries can provide:
- Library namespace (
name) - Named functions (
functions()) - Named values (
values()) accessible aslibname#value - Optional keyword behavior extensions (
keywordslist)
Example:
import com.tellusr.tellem.TellEm
import com.tellusr.tellem.TellEmFunctionSchema
import com.tellusr.tellem.TellEmLibrary
import com.tellusr.tellem.TellEmLibraryFunction
import com.tellusr.tellem.datatype.TeInteger
import com.tellusr.tellem.datatype.TeObject
import com.tellusr.tellem.datatype.TeString
val mathLibrary = object : TellEmLibrary {
override val name: String = "math"
override fun functions(): Map<String, TellEmLibraryFunction> = mapOf(
"double" to TellEmLibraryFunction(
handler = { params ->
val n = (params.first() as TeInteger).value
TeInteger(n * 2)
},
schema = TellEmFunctionSchema(
description = "Double an integer",
input = TeObject(mapOf("type" to TeString("array"))),
output = TeObject(mapOf("type" to TeString("integer")))
)
)
)
override fun values() = mapOf(
"version" to TeString("1.0")
)
}
val operation = TellEm.compileWithLibraries("math#double(21)", listOf(mathLibrary))
Function schema metadata is available as values:
math#double.schema
math#double.schema.description
math#schema
math#package
math#values
math#help
math#double.help
package#functionName is not a schema value. It returns TeUnknown unless there is an actual value with that name.
Use package#functionName.schema to inspect schema metadata.
package#help and function.help return schema/help as markdown.
package#package returns package info as an object with name, members, functions, values, and aliases.
lib#schema returns MCP-style tool metadata:
{
"library": "math",
"tools": [
{
"name": "double",
"description": "Double an integer",
"inputSchema": { "type": "array" },
"outputSchema": { "type": "integer" }
}
]
}
Named arguments are supported in function calls:
somefunction(a = 1, b = 2, c = "abc")
Named arguments are converted into a single object and passed to the function as one parameter.
Named arguments are also bound as variables in a fresh function-call scope while evaluating the argument expressions.
For postfix calls, this is bound to the source value in that call scope.
String interpolation uses template literals with an f prefix:
var x = 7
f"x={x}"
Template expressions inside {...} are parsed at script compile time and evaluated at runtime.
Literal braces can be escaped with doubled braces: f"{{x}}".
Equivalent explicit options form:
import com.tellusr.tellem.TellEmOptions
val options = TellEmOptions(
allowGetterCallback = false,
libraries = listOf(mathLibrary)
)
val operation = TellEm.compile("math#version", options)
When allowGetterCallback = false, unknown functions return descriptive TeUnknown values at runtime.
Minimal Grammar (Informal)
program := statement*
statement := varStmt | whenStmt | scopeStmt | expression
varStmt := "var" IDENT "=" expression
whenStmt := "when" "(" expression ")" "{" whenCase+ "}"
scopeStmt := "scope" (IDENT ("," IDENT)*)?
whenCase := expression ("," expression)* "->" expression
expression := value (OP value)*
value := literal
| IDENT_OR_PATH
| "(" expression ")"
| arrayLiteral
| objectLiteral
| functionCall
| postfixChain
postfixChain := value (collectionFn | postfixCall)*
collectionFn := ".map" lambda
| ".associate" lambda
| ".sum" ("()" | lambda)
| ".count" ("()" | lambda)
| ".filter" lambda
| ".bestBy" lambda
| ".find" lambda
| ".distinctBy" lambda
| ".reduce" lambda
| ".contains" "( ... )"
| ".take" "( ... )"
| ".drop" "( ... )"
| ".takeWhile" lambda
| ".dropWhile" lambda
| ".count" lambda
| ".sortedBy" lambda
| ".keys()"
| ".values()"
| ".any" lambda
| ".all" lambda
| ".none" lambda
| ".first" ("()" | lambda)
| ".last" ("()" | lambda)
lambda := "{" IDENT ("," IDENT)? "->" expression "}"
Builtin reference
TellEm Reference
This reference lists built-in functions and postfixes by datatype.
Normal datatypes in this file: string, integer, float, boolean, null, array, object.
unknown is intentionally excluded (it usually propagates, with .or { ... } as the recovery path).
Works With All Normal Datatypes
Summary
value.or { fallbackExpr }value.when { ... }(postfixwhen)
Detailed
or
- Form:
value.or { fallbackExpr } - Input: any datatype as source
- Output: source type or fallback type
- Behavior: returns fallback only when source is
unknown; otherwise returns source unchanged. - Example:
missing.path.or { 0 }
postfix when
- Form:
value.when { case -> expr ... else -> expr } - Input: any datatype as source
- Output: first matching branch result, or
null - Behavior: branch matching with support for type tests (
isNull,isString,isInteger,isFloat,isNumber,isBoolean,isArray,isObject,isUnknown) — likeis Stringin a Kotlinwhen. A test matches only its own type; anullsubject matches onlyisNull(or a literalnullcase). - Example:
x.when { isNumber -> "n" else -> "other" }
postfix f lambda
- Form:
value.f { v -> "template {v}" } - Input: any datatype as source
- Output:
string - Behavior: binds source to lambda var (
v/it), evaluates lambda to template text, then runsfinterpolation. - Example:
7.f { v -> "value={v}" }
String
Summary
len,split,trim,lower,upper,capitalizestartsWith,endsWith,replace,padcamel,unCamel,snake,unSnakef(string template function form)contains,append,prepend
Detailed
len
- Forms:
len(str),str.len() - Output:
integer - Example:
"hello".len()
split
- Forms:
split(str, sep),str.split(sep) - Output:
array<string> - Example:
"a,b".split(",")
trim
- Forms:
trim(str),str.trim() - Output:
string
lower
- Forms:
lower(str),str.lower() - Output:
string
upper
- Forms:
upper(str),str.upper() - Output:
string
capitalize
- Forms:
capitalize(str),str.capitalize() - Output:
string - Behavior: first letter uppercase, rest lowercase.
startsWith
- Forms:
startsWith(str, prefix),str.startsWith(prefix) - Output:
boolean
endsWith
- Forms:
endsWith(str, suffix),str.endsWith(suffix) - Output:
boolean
replace
- Forms:
replace(str, old, new),str.replace(old, new) - Output:
string - Behavior: replaces all exact substring matches.
pad
- Forms:
pad(str, length[, fill]),str.pad(length[, fill]) - Output:
string - Behavior: pad-start semantics;
fillmust be one character.
camel
- Forms:
camel(str),str.camel() - Output:
string - Behavior: space-delimited words -> camelCase; all-uppercase acronyms are preserved.
unCamel
- Forms:
unCamel(str),str.unCamel() - Output:
string - Behavior: camel/Pascal-like text -> space-delimited words, preserving acronym blocks.
snake
- Forms:
snake(str),str.snake() - Output:
string - Behavior: space-delimited words -> lowercase snake_case.
unSnake
- Forms:
unSnake(str),str.unSnake() - Output:
string - Behavior: snake_case -> space-delimited words; uppercase acronym tokens are preserved.
f"..." (template literal)
- Forms:
f"value={expr}" - Output:
string - Behavior: parses
{...}expressions at compile time and evaluates them at runtime.
contains (string mode)
- Forms:
contains(str, part),str.contains(part) - Output:
boolean
append / prepend (string mode)
- Forms:
append(str, suffix),prepend(str, prefix),str.append(suffix),str.prepend(prefix) - Output:
string
Array
Summary
map,associate,filter,bestBy,find,distinctBy,sortedBysum,count,reduce,any,all,none,first,lastcontains,take,drop,takeWhile,dropWhilejoin,reverse,append,prepend
Detailed
map
- Form:
arr.map { i, v -> expr } - Output:
array
associate
- Form:
arr.associate { i, v -> [key, value] } - Output:
object - Behavior: lambda must return a 2-item array
[key, value]; key is stringified.
filter
- Form:
arr.filter { i, v -> predicate } - Output:
array
bestBy
- Form:
arr.bestBy { i, v -> score } - Output: element or
nullon empty input
find
- Form:
arr.find { i, v -> predicate } - Output: first matching element or
null
distinctBy
- Form:
arr.distinctBy { i, v -> key } - Output:
array
sortedBy
- Form:
arr.sortedBy { i, v -> key } - Output:
array
sum
- Forms:
sum(arr),arr.sum(),arr.sum { i, v -> numericExpr } - Output:
integerorfloat
count
- Forms:
count(arr),arr.count(),arr.count { i, v -> predicate } - Output:
integer
reduce
- Form:
arr.reduce { a, n -> expr } - Output: reduced value (
nullfor empty array)
any / all / none
- Forms:
arr.any { ... },arr.all { ... },arr.none { ... } - Output:
boolean
first / last
- Forms:
arr.first(),arr.last(),arr.first { ... },arr.last { ... } - Output: element or
null
contains
- Forms:
contains(arr, value),arr.contains(value) - Output:
boolean
take / drop
- Forms:
arr.take(n),arr.drop(n) - Output:
array
takeWhile / dropWhile
- Forms:
arr.takeWhile { ... },arr.dropWhile { ... } - Output:
array
join
- Forms:
join(arr[, sep]),arr.join([sep]) - Output:
string - Behavior: expects array of strings.
reverse
- Forms:
reverse(arr),arr.reverse() - Output:
array
append / prepend
- Forms:
append(arr, value),prepend(arr, value),arr.append(value),arr.prepend(value) - Output:
array
Object
Summary
map,associate,filter,bestBy,find,distinctBy,sortedBysum,count,any,all,none,first,lastcontains,takeWhile,dropWhile,keys,values
Detailed
All lambda-based collection postfixes use k/v semantics on objects (k is key, v is value).
map
- Form:
obj.map { k, v -> expr } - Output:
array
associate
- Form:
obj.associate { k, v -> [newKey, newValue] } - Output:
object
filter
- Form:
obj.filter { k, v -> predicate } - Output:
arrayof values that pass
bestBy
- Form:
obj.bestBy { k, v -> score } - Output: best value or
null
find
- Form:
obj.find { k, v -> predicate } - Output: first matching value or
null
distinctBy
- Form:
obj.distinctBy { k, v -> key } - Output:
arrayof distinct values
sortedBy
- Form:
obj.sortedBy { k, v -> key } - Output:
arrayof values
sum
- Forms:
sum(obj),obj.sum(),obj.sum { k, v -> numericExpr } - Output:
integerorfloat
count
- Forms:
count(obj),obj.count(),obj.count { k, v -> predicate } - Output:
integer
any / all / none
- Forms:
obj.any { ... },obj.all { ... },obj.none { ... } - Output:
boolean
first / last
- Forms:
obj.first(),obj.last(),obj.first { ... },obj.last { ... } - Output: value or
null
contains
- Forms:
contains(obj, key),obj.contains(key) - Output:
boolean - Behavior: checks key existence.
takeWhile / dropWhile
- Forms:
obj.takeWhile { ... },obj.dropWhile { ... } - Output:
arrayof values
keys / values
- Forms:
keys(obj),values(obj),obj.keys(),obj.values() - Output:
array
Integer / Float
Summary
- No number-only built-in function/postfixes beyond the universal section.
Detailed
- Numbers are commonly used inside lambdas (
sum,map,filter, etc.), but there is no dedicated numeric function family yet.
Boolean
Summary
- No boolean-only built-in function/postfixes beyond the universal section.
Detailed
- Booleans are mainly consumed by predicate lambdas and
whenbranches.
Null
Summary
- No null-only built-in function/postfixes beyond the universal section.
Detailed
nullparticipates in expressions andwhen, and can be handled with.or { ... }when an upstream value isunknown.