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

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

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.