⚠️ mlld is pre-release
Join the discord if you have questions. Report bugs on GitHub.
Control Flow
tldr
mlld provides three control flow mechanisms: conditionals (/when), iteration (/for and foreach), and pipelines (|). Use /when for decisions, /for for actions per item, foreach for transforming collections, and pipelines for chaining transformations with retry logic.
Conditionals
Basic When
Use /when with => for simple conditionals:
/var @score = 85
/when @score > 80 => show "Excellent work!"
When First (Switch-Style)
Use /when first to stop at the first matching condition:
/var @role = "admin"
/when first [
@role == "admin" => show "✓ Admin access granted"
@role == "user" => show "User access granted"
* => show "Access denied"
]
The * wildcard matches anything (catch-all). Use none when no conditions matched:
/var @status = "unknown"
/when first [
@status == "active" => show "Service running"
@status == "inactive" => show "Service stopped"
none => show "Unknown status"
]
When All (Bare Form)
Without first, all matching conditions execute:
/var @score = 95
/when [
@score > 90 => show "Excellent!"
@score > 80 => show "Above average!"
@score == 95 => show "Perfect score!"
]
Output:
Excellent!
Above average!
Perfect score!
Executable When Patterns
Use /exe...when to create value-returning conditional functions:
/exe @classify(score) = when first [
@score >= 90 => "A"
@score >= 80 => "B"
@score >= 70 => "C"
* => "F"
]
/var @grade = @classify(85)
/show @grade
Output:
B
Complex conditions with operators:
/var @tokens = 1200
/var @mode = "production"
/when (@tokens > 1000 && @mode == "production") => show "High usage alert"
/var @role = "editor"
/var @isActive = true
/when (@role == "admin" || @role == "editor") && @isActive => show "Can edit"
Local Variables in When Blocks
Use let to declare local variables scoped to a when block:
/var @mode = "active"
/when @mode: [
let @prefix = "Status:"
"active" => show "@prefix Active"
* => show "@prefix Unknown"
]
Output:
Status: Active
Local variables work in all when forms:
/exe @format(name) = when [
let @greeting = "Hello"
let @punctuation = "!"
* => "@greeting @name@punctuation"
]
/show @format("World")
Output:
Hello World!
let variables are scoped to their when block and don't persist outside:
/var @status = "ok"
/when @status: [
let @msg = "Completed"
"ok" => show @msg
]
# @msg is not accessible here
Iteration
For Loops
Use /for to execute actions for each item:
/var @fruits = ["apple", "banana", "cherry"]
/for @fruit in @fruits => show `Fruit: @fruit`
Output:
Fruit: apple
Fruit: banana
Fruit: cherry
Object Iteration with Keys
When iterating objects, access keys with _key:
/var @config = {"host": "localhost", "port": 3000}
/for @value in @config => show `@value_key: @value`
Output:
host: localhost
port: 3000
Nested Loops
Chain multiple /for loops for nested iteration:
/for @x in ["red", "blue"] => for @y in [1, 2] => for @z in ["a", "b"] => show "@x-@y-@z"
Output:
red-1-a
red-1-b
red-2-a
red-2-b
blue-1-a
blue-1-b
blue-2-a
blue-2-b
Collection Form
Use for (without /) to collect results into an array:
/var @numbers = [1, 2, 3, 4]
/var @doubled = for @n in @numbers => js { return @n * 2 }
/show @doubled
Output:
[2, 4, 6, 8]
Parallel /for
Run iterations in parallel with an optional per-loop cap and pacing between starts. Use the directive form for side effects (order may vary) or the collection form for ordered results.
/exe @upper(s) = js { return String(s).toUpperCase() }
# Directive form (streams as done; order not guaranteed)
/for parallel @x in ["a","b","c","d"] => show @x
# Cap override and pacing between task starts
/for parallel(2, 1s) @n in [1,2,3,4] => show `Item: @n`
# Collection form (preserves input order)
/var @res = for parallel(2) @x in ["x","y","z"] => @upper(@x)
/show @res
Output (collection form):
["X","Y","Z"]
Foreach Transforms
Use foreach to transform collections with templates or executables:
/var @names = ["Alice", "Bob", "Charlie"]
/exe @greeting(name) = :::{{name}}, welcome to the team!:::
/var @welcomes = foreach @greeting(@names)
/show @welcomes
Output:
["Alice, welcome to the team!", "Bob, welcome to the team!", "Charlie, welcome to the team!"]
Batch Pipelines on Results
Add => | after the per-item expression to run a pipeline on the collected results. The batch stage receives the gathered array (or object) directly, so helpers can work with native values while .text stays available if you need the string form.
/exe @wrap(x) = js { return [x, x * 2]; }
/exe @flat(values) = js {
if (!Array.isArray(values)) throw new Error('expected array input');
return values.flat();
}
/var @pairs = for @x in [1, 2, 3] => @wrap(@x) => | @flat
/show @pairs
Output:
[
1,
2,
2,
4,
3,
6
]
Batch pipelines can also collapse results to a single value:
/exe @sum(values) = js {
if (!Array.isArray(values)) throw new Error('expected array input');
return values.reduce((total, value) => total + Number(value), 0);
}
/var @total = for @n in [1, 2, 3, 4] => @n => | @sum
/show @total
Output:
10
foreach uses the same syntax:
/exe @duplicate(item) = js { return [item, item.toUpperCase()]; }
/exe @flat(values) = js {
if (!Array.isArray(values)) throw new Error('expected array input');
return values.flat();
}
/var @names = ["one", "two"]
/var @result = foreach @duplicate(@names) => | @flat
/show @result
Output:
[
"one",
"ONE",
"two",
"TWO"
]
Multiple parameters:
/var @greetings = ["Hello", "Hi", "Hey"]
/var @names = ["Alice", "Bob", "Charlie"]
/exe @custom_greeting(greet, name) = :::{{greet}}, {{name}}! Nice to see you.:::
/var @messages = foreach @custom_greeting(@greetings, @names)
/show @messages
Define foreach in /exe and invoke it:
/exe @wrap(x) = `[@x]`
/exe @wrapAll(items) = foreach @wrap(@items)
/show @wrapAll(["a","b"]) | @join(',') # => [a],[b]
Use /show foreach with options:
/var @names = ["Ann","Ben"]
/exe @greet(n) = `Hello @n`
/show foreach @greet(@names) with { separator: " | ", template: "{{index}}={{result}}" }
# Output: 0=Hello Ann | 1=Hello Ben
Inline Pipeline Effects
Attach lightweight side effects after any stage without a full directive:
| log "message" # stderr
| show "message" # stdout + document
| output @var to "file" # reuse /output routing
| append "file.jsonl" # append stage output
Example append usage:
/var @runs = ["alpha", "beta", "gamma"]
/var @_ = for @name in @runs =>
`processed @name` | append "runs.log"
/show <runs.log>
You can pass an explicit source to append when you need different content:
/var @_ = "summary" | append @runs to "runs.jsonl"
When-Expressions in for RHS
Use a when [...] expression as the right-hand side in collection form. Combine with none => skip to filter non-matches:
/var @xs = [1, null, 2, null, 3]
/var @filtered = for @x in @xs => when [
@x != null => @x
none => skip
]
/show @filtered # => ["1","2","3"]
Template Loops (backticks and ::)
Write inline /for loops inside templates for simple rendering tasks. The loop header and /end must start at line begins inside the template.
Backticks:
/var @tpl = `
/for @v in ["x","y"]
- @v
/end
`
/show @tpl
Double-colon:
/var @items = ["A","B"]
/var @msg = ::
/for @x in @items
- @x
/end
::
/show @msg
Notes:
- Loops are supported in backticks and
::…::templates. :::…:::and[[…]]templates do not support loops.
Pipelines
Basic Pipelines
Chain operations with |:
/var @data = run {echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}'} | @json
/show @data.users[0].name
Output:
Alice
Pipeline Context
Access pipeline context with @ctx and pipeline history with @p (alias for @pipeline):
/exe @validator(input) = when first [
@input.valid => @input.value
@ctx.try < 3 => retry "validation failed"
none => "fallback value"
]
/var @result = "invalid" | @validator
/show @result
Context object (@ctx) contains:
try- current attempt number in the context of the active retrytries- array containing history of attemptsstage- current pipeline stageinput- current stage input (use@p[0]to read the original pipeline input)hint- the most recent hint passed viaretry "..."(string or object)lastOutput- output from the previous stage (if any)isPipeline- true when executing inside a pipeline stage
Pipeline array (@p) contains:
@p[0]- original/base input to the pipeline@p[1]…@p[n]- outputs of completed visible stages@p[-1]- previous stage output;@p[-2]two stages back, etc.@p.retries.all- all attempt outputs from all retry contexts (for best-of-N patterns)- Pipeline stage outputs are
StructuredValuewrappers with.text(string view) and.data(structured payload) properties. Templates and display automatically use.text; use.datawhen you need structured information.
Gotchas:
@ctx.tryand@ctx.triesare local to the active retry context. Stages that are not the requester or the retried stage will seetry: 1andtries: [].@ctx.inputis the current stage input, not the original. Use@p[0]for the original pipeline input.- A synthetic internal stage may be created for retryable sources; stage numbers and
@pindices shown above hide this internal stage.
Retry with Hints
Use retry with hints to guide subsequent attempts:
/exe @source() = when first [
@ctx.try == 1 => "draft"
* => "final"
]
/exe @validator() = when first [
@ctx.input == "draft" => retry "missing title"
* => `Used hint: @ctx.hint`
]
/var @result = @source() | @validator
/show @result
Output:
Used hint: missing title
Parallel Pipelines
Run multiple transforms concurrently within a single pipeline stage using ||.
/exe @left(input) = `L:@input`
/exe @right(input) = `R:@input`
/exe @combine(input) = js {
// Parallel stage returns a JSON array string
const [l, r] = JSON.parse(input);
return `${l} | ${r}`;
}
/var @out = "seed" with { pipeline: [ @left || @right, @combine ] }
/show @out
Pipelines can also start with a leading || to run parallel stages immediately:
/exe @fetchA() = "A"
/exe @fetchB() = "B"
/exe @fetchC() = "C"
>> Leading || runs all three in parallel
/var @results = || @fetchA() || @fetchB() || @fetchC()
/show @results
>> Works in /run directive too
/run || @fetchA() || @fetchB() || @fetchC()
>> Control concurrency with (cap, delay) syntax
/var @limited = || @fetchA() || @fetchB() || @fetchC() (2, 100ms)
Output:
["A","B","C"]
["A","B","C"]
["A","B","C"]
The leading || syntax is equivalent to the longhand form:
>> These produce identical results:
/var @shorthand = || @a() || @b() | @combine
/var @longhand = "" with { pipeline: [[@a, @b], @combine] }
Notes:
- Results preserve order of commands in the group.
- The next stage receives a JSON array string (parse it or accept as text).
- Concurrency is capped by
MLLD_PARALLEL_LIMIT(default4). - Leading
||syntax avoids ambiguity with boolean OR expressions. - Use
(n, wait)after the pipeline to override concurrency cap and add pacing between starts. - Returning
retryinside a parallel group is not supported; do validation after the group and request a retry of the previous (non‑parallel) stage if needed. - Inline effects attached to grouped commands run after each command completes.
Complex Retry Patterns
Multi-stage pipelines with retry and fallback:
/exe @randomQuality(input) = js {
const values = [0.3, 0.7, 0.95, 0.2, 0.85];
return values[ctx.try - 1] || 0.1;
}
/exe @validateQuality(score) = when first [
@score > 0.9 => `excellent: @score`
@score > 0.8 => `good: @score`
@ctx.try < 5 => retry
none => `failed: best was @score`
]
/var @result = @randomQuality | @validateQuality
/show @result
Error Handling
mlld has no early exit (return/exit). Model different outcomes with /when and state:
/var @validation = @validate(@input)
/when [
@validation.valid => show "Processing successful"
!@validation.valid => show `Error: @validation.message`
]
Use flags to coordinate flow:
/var @canDeploy = @testsPass && @isApproved
/when [
@canDeploy => run {npm run deploy}
!@canDeploy => show "Deployment blocked - check tests and approval"
]
Common Patterns
Guarded Execution
/var @result = @data | @validate | @process
/when [
@result.success => output @result.data to "output.json"
!@result.success => show `Processing failed: @result.error`
]
Conditional Actions
/exe @isProduction() = sh {test "$NODE_ENV" = "production" && echo "true"}
/when first [
@isProduction() && @testsPass => run {npm run deploy:prod}
@testsPass => run {npm run deploy:staging}
* => show "Cannot deploy: tests failing"
]
Collection Processing
/var @files = ["config.json", "data.json", "users.json"]
/exe @processFile(file) = when first [
@file.endsWith(".json") => `Processed: @file`
* => `Skipped: @file`
]
/var @results = foreach @processFile(@files)
/for @result in @results => show @result