output writes to files and stdout. file/files project one or many files. log writes to stderr. append adds to existing files. stream enables real-time token-by-token output from LLM calls.
Output Directive
output @content to "out.txt"
output @data to "config.json"
output @message to stdout
output @error to stderr
Objects and arrays are automatically serialized to JSON when writing to .json files. For other file types, the value is converted to a string.
See Also
- Append Directive - Append structured or text output incrementally.
- Log Directive - Emit output to stderr.
- Pipelines Basics - Attach
outputas a pipeline effect stage.
Log Directive
var @debugMode = true
log @message >> same as output @message to stderr
log `Processing: @item`
>> In action contexts
for @item in @items => log @item
when @debugMode => log "Debug info"
See Also
- Output Directive - File/stdout/stderr output patterns and formatting.
- Pipelines Basics - Use
| logas a pipeline effect stage.
Append Directive
append @record to "events.jsonl" >> JSON object per line
append "raw line" to "events.log"
>> In pipelines
@data | append "audit.jsonl"
>> In loops
for @name in @runs => append @name to "pipeline.log"
.jsonl enforces JSON serialization. Other extensions write text. .json blocked.
Streaming
>> In definition (recommended)
exe @llm(prompt) = stream cmd {
claude -p "@prompt" --output-format stream-json --verbose --include-partial-messages
} with { streamFormat: "claude-code" }
show @llm("Explain TCP/IP") >> streams tokens as they arrive
>> At invocation
exe @raw(prompt) = cmd {claude -p "@prompt" --output-format stream-json --verbose --include-partial-messages}
run stream @raw("Hello") with { streamFormat: "claude-code" }
Definition-level stream and streamFormat are inherited by both run @exe() and show @exe().
import { @claude } from @mlld/claude
show @claude("Explain TCP/IP", { model: "haiku", stream: true })
Format Adapters
NDJSON streams require a format adapter to parse and display incrementally.
>> Built-in adapter (string shorthand)
run stream @exe("prompt") with { streamFormat: "claude-code" }
>> Installable adapter
import { @claudeAgentSdkAdapter } from @mlld/stream-claude-agent-sdk
run stream @exe("prompt") with { streamFormat: @claudeAgentSdkAdapter }
| Adapter | Aliases | Parses |
|---|---|---|
ndjson |
— | Generic NDJSON (default) |
claude-code |
claude-agent-sdk |
Claude CLI stream-json events |
Without an adapter, raw NDJSON passes through unformatted.
Shell Streaming
Non-NDJSON commands stream raw stdout:
exe @build() = stream sh {npm run build}
show @build() >> live build output
Parallel
stream @a() || stream @b() >> concurrent, buffered per-task
Suppress
mlld script.mld --no-stream
MLLD_NO_STREAM=true mlld script.mld
Debug
mlld script.mld --show-json >> mirror NDJSON to stderr
mlld script.mld --append-json out.jsonl >> save events to file
Limitations
- After-guards conflict with streaming (output must be complete for validation)
- Pipeline stages buffer between stages; streaming is within each stage
File Directive
file "task.md" = @task
file "notes/today.md" = `## Notes\n@summary`
file writes exactly one file. Paths must be relative (no leading /) and cannot use ...
Inside a box block — writes to the box's workspace VFS:
box [
file "task.md" = @task
file "src/main.js" = @source
run cmd { cat task.md }
]
Files written this way are scoped to the box. ShellSession commands can read them. No resolver is created.
Outside a box block — writes to the real filesystem, relative to project root.
Immutability: A path can only be written once per scope. Writing the same path again throws an error. Commands inside box blocks (via ShellSession) can modify files — the one-shot rule applies only to file directive writes.
Files Directive
files "src/" = [
{ "index.js": @source, desc: "Entry point" },
{ "config.json": @config }
]
Each entry is an object with one file key mapped to content, and optional desc for metadata.
Named resolver — workspace creation:
files <@workspace/> = [{ "task.md": @task }]
files <@workspace/src/> = [{ "main.js": @code }]
var @content = <@workspace/task.md>
show @content
The <@name/> syntax creates a VFS-backed resolver on first use. Subsequent files <@name/subdir> writes extend the same VFS. Read files back with <@name/path>.
Descriptions:
The desc field populates workspace.descriptions — a map of path to description. Agents can inspect file purposes without reading content:
files <@ws/> = [
{ "task.md": @task, desc: "Current task specification" },
{ "context.md": @ctx, desc: "Background context" }
]
show @ws
Inside a box block — writes to the active workspace VFS without creating a resolver:
box [
files "src/" = [
{ "index.js": @source },
{ "util.js": @utils }
]
run cmd { ls src/ }
]
Git hydration:
files <@workspace/> = git "https://github.com/user/repo" branch:"main"
files <@workspace/docs/> = git "https://github.com/user/repo" path:"docs/" depth:1
Clones text files into VFS. Binary files and symlinks are skipped. Options: auth: (keychain ref), branch: (branch/tag/commit), path: (subdirectory), depth: (default 1). Git-sourced files carry src:git taint.
Workspace inspection:
files <@ws/> = [{ "file.md": "draft" }]
show @ws.mx.edits
show <@ws/file.md>.mx.diff
.mx.edits returns the change list. <@ws/path>.mx.diff returns a unified diff string.