NEXSS FLOW · LANGUAGE
12 min read | Updated 2026-10-01
Language

The nflow Language Reference

The complete nflow DSL reference: pipeline operators (->, ||, &), projection blocks, @arguments, :modifiers, and the directive surface (@require, @include, @assert, @on_error, @config:strict).

dslsyntaxreferenceatomsmodifiersdirectives

What a .nflow file contains

A .nflow file contains Flow directives and a pipeline expression. An atom names an action available to the runner; operators compose them; projections can reshape values between them.

The compiler pre-processes directives, parses the pipeline, and resolves referenced atoms and modifiers against the active registry. Directive and modifier availability can depend on the bundles loaded by a runner; use nflow list to inspect the active catalog rather than relying on a fixed feature list.

Pipeline operators

Six forms. Everything else composes from these.

  • A -> B — pass the left result to the next action.
  • A | B — alias for ->.
  • A || B — try the right expression if the left expression fails.
  • (A & B) — run the grouped branches in parallel.
  • { key: expression } — project fields and expressions into a new object.
  • action @{ key: value } — pass named values as the action's arguments.
  • action:mod=val — apply a modifier to that action.

Parallel results use each branch action's name as its gather key. A name used by only one branch keeps its existing key; repeated names get name#1, name#2, and so on in source order. If a generated label is already in use, trailing # characters are added until it is unique.

nflow nflow
# Built-in parallel composition feature test
( const @{ value: "left" } & const @{ value: "right" } )

Projection blocks

The { ... } block lets you reshape complex JSON on the fly — extracting fields, injecting new values, and merging state without writing any code.

nflow nflow
@assert: result.summary == "Julia (ID: 101) is active"

{
    user: { id: 101, name: "Julia", metadata: { status: "active" } },
    system_flag: true
}
-> {
    id:      .user.id,
    name:    .user.name,
    status:  .user.metadata.status,
    summary: .user.name + " (ID: " + string(.user.id) + ") is " + .user.metadata.status
}

Projection expressions use expr-lang. String concatenation, arithmetic, comparison, and field access all work. .user.id accesses a nested path; string(.user.id) coerces an integer for concatenation.

Named arguments with @{}

Use action @{ ... } to pass named values as the action's typed arguments.

nflow nflow
sleep @{ duration_ms: 5000 }
const @{ value: "hello" }
fail @{ kind: "Validation", message: "User email is invalid" }

Arguments are injected before the validation Before hook, so they join the input map in validation. They are the typed channel for supplying per-call parameters that are not part of the data pipe.

Modifiers

A modifier is one :name or :name=value annotation on an atom. Modifiers mutate the atom's *action.Builder in source order before the action is built. They are the DSL's only mechanism for per-atom configuration that is not an argument and not part of the pipeline topology.

nflow nflow
fetch:timeout=5s:retry=3:auth:role=admin:tag="fast,hot"
  • :timeout=2s — per-action wall-clock deadline.
  • :retry=3 — retry up to N times with exponential jitter backoff.
  • :cache=30s — read-through cache with singleflight stampede protection.
  • :auth — require authenticated context (flag, no value).
  • :role=admin — enforce an RBAC role claim.
  • :name=mykey — override the gather key for parallel branches.
  • :tag="hot,fast" — metadata tags for catalog and observability.
  • :read_only — mark the atom as side-effect free for the runtime.
  • :strict / :lenient — opt in/out of runtime request validation per atom.
Three-layer modifier surface
Parsing primitives live in flow/core (Duration, Int, String, Flag, etc.). Standard modifiers (:timeout=, :retry=, :auth, :role=, ...) live in flow/modifiers. Extension modifiers (:breaker_failures=, :budget=, :model=, ...) live in flow/extensions/<domain>. Nothing in between duplicates either.

Directives

Directives start with @ and run at compile time, before the pipeline is compiled.

  • @description "..." — attach a human-readable description.
  • @assert: expression — check the final result after the flow runs.
  • @pipeline name ... @end — declare a named pipeline.
  • @include "path.nflow" — inline another local Flow file at this position.
  • @require ... — request an extension or library bundle for the flow.
  • @on_error { when error.kind == "X" -> action, ... else -> action } — route failures by error kind.
  • @config:strict=true — enable runtime request validation via validation.Struct.
  • @sandbox name { image, memory_mb, cpus, ... } — declare a named sandbox profile.
nflow nflow
@description "Per-action Timeout and Local Fallback"
@assert: result.recovered == true

{ query: "SELECT * FROM huge_table" }
-> sleep:timeout=1s @{ duration_ms: 5000 }
|| { recovered: true, error: "Database took too long, serving fallback data." }

Routing failures with @on_error

@on_error routes failures to an action by matching error.kind. Combine with retry on the same atom to keep transient failures out of the catch block.

nflow nflow
@on_error {
  when error.kind == "Timeout"     -> error.info
  when error.kind == "Validation"  -> error.info
  else                              -> error.info
}

@assert: result.kind == "Validation"

fail @{ kind: "Validation", message: "User email is invalid" }

The error.info action returns the recovered error as a structured map — useful for logging, metrics, and structured responses. The handler receives the categorized xerr.Kind, not the raw Go error.

Runtime validation with @config:strict

`@config:strict` + `:strict` / `:lenient` enables runtime request validation via validation.Struct. This closes the gap that projections and @{} injections open: the compile-time edge check in pipeline_types.go only catches direct A -> B, not when a projection is in between.

  • Per file: @config:strict=true enables validation for every typed atom in the file.
  • Per atom: consumer:strict opts in even when the file is lenient.
  • Opt-out per atom: helper:lenient disables validation when global strict is on.
  • :lenient always wins over :strict when both appear.

Strict runs only on typed actions (those with TypedPayload.ReqPayload()). An any action has no schema to check, so validation never runs regardless of flags. The error reaches the transport as xerr.KindValidation (HTTP 400).

What strict does not do
It does not validate the response (only the request). It does not run at compile time. It does not block @{} injection (args join validation). It does not check @schema (separate runtime guard for map[string]any).