pre-release

⚠️ mlld is pre-release

Join the discord if you have questions. Report bugs on GitHub.

mlld Modules

tldr

Modules let you package and reuse mlld code. Create modules with /export, import with /import, and publish to the registry with mlld publish. Modules capture their environment so executables work anywhere.

Creating Modules

Basic Module Structure

---
name: greetings
author: alice
version: 1.0.0
about: Simple greeting utilities
needs: []
license: CC0
---

/exe @sayHello(name) = `Hello, @name!`
/exe @sayGoodbye(name) = `Goodbye, @name!`

/export { @sayHello, @sayGoodbye }

Save as greetings.mld and it's ready to import.

Export Control

/export declares what others can import:

/exe @publicHelper(x) = `Value: @x`
/exe @_internalHelper(x) = run {echo "@x" | tr a-z A-Z}

/export { @publicHelper }

Only @publicHelper is accessible to importers. Without /export, all variables are exported (legacy behavior).

Module Dependencies

Declare what your module needs:

---
needs: [js, sh]
needs-js:
  packages: [lodash]
needs-sh:
  commands: [git, curl]
---

Auto-detect dependencies:

mlld add-needs greetings.mld

Importing Modules

Registry Modules

/import { @helper } from @alice/utils
/show @helper("data")

Install first:

mlld install @alice/utils

Local Files

/import { @config } from <./config.mld>
/show @config.apiKey

URL Imports

/import cached(1h) <https://example.com/utils.mld> as @remote
/show @remote.version

Import Types

Control when and how imports resolve:

# Registry module (offline after install)
/import module { @api } from @company/tools

# Embedded at parse time
/import static <./prompts/system.md> as @systemPrompt

# Always fetch fresh
/import live <https://api.status.io> as @status

# Cached with TTL
/import cached(30m) <https://feed.xml> as @feed

# Local development (llm/modules/)
/import local { @helper } from @alice/dev-module

Import Patterns

Selected imports:

/import { @helper, @validator } from @alice/utils

Namespace imports:

/import @alice/utils as @utils
/show @utils.helper("data")

Avoid collisions:

/import { @helper } from @alice/utils
/import @bob/tools as @bob  # Use namespace for second import
/show @helper("x")
/show @bob.helper("y")

Module Environment

Modules capture their defining environment. Executables reference their original variables:

# In utils.mld
/var @prefix = "Result: "
/exe @format(x) = `@prefix @x`
/export { @format }

When imported:

/import { @format } from <./utils.mld>
/show @format("success")  # → "Result: success"

@prefix resolves from the module, not your script.

Development Workflow

Local Development

  1. Create module in llm/modules/:
mkdir -p llm/modules/@alice
# Create llm/modules/@alice/my-tool.mld
  1. Use local import:
/import local { @tool } from @alice/my-tool
  1. Test and iterate without registry publishing

Quick Start

Generate module template:

mlld init-module @alice/my-tool

This creates a complete module structure with frontmatter and examples.

Testing Modules

Create test scripts in your project:

# test-module.mld
/import { @helper } from <./my-module.mld>
/var @result = @helper("test")
/when @result == "expected" => show "✓ Pass"
/when @result != "expected" => show "✗ Fail"

Run tests:

mlld test-module.mld

Configuration

Project Config (mlld-config.json)

{
  "dependencies": {
    "@alice/utils": "1.0.0",
    "@company/auth": "latest"
  },
  "dev": {
    "localModulesPath": "llm/modules",
    "enabled": true
  }
}

Create config:

mlld setup

Lock File (mlld-lock.json)

Auto-generated when you install modules. Ensures reproducible imports:

{
  "lockfileVersion": 1,
  "modules": {
    "@alice/utils": {
      "version": "1.0.0",
      "resolved": "abc123def456...",
      "source": "@alice/utils",
      "sourceUrl": "https://registry.mlld.org/modules/@alice/utils/1.0.0",
      "integrity": "sha256:abc123...",
      "fetchedAt": "2024-01-15T10:00:00Z",
      "registryVersion": "1.0.0"
    }
  }
}

Never edit manually. Use mlld install, mlld update, or mlld outdated.

Commands

Install Dependencies

mlld install                    # Install from mlld-config.json
mlld install @alice/utils       # Install specific module
mlld install @alice/utils@1.2.0 # Install specific version

Update Modules

mlld update                     # Update all modules
mlld update @alice/utils        # Update specific module
mlld outdated                   # Check for newer versions

Publishing

mlld publish my-module.mld      # Publish to registry
mlld publish --pr               # Force PR workflow

See registry.md for publishing details.

Version Resolution

Use semantic versioning:

/import { @helper } from @alice/utils          # latest
/import { @helper } from @alice/utils@1.0.0    # exact
/import { @helper } from @alice/utils@^1.0.0   # compatible
/import { @helper } from @alice/utils@beta     # tag

Lock file pins exact versions for reproducibility.

Best Practices

Use explicit exports:

/export { @publicAPI, @helper }

Prefix internals (convention):

/exe @_internal(name) = "opaque"  # Not in export list

Document exports:

# Public API for data transformation
/exe @transform(data) = `Processed: @data`
/export { @transform }

Test before publishing:

mlld my-module.mld  # Run as script first

Use local imports for development:

/import local { @tool } from @me/dev-module

Common Patterns

Config Module

---
name: config
about: App configuration
---

/var @apiUrl = "https://api.example.com"
/var @timeout = 30000
/export { @apiUrl, @timeout }

Utility Collection

---
name: text-utils
about: String manipulation utilities
---

/exe @uppercase(text) = run {echo "@text" | tr a-z A-Z}
/exe @trim(text) = js { return @text.trim() }
/export { @uppercase, @trim }

Template Library

---
name: prompts
about: Reusable prompts
---

/exe @systemPrompt(role) = `You are a @role assistant.`
/exe @userPrompt(task) = `Please help me with: @task`
/export { @systemPrompt, @userPrompt }

Dynamic Modules (SDK)

Runtime module injection without filesystem I/O. Enables multi-tenant applications to inject per-user/project context from database.

String Modules

Inject mlld source as strings:

const output = await processMlld(template, {
  dynamicModules: {
    '@user/context': `/export { @userId, @userName }\n/var @userId = "123"\n/var @userName = "Ada"`
  }
});

Object Modules

Inject structured data directly (recommended):

const output = await processMlld(template, {
  dynamicModules: {
    '@state': {
      count: 0,
      messages: ['Hello', 'World'],
      preferences: { theme: 'dark' }
    },
    '@payload': {
      text: userInput,
      userId: session.userId
    }
  }
});

Access in your script:

/var @count = @state.count + 1
/var @theme = @state.preferences.theme
/var @input = @payload.text

Security

Dynamic modules are automatically labeled src:dynamic for guard enforcement:

/guard before secret = when [
  @input.ctx.taint.includes('src:dynamic') =>
    deny "Cannot use dynamic data as secrets"
  * => allow
]

Priority

Dynamic modules override filesystem and registry modules with the same path (highest priority).

Use cases: Multi-tenant SaaS, per-request context injection, testing with mock data.

Not for CLI: CLI users should use filesystem modules. Dynamic modules are SDK-only.

Troubleshooting

Import not found:

mlld install @alice/utils  # Install first

Version mismatch:

mlld update @alice/utils   # Update to latest
# Or edit mlld-config.json and run mlld install

Export not found:
Check /export directive includes the variable name.

Collision error:
Use namespace imports:

/import @alice/utils as @alice
/import @bob/utils as @bob

Module not updating:
Local imports read from disk every time. Registry modules are cached - use mlld update to refresh.