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. Language SDKs wrap the core for Go, Python, Rust, Ruby, and Elixir.
SDK
Four modes for SDK consumers:
document (default): Returns string
const output = await processMlld(script);
structured: Returns full result object
const result = await interpret(script, { mode: 'structured' });
console.log(result.effects);
console.log(result.stateWrites);
stream: Real-time events
const handle = interpret(script, { mode: 'stream' });
handle.on('stream:chunk', e => process.stdout.write(e.text));
await handle.done();
debug: Full trace
const result = await interpret(script, { mode: 'debug' });
console.log(result.trace);
Execute Function
File-based execution with state management.
const result = await execute('./agent.mld', payload, {
state: { conversationId: '123', messages: [...] },
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 - State writes via
state://protocol
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:
output { count: 5 } 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.
Dynamic Module Injection
Inject runtime context without filesystem I/O.
execute('./script.mld', { text: 'user input', userId: '123' });
>> Destructuring import (fields must exist)
import { text, userId } from @payload
show @text
For optional fields, use namespace import with ternary:
>> Namespace import for optional field access
import "@payload" as @payload
var @text = @payload.text ? @payload.text : "default"
CLI usage with mlld run:
mlld run myscript --topic foo --count 5
>> In myscript.mld - required fields
import { topic, count } from @payload
show `Topic: @topic, Count: @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.
Destructuring import (required fields - fails if missing):
import { topic, count } from @payload
show `Topic: @topic, Count: @count`
Namespace import (optional fields with defaults):
import "@payload" as @payload
var @topic = @payload.topic ? @payload.topic : "default"
var @count = @payload.count ? @payload.count : 0
SDK usage:
execute('./script.mld', { topic: 'foo', count: 5 });
CLI usage — both mlld run and direct invocation support payload:
mlld run myscript --topic foo --count 5
mlld script.mld --topic foo --count 5
Unknown flags become @payload fields automatically. Kebab-case flags are converted to camelCase (e.g., --dry-run becomes @dryRun).
@payload is always available as {} even when no flags are passed — scripts can safely reference @payload fields without checking whether payload was injected.
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.
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