Skip to content

Custom Parameters

A custom parameter lets your prose speak your domain’s notation — £2.55, June 6, 2026, LHR — while your steps receive real domain values. You declare it once, next to the state it belongs to, and every step expression in that file can use it as a {name} placeholder.

const { stimulus, sensor } = steps(() => ({ fee: GBP(0) })).param(
'money',
/£\d+\.\d{2}/,
(raw) => GBP(Number.parseFloat(raw.slice(1))),
(m) => `£${m.value.toFixed(2)}`,
)
sensor('owes a {money} late fee', (state) => state.fee)

This page is the reference for the three fields of a custom parameter type and for how a parameter mismatch is rendered. The rules are identical in every port (TypeScript, Java, Kotlin, Python, Ruby).

Field Direction Role
regexp which spans of prose this parameter matches
parse document → value turn the matched text into the value your handlers receive
format value → document turn a value back into the document’s notation (mismatch display)

The pattern that recognizes the parameter inside a sentence.

  • Capture the value. Wrap the part of the match that is the value in the first capture group — that is what your handler receives and the exact span an editor highlights. With no capture group the whole match is the value (that is when you reach for parse). Use non-capturing groups — (?:st|nd|rd|th) — for alternation or repetition you don’t want captured.

  • Matching runs against the raw sentence text — markup and all. #urgent reaches your regexp as #urgent, so when a notation run is your parameter, put the markers in the pattern and a capture group around the data inside:

    param('tag', /#(\w+)/)

    The # stays notation; the group urgent is the value, no parse needed. Markdown emphasis is common enough that it ships built-in as {emph} (see Built-in parameter types) — you don’t hand-roll it. The recipe above is for any other notation that is really data: a hashtag, a ticket id, a wiki link. Markup is notation, exactly like £2.50.

  • The pattern participates in step matching, so keep it anchored to real notation. A pattern that matches everywhere makes every sentence a step candidate.

Turns the matched text into the value your step handlers receive — a number, a date, a domain object. It is a varargs function over the capture groups. If omitted, the value is the first capture group — or the whole matched text when the pattern has none (TypeScript, Python, and Java’s two-argument param; the Kotlin signature always takes a trailing parse lambda).

In TypeScript, the parse function’s return type flows into the handlers: with the money type above, a handler for 'owes a {money} late fee' receives a typed Money argument with no annotation needed.

The transformed value — not the raw text — is what a sensor’s return is deep-compared against.

The inverse of parse: turns a value back into the document’s notation. It has exactly one job — rendering the actual value when a sensor’s return doesn’t match the document.

Without format, a failure has to fall back to a generic rendering of your domain object:

CellMismatchError: cell 1: expected £2.55 but was {"currency":"GBP","value":2.6}

With format, both sides of the diff speak the document’s language:

CellMismatchError: cell 1: expected £2.55 but was £2.60

format never affects whether a comparison passes — the verdict is always deep equality on the transformed values. It only affects how a failure reads.

When a sensor’s returned value differs from a transformed parameter, the failure carries one span-anchored diff per parameter. The expected side is always the document’s own text at the parameter’s span. The actual side is rendered by the first rule that applies:

  1. the parameter type’s format, when it defines one — the document’s notation, identical in every port;
  2. a string value is used as-is;
  3. any other primitive (number, boolean, …) is stringified;
  4. anything else falls back to the port’s native rendering — JSON.stringify in TypeScript, repr() in Python, toString() on the JVM. This keeps the message informative, but it is port-specific: if you care how a domain object reads in a failure, give its parameter type a format.

Because every diff is anchored to a source span, editors highlight the exact failing characters in the Markdown and show the rendered actual value in place. Test frameworks that support an expected/actual pair (vitest, for example) receive the format-rendered strings, so their diff also reads £2.55 vs £2.60; only when no format exists do they receive the raw values instead, so an object mismatch still gets the framework’s structural - Expected / + Received diff rather than two JSON strings.

The Cucumber Expressions built-ins are always available and need no declaration: {int}, {float}, {double}, {byte}, {short}, {long}, {biginteger}, {bigdecimal}, {word}, {string} and the anonymous {}. Each produces the obvious value in its port ({int} is a number in TypeScript, an Integer in Java, an Int in Kotlin, an int in Python). Their values are primitives or strings, so mismatch rendering never needs a format for them.

On top of those, Varar adds one built-in of its own: {emph}, Markdown emphasis in any of its uniform forms — *x*, _x_, **x**, __x__, ***x***, ___x___. Only the inner text reaches your handler (*Emma*Emma; only the outermost delimiter pair is stripped, so **_Emma_**_Emma_), and editors highlight just that value, not the markers. It carries a format that renders a mismatch back as *value*.

Custom parameter types are declared per step file with param, alongside steps — a chained .param() (TypeScript), the returned param function (Python), the binder’s param (Java), or a param call inside the steps block (Kotlin and Ruby). A type declared in one step file is not visible to another file’s expressions — like state, parameter types are a per-file concern.