Skip to main content
Varianz stages are written in CEL (Common Expression Language) — sandboxed, deterministic, and safe to push to running services. Expressions are compiled against the target VPoint’s schema, and type and field errors are reported as validation issues.
That reporting is advisory, not a gate. Inserting a stage does not reject a bad expression, and await_sync_or_fail does not fail on one (details) — a stage that fails to compile is skipped by the service, and the test continues as though it had been applied. The coordinator can also only check against schemas it already holds, so a stage inserted before its subscriber registers is not checked at all.For an eager, hard failure while you are authoring an expression, attach it in-process with attach_cel_stage / attachCelStage where the VPoint is registered: that path raises immediately on a parse error, unknown field, or unknown struct type.

Namespaces

args — the VPoint’s inputs. Parameters are reachable only through args:
Parameter names follow the SDK’s schema naming (for example Go’s BasePrice is args.base_price — see each SDK’s field-naming rules). ctx — context functions registered by the service: ctx.applyTax(invoke(), 0.08). See context functions.

Varianz builtins

invoke()

Executes the next stage in the pipeline — or, at the end of the chain, the real function — and returns its result.
  • invoke() with zero arguments forwards the original arguments.
  • invoke(a, b, ...) with exactly the VPoint’s parameter count substitutes different arguments.
  • Any other arity is a compile error.
An expression without invoke() fully replaces the function’s result; the real code never runs.

sample(name, value)

Captures value under the label name for the test session to assert on, and returns value unchanged. Exactly two arguments. Because it’s transparent, it composes anywhere:

Struct construction

  • Type names must match the schema exactly and are case-sensitive. Use the simple name (ChargeResult), never a package-qualified one (pb.ChargeResult, hipstershop.ChargeResult).
  • All required fields must be provided.
  • Nested structs work inline: GetProductOutput { id: "X", price: Money { currency_code: "USD", units: 42, nanos: 0 } }.
On Java and Kotlin, a struct with no fields cannot be constructed. A CEL literal such as Empty {} — or any generated protobuf message with no fields — fails to build, because the JVM processor emits no construction recipe for a fieldless type. Registration aborts for an auto-derived class, and a @Struct-annotated one fails at CEL with VirtualStruct cannot be converted to JObject directly.There is no workaround on the JVM. Python, TypeScript and Go handle fieldless structs correctly.

Supported syntax

Tuple values expose synthetic positional fields: read pair._0, or pair[0] when the schema proves a fixed tuple. Constructing a tuple is the one case where you name the wrapper. That name is derived, the same way in every runtime, from the tuple’s element types under a reserved V_ prefix:
Elements are spelled in CEL’s own vocabulary — bool, int, float, string, bytes, null, any — so every integer width is int and every float width is float. A user struct contributes its short name (orders.Order → Order); a container element is spelled whole, so a tuple over a list of orders is V_Tuple_List_struct_orders_Order_. If your service registers a tuple against a concrete class, that carrier joins the name too: V_Tuple_com_acme_MyTuple_string_int. V_ is reserved for these — don’t start your own struct names with it. Diagnostics, hover and completion use that same CEL vocabulary rather than whatever your runtime calls a type internally, so a string[] in TypeScript, a List<String> in Java and a list[str] in Python all report as list<string>. Containers render list<T>, map<K, V>, T? and (A, B).

Optionals and null at the stage boundary

During evaluation, CEL keeps the standard distinction — optional.of(null).hasValue() is true, optional.none().hasValue() is false. At the stage boundary, however, Varianz transports an optional as either its inner value or absent: both optional.of(null) and optional.none() materialize as absence to the SDK on the other side.

Not yet supported

  • Comprehensions map, filter, exists_one (only exists/all)
  • More than one invoke() per expression — the second fails the call with No next stage found, after the body has already run. Binding it (let p = invoke()) is not a workaround when the VPoint returns a pointer: the bound value arrives wrapped as an optional and its fields are unreachable through the binding, though invoke().field inline works
  • Field access on invoke() when the VPoint returns a single-field struct — that return is flattened to the field’s scalar, so invoke().f fails with Value is not a collection or map
  • Type conversions (int(), string(), …) and date/time functions
  • Error diagnostics carry byte offsets, not line/column

Where CEL runs

CEL stages attach to a VPoint at an anchor — MID by default — scoped to your session. Insert them from any test SDK (cel(target, expression)); target syntax and routing filters are in Naming and routing. The Python and TypeScript SDKs can also compile and run a CEL expression in-process, with no coordinator and no session, via attach_cel_stage / attachCelStage — same language, same schema validation, but the anchor is explicit and the stage applies to every call in that process. See local stages.