UserFn expression language
UserFn is Vectara's small expression language for computing values from a JSON context. It started life as the language for the User Defined Function reranker and is now also used to express conditions in agent step transitions and in pipeline verification.
The grammar, parser, and built-in functions are identical across all three callers. What differs is:
| Caller | Expected return | Context the expression reads |
|---|---|---|
| UDF reranker | number (or null to drop the result) | One search result at a time |
| Agent step transitions | boolean | The full agent session |
| Pipeline verification | boolean | The completed worker session |
This page is the syntax and built-in function reference. For the
context schemas — what get() can read in each caller — see the
caller-specific docs linked above and the
caller contexts summary at the bottom.
Quick example
A REPRESENTATIVE EXPRESSION
Code example with sql syntax.1
This reads a metadata field, branches on it, and computes a new
score. The same get(), if, and operators work in step transitions
and verification — only the return type and the available paths change.
Types and literals
UserFn has five types: number, string, boolean, datetime, and
duration. There are no variables — every expression is a single
expression that evaluates to one value.
Literals you can write directly:
| Type | Literal | Notes |
|---|---|---|
number | 2, 2.45, .5 | All numbers are doubles. Integer literals decay to doubles. |
string | 'hello' | Single quotes only. Escape an inner quote by doubling it: 'Vectara''s'. |
boolean | true, false | |
null | null | Returned by get() when a path is missing. See Null behavior. |
datetime and duration have no literal syntax — you produce them
with functions like now(), iso_datetime_parse(), or seconds().
Durations vs numbers
Subtracting two datetimes returns a duration, not a number — this
keeps unit conversions explicit. To turn a duration into a number
(or a number into a duration), use seconds(), minutes(), or
hours(). These functions are bidirectional:
BIDIRECTIONAL DURATION HELPERS
Code example with sql syntax.1
Operators
| Category | Operators | Notes |
|---|---|---|
| Arithmetic | *, /, %, +, - | Standard precedence: *///% bind tighter than +/-. |
| Comparison | <, <=, >, >= | Numbers, datetimes, durations. |
| Equality | ==, =, !=, <> | = is an alias for ==. |
| Logical AND | and, && | Binds tighter than or. |
| Logical OR | or, || | Lowest binding of the logical operators. |
| Logical NOT | !, not | Unary. |
| Grouping | ( ... ) |
There is no ternary ? : operator. Use if (cond) a else b
instead (covered below). Expressions like
x == 'y' ? a : b will fail to parse.
If expressions
if is an expression, not a statement, and both branches are
required. The grammar is if ( expr ) expr else expr.
IF EXPRESSION
Code example with sql syntax.1
There is no then keyword. Expressions like
if x > 0 then a else b will fail to parse — wrap the condition
in parens and drop then: if (x > 0) a else b.
get() — reading from the context
get(path) reads a value from the active context using a
JSONPath string. get(path, default)
returns the default when the path is missing.
get('$.score')
get('$.document_metadata.publish_ts')
get('$.document_metadata.reviews[0].score', 0)
get('$.output.intent')
get('$.tools.approval.outputs.latest.decision')
Three behaviors to design around:
get()returns scalars only. It can return anumber,string,boolean, ornull. Pointing it at an object or array — for exampleget('$.tools.approval.outputs.latest')when the tool output is a JSON object — raises an error. Read a specific leaf field instead. If a tool needs to expose a routing value, surface it at a leaf path.- A missing path returns
null, not an error. That's deliberate: conditions likeget('$.tools.approval.outputs.latest.decision') == 'approved'cleanly evaluate tofalsewhen the tool was never called, so a catch-all branch can handle the "missing" case without extra scaffolding. Provide a default with the two-argument form when you want a different fallback (get('$.score', 0)). - The path is JSONPath against the context root. What lives
at
$depends on which caller invoked UserFn — see caller contexts.
Time functions
| Function | Returns | Description | Example |
|---|---|---|---|
now() | datetime | Current UTC time. | now() |
iso_datetime_parse(s) | datetime | Parse an ISO-8601 string. | iso_datetime_parse('2024-12-04T10:14:50Z') |
datetime_parse(s, format) | datetime | Parse with a Java DateTimeFormatter pattern. | datetime_parse('2024 02 09', 'yyyy MM dd') |
to_unix_timestamp(d) | number | Convert a datetime to seconds since epoch. | to_unix_timestamp(now()) |
extract_datetime_part(d, part) | number | Pull a calendar field out of a datetime. part is one of 'year', 'month', 'day', 'hour', 'minute', 'second', 'day_of_week'. UTC. | extract_datetime_part(now(), 'year') |
seconds(x) | duration or number | If x is a number → duration of that many seconds. If x is a duration → number of seconds in it. | seconds(minutes(1)) == 60 |
minutes(x) | duration or number | Same pattern as seconds. | hours(minutes(60)) == 1 |
hours(x) | duration or number | Same pattern as seconds. | minutes(hours(1)) == 60 |
Recency boosting example
HALVE THE SCORE OF DOCUMENTS OLDER THAN A YEAR
Code example with sql syntax.1
Math functions
| Function | Description | Example |
|---|---|---|
abs(x) | Absolute value. | abs(-123) == 123.0 |
power(a, b) | a raised to the b. | power(2, 3) == 8.0 |
min(a, b) | Smaller of two values. | min(1, 2) == 1.0 |
max(a, b) | Larger of two values. | max(1, 2) == 2.0 |
sqrt(x) | Square root. | sqrt(64) == 8.0 |
trunc(x) | Truncate towards zero. | trunc(1.9) == 1.0 |
sign(x) | -1, 0, or 1. | sign(-2) == -1.0 |
radians(x) | Degrees → radians. | radians(180) ≈ 3.1415 |
degrees(x) | Radians → degrees. | degrees(3.1415926) ≈ 180.0 |
log(base, x) | Logarithm in base. | log(2, 16) == 4.0 |
ln(x) | Natural log. | ln(2.71828) ≈ 1.0 |
log10(x) | Base-10 log. | log10(100) == 2.0 |
sin(x), cos(x), tan(x) | Trig in radians. | sin(1.5707) ≈ 1.0 |
sind(x), cosd(x), tand(x) | Trig in degrees. | sind(90) == 1.0 |
Reranker-only functions
These functions only make sense inside the UDF reranker, because they read per-result data that isn't present in agent step or pipeline verification contexts.
| Function | Description |
|---|---|
knee() | Returns the original score for results before the score knee point, null afterwards (so they're dropped). Uses default sensitivity 0.5 and early-bias 0.2. |
knee(sensitivity, early_bias) | Same, with explicit tuning parameters. Both arguments are numbers between 0.0 and 1.0. |
See the dedicated knee reranking page for tuning guidance and when to use it.
Null behavior
null is a first-class value. Three rules:
- Reading a missing path returns
null. That's how you write conditions that tolerate optional fields without raising errors. - Comparisons against
nullreturnfalse.null == 'approved'isfalse. There is no separate "undefined" vs "null" distinction. - The reranker treats
nullas "drop this result". If the expression returnsnullfor a result, that result is removed from the set before limits are applied and before the next reranker in a chain runs. Step transitions and verification do not have this behavior — they require abooleanreturn.
USE NULL TO FILTER OUT LOW-SCORE RESULTS IN THE RERANKER
Code example with sql syntax.1
Caller contexts
This is a summary of what $ looks like for each caller. Each
caller's own page has more detail and examples.
Reranker context
One search result at a time, repeated for every result in the set.
{
"score": 0.87,
"text": "search result text",
"document_id": "doc_abc123",
"document_metadata": { "...": "..." },
"part_metadata": { "...": "..." }
}
Full schema and examples: UDF reranker.
Agent step transition context
Evaluated once per step turn, after the LLM produces its output.
{
"agent": { "metadata": { "..." : "..." } },
"session": { "metadata": { "..." : "..." } },
"tools": { "<tool_config_name>": { "outputs": { "latest": { "..." : "..." } } } },
"output": { "...": "..." }
}
$.output is the LLM output of the current step — { "text": "..." }
with the default parser, or your structured fields directly under
$.output with a structured parser. Full schema and behavior
notes: steps and transitions.
Pipeline verification context
Evaluated once per worker session, after it completes.
{
"agent": { "key": "my-processor" },
"session": { "key": "pip_a1b2c3d4" },
"output": { "...": "..." },
"tools": { "<tool_name>": { "outputs": { "latest": "..." } } }
}
$.output is the worker agent's final output — a parsed object for
structured-output agents, { "text": "..." } for plain-text agents.
Full schema and the judge-agent alternative:
pipeline verification.
Common pitfalls
- Using a ternary or
then. UserFn has neither. Alwaysif (cond) a else bwith parens around the condition. get()on an object.get('$.tools.x.outputs.latest')errors when the latest output is a JSON object. Read a leaf:get('$.tools.x.outputs.latest.status'). If you control the tool, expose the routing value at a leaf.- Forgetting that step/verification want a boolean. A reranker
expression like
get('$.score') * 1.5is invalid in a step transition — wrap it in a comparison:get('$.score') > 1.0. - String comparisons are case-sensitive.
'Sales' == 'sales'isfalse. Normalize at the source (in your tool output or structured-output schema) rather than trying to lowercase inside the expression — there is no string-manipulation library. - Datetime arithmetic returns durations.
now() - parsed > 1doesn't compile (comparing a duration to a number). Convert first:hours(now() - parsed) > 24.