Skip to main content

Transform DSL

A flow transforms a document with a list of declarative steps — no code. Steps run in order as a patch-by-default pipeline: each step changes only what it names and leaves the rest of the document intact. Values are expressions, so you can read fields, compose, and branch inline.

# flows/order.yaml
steps:
- _set:
id: { _upper: $id }
name: { _concat: { parts: [$first, $last], sep: ' ' } }
- _when:
cond: { _eq: [$status, new] }
then:
- _set: { priority: high }
else:
- _set: { priority: normal }

Sigils

One rule: _ runs an operator, $ reads a path, plain is literal data.

SigilWhereMeaningExample
$a valueread a path from the doc$order.line[0].sku
_a map keyinvoke an operator (a step, or inside a value){ _concat: [...] }
$$a valuean escaped literal $ (so $$x is the text $x)$$rate
_lita valuereturn the argument verbatim (escape an operator){ _lit: { _x: 1 } }
(none)a map keya literal field name / datastatus: new

Plain arrays and objects are evaluated deeply, so references and operators nest anywhere inside a value. A $path that doesn't exist evaluates to nothing (missing), which _set skips — so copying an optional field is a no-op, not a null write.

Structural steps

Each step is exactly one _-prefixed operator. They are patch operators (keep the rest of the document) except _select, which reshapes.

StepShapeDoes
_set{ <path>: <expr>, ... }set each path; keep everything else
_default{ <path>: <expr>, ... }set each path only where it is currently absent
_unset[<path>, ...]remove paths
_rename{ <from>: <to>, ... }move paths (missing sources are skipped)
_append{ to: <path>, value: <expr> }append to an array (created if absent)
_select{ <path>: <expr>, ... }reshape: output only the named paths
_when{ cond: <expr>, then: [steps], else: [steps] }run a branch by condition (else optional)
_ts{ module, from?, to? }run a custom function

_set/_default/_rename take maps (many paths per step). _set evaluates all its values against the document as it was at the start of the step, so sibling keys are independent.

Value operators

Usable anywhere a value is expected (inside _set/_select/_append/… and in _when.cond).

OperatorShapeResult
reference$a.b[0]the value at that path
_concat[<expr>, ...] or { parts, sep }joined string
_upper / _lower / _trim<expr>transformed string
_toIso<expr>date string → ISO-8601 UTC
_coalesce[<expr>, ...]first non-null
_eq / _gt / _lt[<expr>, <expr>]boolean comparison
_in[<needle>, <arrayExpr>]membership boolean
_exists<expr>true if the value is present
_and / _or[<expr>, ...]boolean over the list
_not<expr>boolean negation
_cond{ if: <expr>, then: <expr>, else: <expr> }a value chosen by a condition

Operators compose: { _cond: { if: { _gt: [$total, 100] }, then: gold, else: standard } }.

Errors

A malformed step or a bad reference fails with a TransformError naming the step and operator; nested _when branches carry their context:

step 1 (_set): "_concat" expects a list or { parts, sep }
step 0 (_when): step 0 (_ts): no function "enrich"

When not to use config

The DSL is for declarative reshaping. Reach for the TypeScript escape hatch (_ts) when a transform needs real logic the operators can't express clearly. If a flow grows a long tail of _when steps faking control flow, that is the signal to drop to code.