Skip to main content
Version: 2.0

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:

CallerExpected returnContext the expression reads
UDF rerankernumber (or null to drop the result)One search result at a time
Agent step transitionsbooleanThe full agent session
Pipeline verificationbooleanThe 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:

TypeLiteralNotes
number2, 2.45, .5All numbers are doubles. Integer literals decay to doubles.
string'hello'Single quotes only. Escape an inner quote by doubling it: 'Vectara''s'.
booleantrue, false
nullnullReturned 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

CategoryOperatorsNotes
Arithmetic*, /, %, +, -Standard precedence: *///% bind tighter than +/-.
Comparison<, <=, >, >=Numbers, datetimes, durations.
Equality==, =, !=, <>= is an alias for ==.
Logical ANDand, &&Binds tighter than or.
Logical ORor, ||Lowest binding of the logical operators.
Logical NOT!, notUnary.
Grouping( ... )
warning

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
warning

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 a number, string, boolean, or null. Pointing it at an object or array — for example get('$.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 like get('$.tools.approval.outputs.latest.decision') == 'approved' cleanly evaluate to false when 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

FunctionReturnsDescriptionExample
now()datetimeCurrent UTC time.now()
iso_datetime_parse(s)datetimeParse an ISO-8601 string.iso_datetime_parse('2024-12-04T10:14:50Z')
datetime_parse(s, format)datetimeParse with a Java DateTimeFormatter pattern.datetime_parse('2024 02 09', 'yyyy MM dd')
to_unix_timestamp(d)numberConvert a datetime to seconds since epoch.to_unix_timestamp(now())
extract_datetime_part(d, part)numberPull 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 numberIf 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 numberSame pattern as seconds.hours(minutes(60)) == 1
hours(x)duration or numberSame 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

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

FunctionDescription
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:

  1. Reading a missing path returns null. That's how you write conditions that tolerate optional fields without raising errors.
  2. Comparisons against null return false. null == 'approved' is false. There is no separate "undefined" vs "null" distinction.
  3. The reranker treats null as "drop this result". If the expression returns null for 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 a boolean return.

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. Always if (cond) a else b with 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.5 is invalid in a step transition — wrap it in a comparison: get('$.score') > 1.0.
  • String comparisons are case-sensitive. 'Sales' == 'sales' is false. 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 > 1 doesn't compile (comparing a duration to a number). Convert first: hours(now() - parsed) > 24.