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 @state module
  • Payload injection via @payload, including per-field labels via payloadLabels
  • State writes via state:// protocol
  • Stream handles can apply labeled in-flight state updates with updateState(path, value, labels?)
  • mcpServers maps 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 usagemlld 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 mlld
  • execute(filepath, payload, options) — file-based execution with state
  • analyze(filepath) — static analysis without execution
  • process_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 use payloadLabels
    Python, Ruby, and Elixir use payload_labels
    Go uses PayloadLabels
    Rust uses payload_labels on ProcessOptions / ExecuteOptions
  • State update labels:
    Python, Ruby, and Elixir use update_state(..., labels=[...])
    Go uses variadic labels: UpdateState(path, value, "untrusted")
    Rust exposes update_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:

  • SupervisionMlld.Client is a GenServer with child spec support
  • Connection poolMlld.Pool with checkout/checkin and overflow
  • Telemetry:telemetry events with [:mlld, ...] prefix
  • Phoenix bridgeMlld.Phoenix.stream_execute for channel integration

Requirements

All SDKs require:

  • mlld CLI 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();