The mlld SDK embeds the interpreter in Node.js applications. Four execution modes cover file-based runs, string evaluation, dynamic module injection, and static analysis. State management enables multi-turn workflows. VirtualFS adds copy-on-write file overlays for sandboxed and review-first workflows. Language SDKs wrap the core for Go, Python, Rust, Ruby, and Elixir.
SDK
Two public functions cover all SDK use cases:
processMlld — returns output as a string (simplest API)
const output = await processMlld(script);
execute — returns a structured result object
const result = await execute(filePath, payload);
console.log(result.output);
console.log(result.effects);
console.log(result.stateWrites);
Streaming — pass stream: true to get real-time events
const stream = await execute(filePath, payload, { stream: true });
stream.on('effect', e => console.log(e));
stream.on('stream:chunk', e => process.stdout.write(e.text));
await stream.done();
The StreamExecution object supports on/off/once event subscriptions and is also an AsyncIterable:
for await (const event of stream) {
console.log(event.type, event);
}
Execute Function
File-based execution with state management.
const result = await execute('./agent.mld', payload, {
state: { conversationId: '123', messages: [...] },
payloadLabels: { history: ['untrusted'] },
timeout: 30000
});
for (const write of result.stateWrites) {
await updateState(write.path, write.value);
}
Features:
- In-memory AST caching (mtime-based invalidation)
- State hydration via
@statemodule - Payload injection via
@payload, including per-field labels viapayloadLabels - State writes via
state://protocol - Stream handles can apply labeled in-flight state updates with
updateState(path, value, labels?) mcpServersmaps logical names to MCP server commands per-execution
MCP server injection lets parallel execute() calls each get independent MCP server instances:
const result = await execute('./agent.mld', payload, {
mcpServers: { tools: 'uv run python3 server.py --config abc' }
});
The script uses import tools from mcp "tools" as @t — "tools" resolves to the SDK-provided command instead of being treated as a shell command.
State Management
@state Module
Hydrate mutable state from the SDK:
const result = await execute('./agent.mld', payload, {
state: { conversationId: '123', count: 0 }
});
Access in mlld:
import { @conversationId, @count } from @state
show `Conversation @conversationId, count @count`
@state is a reserved variable — it's always available when state is provided via SDK or CLI.
state:// Protocol
Write state back from mlld using the state:// protocol:
var @countUpdate = { count: 5 }
output @countUpdate to "state://count"
output @result to "state://lastResult"
State writes are collected in the execution result:
for (const write of result.stateWrites) {
await updateState(write.path, write.value);
}
stateWrites merges final-result writes and streamed state:write events emitted during execution.
In-Flight State Updates
SDK clients can mutate @state during execution via update_state. This enables external control of running scripts:
### Python
handle = client.process_async(
'loop(99999, 50ms) until @state.exit [\n continue\n]\nshow "done"',
state={'exit': False},
timeout=10,
)
time.sleep(0.12)
handle.update_state('exit', True)
print(handle.result())
// Go
handle, _ := client.ProcessAsync(script, &mlld.ProcessOptions{
State: map[string]any{"exit": false},
Timeout: 10 * time.Second,
})
time.Sleep(120 * time.Millisecond)
handle.UpdateState("exit", true)
output, _ := handle.Result()
All language SDKs support update_state with retry semantics on REQUEST_NOT_FOUND.
Labeled State Updates
Boundary values injected through update_state can carry security labels, just like labeled values declared in mlld source.
handle.update_state("tool_result", result_text, labels=["untrusted"])
handle.UpdateState("tool_result", resultText, "untrusted")
handle.update_state_with_labels("tool_result", result_text, ["untrusted"])?;
Labels applied through SDK state updates:
- are visible on
@state.some_path.mx.labels - propagate through normal mlld label flow
- are optional; omitting labels preserves unlabeled behavior for that updated path
Dynamic Module Injection
Inject runtime context without filesystem I/O.
execute('./script.mld', { text: 'user input', userId: '123' });
>> @payload is available directly — no import required
show @payload.text
var @name = @payload.userId ? @payload.userId : "anonymous"
Destructuring import also works for required fields:
import { text, userId } from @payload
show @text
CLI usage with mlld run:
mlld run myscript --topic foo --count 5
>> In myscript.mld
show `Topic: @payload.topic, Count: @payload.count`
Dynamic imports are labeled src:dynamic and marked untrusted.
Analyze Module
Static analysis without execution.
const analysis = await analyzeModule('./tools.mld');
if (!analysis.valid) {
console.error('Errors:', analysis.errors);
}
const tools = analysis.executables
.filter(e => analysis.exports.includes(e.name));
Use cases: MCP proxy, module validation, IDE/LSP, security auditing.
Payload Access
@payload contains data passed to a script at invocation time. It's available as a direct variable — no import required.
show `Topic: @payload.topic, Count: @payload.count`
var @env = @payload.env ? @payload.env : "dev"
Destructuring import (required fields - fails if missing):
import { topic, count } from @payload
show `Topic: @topic, Count: @count`
SDK usage:
execute('./script.mld', { topic: 'foo', count: 5 });
Per-Field Payload Labels
SDK payload fields can carry security labels at the boundary.
await execute('./agent.mld', {
history: 'external tool output',
query: 'user request'
}, {
payloadLabels: {
history: ['untrusted']
}
});
result = client.execute(
"./agent.mld",
{"history": "external tool output"},
payload_labels={"history": ["untrusted"]},
)
Python also supports inline helpers:
from mlld import trusted, untrusted
result = client.execute(
"./agent.mld",
{
"query": trusted("approved request"),
"history": untrusted("external tool output"),
},
)
Per-field payload labels work for both direct @payload.field access and imports from @payload.
CLI usage — mlld run, direct invocation, and -e all support payload:
mlld run myscript --topic foo --count 5
mlld script.mld --topic foo --count 5
mlld -e 'show @payload.topic' --topic foo
Unknown flags become @payload fields automatically. Kebab-case flags are converted to camelCase (e.g., --dry-run becomes @dryRun).
@payload is always {} when no flags are passed — safe to reference fields without checking.
Language SDKs
Thin wrappers around the mlld CLI for Go, Python, Rust, Ruby, and Elixir. Each keeps a persistent mlld live --stdio subprocess for repeated calls via NDJSON RPC.
Tradeoff: Feature parity with CLI semantics and low maintenance, but requires Node.js at runtime.
Core API (all languages)
All SDKs provide:
process(script, options)— execute inline mlldexecute(filepath, payload, options)— file-based execution with stateanalyze(filepath)— static analysis without executionprocess_async/execute_async— async with handle for in-flight control- Handle:
wait,result,cancel,update_state(path, value)
ExecuteResult.state_writes merges final-result writes and streamed state:write events in all languages.
Boundary Labels
All SDKs now support labels on values crossing into the runtime:
- Payload labels:
TypeScript/live transport usepayloadLabels
Python, Ruby, and Elixir usepayload_labels
Go usesPayloadLabels
Rust usespayload_labelsonProcessOptions/ExecuteOptions - State update labels:
Python, Ruby, and Elixir useupdate_state(..., labels=[...])
Go uses variadic labels:UpdateState(path, value, "untrusted")
Rust exposesupdate_state_with_labels(...)
Those labels surface on @payload.*.mx.labels and @state.*.mx.labels, then propagate through normal mlld policy/guard flow.
Installation
### Go
go get github.com/mlld-lang/mlld/sdk/go
### Python
pip install mlld-sdk
### Rust
### Add to Cargo.toml: mlld = "0.1"
### Ruby
cd sdk/ruby && gem build mlld.gemspec && gem install ./mlld-*.gem
### Elixir
cd sdk/elixir && mix deps.get
Quick Start Examples
Python:
from mlld import Client
client = Client()
output = client.process('show "Hello World"')
result = client.execute('./agent.mld', {'text': 'hello'},
state={'count': 0},
dynamic_modules={'@config': {'mode': 'demo'}},
timeout=10)
print(result.output)
client.close()
Go:
client := mlld.New()
output, _ := client.Process(`show "Hello World"`, nil)
result, _ := client.Execute("./agent.mld",
map[string]any{"text": "hello"},
&mlld.ExecuteOptions{
State: map[string]any{"count": 0},
Timeout: 10 * time.Second,
})
fmt.Println(result.Output)
client.Close()
Rust:
let client = Client::new();
let output = client.process(r#"show "Hello World""#, None)?;
let result = client.execute("./agent.mld",
Some(json!({"text": "hello"})),
Some(ExecuteOptions {
state: Some(json!({"count": 0})),
timeout: Some(Duration::from_secs(10)),
..Default::default()
}))?;
println!("{}", result.output);
Elixir:
{:ok, client} = Mlld.Client.start_link(command: "mlld", timeout: 30_000)
{:ok, result} = Mlld.Client.execute(client, "./agent.mld", %{"text" => "hello"},
state: %{"count" => 0},
dynamic_modules: %{"@config" => %{"mode" => "demo"}},
timeout: 10_000)
IO.puts(result.output)
Elixir-Specific Features
The Elixir SDK adds BEAM-native features:
- Supervision —
Mlld.Clientis a GenServer with child spec support - Connection pool —
Mlld.Poolwith checkout/checkin and overflow - Telemetry —
:telemetryevents with[:mlld, ...]prefix - Phoenix bridge —
Mlld.Phoenix.stream_executefor channel integration
Requirements
All SDKs require:
mlldCLI on PATH (or command override)- Node.js runtime
VirtualFS Overlay
VirtualFS provides a copy-on-write filesystem overlay for SDK runs.
import { VirtualFS, NodeFileSystem } from 'mlld';
const vfs = VirtualFS.over(new NodeFileSystem());
Inspection and lifecycle APIs:
const changes = await vfs.changes(); // canonical
const alias = await vfs.diff(); // compatibility alias
const unified = await vfs.fileDiff('/path/file.ts');
Selective apply/discard:
for (const change of await vfs.changes()) {
if (await approve(change)) {
await vfs.flush(change.path);
} else {
vfs.discard(change.path);
}
}
Patch export/replay:
const patch = vfs.export();
const replay = VirtualFS.over(new NodeFileSystem());
replay.apply(patch);
await replay.flush();