- A CEL stage — a small expression, pushed from a test through the coordinator and scoped to your session. This is the portable kind: it crosses process and language boundaries, so it’s what multi-service tests use.
- A local stage — an ordinary Python or TypeScript callback attached in-process, with no coordinator, no session, and no CEL. This is the cheap kind: it only affects the process that attached it, which is exactly right for unit and single-service tests.
The pipeline
Every VPoint call runs through a fixed sequence of anchors — slots where stages can attach:
This is the complete anchor set, and the original function always runs after the last anchor. A stage that produces a value short-circuits the rest of the pipeline; a stage that passes through lets execution continue. With no stages attached, the call takes a fast path straight to the default plan — the original function, exactly as written.
The three CEL stage patterns
Almost every coordinator-backed test uses one of three shapes. Inside a CEL expression,args.<param> reads the call’s inputs and invoke() executes the next stage or the real function.
Override — replace the result entirely. The real function never runs:
sample() captures a value for the test to assert on and returns it unchanged:
Validated against the schema
CEL stages compile against the VPoint’s schema. Type names, field names, and required fields are checked, andChargeResult { txn: ... } is reported as a validation issue if the field is txnId — but that report is advisory: the insert succeeds and the sync barrier still passes. This is why inspecting the schema first (via the MCP tools or your SDK’s build-time output) beats guessing — a wrong field name costs you a silently inert stage rather than a failed insert.
Key rules:
- Inputs are read only through
args.<param>— a bare parameter name is rejected at parse time. - Type names match the schema exactly and are case-sensitive; use the simple name (
ChargeResult, notpb.ChargeResult). - Without
invoke(), the expression fully replaces the function’s result. ctx.<fn>(...)calls context functions your service registered.
let bindings, and current limitations — is in the CEL reference.
Local stages
The Python and TypeScript SDKs can attach a stage in-process, as a plain callback. Nothing is published, so there is no coordinator to run, no session to create, and no sync barrier to wait on — and the callback is ordinary code with a debugger and a stack trace, rather than a CEL string:- The callback receives a context with
ctx.args(the VPoint’s named arguments, as the caller’s live objects),ctx.self(the receiver, for a class-based VPoint), andctx.vp_lookup_name/ctx.vpLookupName. - Python takes argument names from the function signature. TypeScript erases them, so
ctx.argsis keyedarg0,arg1, … unlessvarianz-scanhas run or the VPoint declaresparamsexplicitly. - Return the exported
CONTINUEtoken to fall through to the next stage or the original function. Return anything else — includingNone/undefined— to override the result. - Overrides are converted against the VPoint’s declared return type; a mistyped override fails the call rather than handing the caller the wrong type. This is the callback contract, and for callback stages it holds. For CEL stages it is only partial in Python and in Go — see the warning below.
- Callbacks must be synchronous. An
async def/ a returnedPromisefails the call instead of being silently un-awaited. - Raising propagates to the caller unchanged, which is how you inject failures.
- Attaching returns a stage handle with
set_enabled/setEnabled,is_enabled/isEnabled, andremove. Scope one to a block with Python’swithor Node 23+‘susing; either way it’s removed on exit.
Local stages are not session-scoped: they apply to every call in that process, session or not. That’s the point in a unit test, and the reason not to attach one in a shared or production process. Session isolation, cross-service delivery, and samples are the CEL path.
