Expressions¶
An expression computes a value — a boolean or a string — from process data
at the moment the engine reaches it. You reach for one to gate which sequence
flow a gateway or task takes, or to derive a value such as a user task's
assignee. Every expression is a data.FormalExpression tagged with a
language URI; the engine routes each to whichever registered engine claims
that URI, so one process can mix text expressions and Go functors freely with
no extra wiring. Full program: examples/expression-routing/.
Taxonomy¶
| BPMN category | Expression → FormalExpression (executable, §10.3.1) |
| Model interface | github.com/dr-dobermann/gobpm/pkg/model/data — data.FormalExpression |
| Routing layer | github.com/dr-dobermann/gobpm/pkg/model/expression — Engine, Registry |
| Text kind | data.TextExpression (a language URI + a body string) |
| Functor kind | data/goexpr.GExpression (a Go func evaluated in-process) |
| Batteries | gobpm:lite (text) and gobpm:goexpr (functor), registered by default |
Where it plugs into the runtime: conditions on flows, gateway branches, and any
consumer that accepts a data.FormalExpression.
The FormalExpression contract¶
Every expression — text or functor — is a data.FormalExpression. Consumers
(flow.WithCondition, activities.WithAssigneeExpr, …) take this interface, so
they never care which engine backs it:
type FormalExpression interface {
foundation.Identifyer
foundation.Documentator
// Language returns the FormalExpression language in URI format.
Language() string
// Evaluate evaluates the expression and returns its result.
Evaluate(ctx context.Context, source Source) (Value, error)
// Result returns the evaluated result (error if not yet evaluated).
Result() (Value, error)
// ResultType returns the name of the result type.
ResultType() string
// IsEvaluated returns true if the result is ready.
IsEvaluated() bool
}
Language() is the routing key. data.Source is the read side of process
data — a single Find(ctx, name) (Data, error) that both engines call through
the same structural-path resolver, so order.customer.tier and
rates["EUR"] resolve identically from text and from Go.
Note: a
TextExpressionrefuses self-evaluation — its ownEvaluatereturns an error. Text bodies are interpreted only through the engine registry, so evaluating one directly would silently bypass language routing. Functors (goexpr) evaluate themselves; the defaultgoexprengine just delegates to theirEvaluate.
Constructors¶
Most models need only the two battery constructors — a lite text condition
and, when Go is easier than a string, a goexpr functor:
| Constructor | Kind · language | Use it for |
|---|---|---|
lite.Cond(body) |
text · gobpm:lite |
a bool condition on a flow (result type pre-declared bool). |
lite.Expr(body) |
text · gobpm:lite |
a plain value expression (a string, a number). |
goexpr.Must(ds, res, fn) |
functor · gobpm:goexpr |
a condition/value written in Go, reading data through data.Source. |
The full set — the raw text constructor and the two functor constructors:
| Constructor | Signature | Notes |
|---|---|---|
lite.Cond |
Cond(body string, opts ...options.Option) (*data.TextExpression, error) |
Expr + a declared bool result. Malformed body → error at construction. |
lite.Expr |
Expr(body string, opts ...options.Option) (*data.TextExpression, error) |
text expression pre-tagged gobpm:lite; result is whatever the body yields. |
data.NewTextExpression |
NewTextExpression(language, body string, opts ...options.Option) (*TextExpression, error) |
the generic text constructor — any language URI (FEEL, JUEL, …); both args required. |
goexpr.New |
New(ds data.Source, res *data.ItemDefinition, gfunc GExpFunc, opts ...options.Option) (*GExpression, error) |
functor expression; res sets the result type, gfunc is func(ctx, ds) (data.Value, error). ds may be nil at construction. |
goexpr.Must |
Must(ds data.Source, res *data.ItemDefinition, gfunc GExpFunc, opts ...options.Option) *GExpression |
panicking New — for package-level or example wiring. |
goexprhere isgithub.com/dr-dobermann/gobpm/pkg/model/data/goexpr(the functor model element). It is a different package frompkg/model/expression/goexpr, which is the default engine that evaluates those functors — you import the former to build one, never the latter.
Build it¶
A condition on a sequence flow gates whether that flow is taken. Mint it
with lite.Cond and attach it with flow.WithCondition:
premiumCond, _ := lite.Cond(
`order.total > 100 and order.customer.tier == "vip"`)
flow.Link(intake, xor, flow.WithCondition(premiumCond))
The same selection point can carry a Go functor beside the text condition —
one flow routed to gobpm:lite, the sibling to gobpm:goexpr. The functor
reads the rates map through the shared resolver (ds.Find):
func eurRateOK() data.FormalExpression {
return goexpr.Must(
nil, // Source bound at eval time
data.MustItemDefinition(values.NewVariable(false)), // result type: bool
func(ctx context.Context, ds data.Source) (data.Value, error) {
d, err := ds.Find(ctx, `rates["EUR"]`)
if err != nil {
return nil, err
}
rate, _ := d.Value().Get(ctx).(float64)
return values.NewVariable(rate < 1.2), nil
})
}
flow.Link(intake, fxAudit, flow.WithCondition(eurRateOK()))
On an exclusive gateway the branches are conditions plus one default flow (the else-branch, taken when no condition holds):
urgentCond, _ := lite.Cond(`deadline < time("2026-12-31T00:00:00Z")`)
flow.Link(xor, urgent, flow.WithCondition(urgentCond))
df, _ := flow.Link(xor, standard)
xor.UpdateDefaultFlow(df)
An expression can also compute a value, not just a boolean. A lite.Expr
string expression derives a user task's assignee per instance —
order.customer.tier + "-manager" resolves to "vip-manager":
assignee, _ := lite.Expr(`order.customer.tier + "-manager"`)
activities.NewUserTask("approve",
activities.WithAssigneeExpr(assignee),
// ...
)
The data these expressions navigate are ordinary process properties — a nested record, a map, a time — built the usual way:
customer, _ := values.NewRecord(values.F("tier", values.NewVariable("vip")))
order, _ := values.NewRecord(
values.F("total", values.NewVariable(500)),
values.F("customer", customer))
rates, _ := values.NewMap(map[string]float64{"EUR": 1.09, "USD": 0.92})
Note: the
lite.*constructors return anerror— a malformed body is rejected at construction, not at run time. Check it; the snippets above elide it for brevity.
Run it¶
cd examples/expression-routing && go run .
The premium condition holds (total 500, tier vip), the deadline branch fires,
and the goexpr lane audits the EUR rate — all three sites in one run:
▶ intake: checking the order
▶ fx-audit: rates["EUR"] < 1.2 (the ##GoExpr functor lane)
▶ urgent: the deadline is near (the lite time() branch)
Approve the urgent order?
✓ expression-routing completed: both engines routed their own languages,
and the lite-computed assignee approved the urgent order
The gobpm:lite text language¶
lite is a small, stdlib-only text language over process data — no runtime
dependencies. It covers the common condition/value needs:
| Feature | Example |
|---|---|
| Numbers · strings · booleans · nil | order.total > 100, status == "open" |
| Times | deadline < time("2026-12-31T00:00:00Z") |
| Structural paths | order.customer.tier, rates["EUR"] |
| Short-circuit booleans | a and b, a or b |
| Builtins | has(order.coupon), len(items), time(...) |
lite.Cond pre-declares a bool result — what flow-gating requires;
lite.Expr yields whatever the body evaluates to. Use Cond on a flow, Expr
where a value is consumed.
Coming from JUEL (a Camunda diagram)¶
gobpm:lite is the engine's answer to JUEL, and the BPMN importer rewrites
JUEL into it rather than interpreting it — so a Camunda-authored condition runs
here without the engine growing a second expression semantics. A file that
declares no expressionLanguage at all, which is what modelers emit, is
recognized by its ${…} delimiters.
| JUEL | gobpm:lite |
|---|---|
${total > 100} |
total > 100 |
${total > 100 && tier == "gold"} |
total > 100 and tier == "gold" |
${!approved \|\| blocked} |
not approved or blocked |
${order.customer.tier == 'vip'} |
unchanged |
${items[0] == "sku-1"} |
unchanged |
${execution.getVariable("total") > 0} |
total > 0 |
What has no counterpart, and is refused by name at import rather than approximated — an approximated condition parses, evaluates, and routes the token the wrong way, somewhere far from the file that caused it:
- the conditional (ternary) operator
a ? b : c; - the
emptyoperator; - the word forms
div,mod,eq,ne,lt,gt,le,ge; - bitwise
&and|; - any call other than the three builtins — a bean method, a
fn:namespace, anything whose behaviour lives in host code the document cannot see; - implicit objects beyond
execution.getVariable("literal"); - a composite
${a}${b}, which is string interpolation, not an expression.
Rewrite those by hand into the equivalent over process data, or keep the behaviour in a Go operation where it belongs.
Routing & engine registry¶
The runtime consumers never talk to a concrete engine — they talk to a
expression.Registry, which is itself an expression.Engine and dispatches by
language:
| Symbol | Role |
|---|---|
expression.Engine |
Type(), Languages() []string, Evaluate(ctx, expr, src) — one engine's language claim + evaluation. |
expression.Registry |
folds every engine's claims into a language → engine map; immutable after New, read lock-free. |
expression.NewRegistry(engines ...Engine) |
rejects a nil engine, an empty claim, or a duplicate claim (two engines answering one language). |
expression.NoneType ("##None") |
the empty registry's kind — every evaluation fails loud. |
Two engines ship in the batteries and are prepended by default: goexpr
(gobpm:goexpr) and lite (gobpm:lite). You extend or replace routing at
engine construction:
Option (pkg/thresher) |
Effect |
|---|---|
WithExpressionEngine(e expression.Engine) |
repeatable — register another engine (FEEL, JUEL, a custom DSL). A duplicate language claim fails construction loud. |
WithoutDefaultExpressionEngines() |
start the registry empty — no batteries; every engine registered by hand. An unclaimed language then fails loud, listing the claims that are registered. |
Building your own engine is its own page: Custom expression engine.
See also¶
- Example:
examples/expression-routing/ - Related guides: Reading & writing by path · Data overview · Exclusive gateway · Custom expression engine
- Design: ADR-032 — Language-routed expression engines
- Full API:
go doc github.com/dr-dobermann/gobpm/pkg/model/expression·go doc github.com/dr-dobermann/gobpm/pkg/model/data