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'
  • 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 (or null), otherwise it does numeric addition.
  • ¤ always concatenates as string (for example 1 ¤ 2 => "12").
  • "ab" * 3 and 3 * "ab" are valid (string repetition).

Operator precedence (high to low):

  1. Unary: -, !
  2. *, /, %
  3. +, -
  4. ¤
  5. <, <=, >, >=
  6. ==, !=
  7. &&
  8. ||

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 io imports one package
  • scope io, stack imports several packages
  • scope clears 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 example arr[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 to expr for branch matching scope.

Collection Postfix Functions

These work on arrays and objects:

  • map
  • associate
  • sum
  • filter
  • bestBy
  • find
  • distinctBy
  • reduce (arrays only)
  • contains
  • take (arrays only)
  • drop (arrays only)
  • takeWhile
  • dropWhile
  • count
  • sortedBy
  • keys (objects only)
  • values (objects only)
  • any
  • all
  • none
  • first
  • last

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:

  • count supports .count() and .count { ... }
  • sum supports .sum() or .sum { ... }
  • associate expects lambda result [key, value] and supports arrays and objects
  • first and last support .first()/.last() and predicate lambdas
  • reduce only supports arrays and returns null for empty arrays
  • contains supports arrays, strings, and object key lookup
  • take and drop only support arrays
  • takeWhile, dropWhile, count, and sortedBy support arrays and objects
  • keys and values only support objects
  • bestBy returns null for empty input
  • filter and find use 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 when must be called with a preceding dot (expr.when { ... }).
  • Inside the when scope, $ is bound to the value of expr.
  • 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, and isNaN remain builtins (not math# 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 as libname#value
  • Optional keyword behavior extensions (keywords list)

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 { ... } (postfix when)

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) — like is String in a Kotlin when. A test matches only its own type; a null subject matches only isNull (or a literal null case).
  • 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 runs f interpolation.
  • Example: 7.f { v -> "value={v}" }

String

Summary

  • len, split, trim, lower, upper, capitalize
  • startsWith, endsWith, replace, pad
  • camel, unCamel, snake, unSnake
  • f (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; fill must 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, sortedBy
  • sum, count, reduce, any, all, none, first, last
  • contains, take, drop, takeWhile, dropWhile
  • join, 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 null on 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: integer or float

count

  • Forms: count(arr), arr.count(), arr.count { i, v -> predicate }
  • Output: integer

reduce

  • Form: arr.reduce { a, n -> expr }
  • Output: reduced value (null for 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, sortedBy
  • sum, count, any, all, none, first, last
  • contains, 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: array of 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: array of distinct values

sortedBy

  • Form: obj.sortedBy { k, v -> key }
  • Output: array of values

sum

  • Forms: sum(obj), obj.sum(), obj.sum { k, v -> numericExpr }
  • Output: integer or float

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: array of 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 when branches.

Null

Summary

  • No null-only built-in function/postfixes beyond the universal section.

Detailed

  • null participates in expressions and when, and can be handled with .or { ... } when an upstream value is unknown.