Modules enable code reuse through imports and exports. Registry provides public sharing.

Importing

Registry modules:

import { @sortBy, @unique } from @mlld/array
import @corp/utils as @corp

>> With version
import { @helper } from @alice/utils@1.0.0
import { @helper } from @alice/utils@^1.0.0   >> semver range

Importing Local Files

Local files (selected exports):

import { @helper } from "./utils.mld"
import { @config } from <@root/config.mld>
import { @prompt } from "../prompts/main.mld"

import {
  @renderHeader,
  @renderBody
} from "./templates.mld"

Local files (namespace import):

import "./utils.mld" as @utils
show @utils.helper("report")

Path resolution:

  • ./ and ../ paths resolve from the importing file's directory, not the shell cwd.
  • <@root/...> resolves from the project root.

Namespace Imports

Namespace imports:

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

show @alice.format(@data)
show @bob.format(@data)   >> no collision

Directory Imports

Directory imports:

import "@agents" as @agentRegistry
show @agentRegistry.alice.tldr
show @agentRegistry.support.helper.name

>> With options
import "./agents" as @agents with { skipDirs: [] }

Directories auto-load */index.mld. Default skipDirs: ["_*", ".*"].

Use directory imports when you want a namespace object.
If you want selected exports, import the entry file directly:

import { @helper } from "./agents/index.mld"

Relative directory paths resolve from the importing file's directory, not the shell cwd.

Template Collections

Template collections import an entire directory of .att files as a callable namespace:

import templates from "@root/agents" as @agents(message, context)

>> Access templates by name (bracket or dot notation)
show @agents["alice"](@msg, @ctx)           >> agents/alice.att
show @agents.support["helper"](@msg, @ctx)  >> agents/support/helper.att

Directory structure example:

agents/
  alice.att          >> @agents["alice"](msg, ctx)
  bob.att            >> @agents["bob"](msg, ctx)
  support/
    helper.att       >> @agents.support["helper"](msg, ctx)
    escalate.att     >> @agents.support["escalate"](msg, ctx)

Key rules:

  • All templates in a collection share the same parameter signature
  • Filenames with hyphens become underscores: json-pretty.att@tpl["json_pretty"]
  • Use dot notation for directories, brackets for template names
  • Collections require parameters in the as @name(params) clause

When to use collections:

  • Multiple templates with the same interface (agent prompts, formatters)
  • Dynamic template selection based on runtime values
  • Organizing related templates by category

For single templates, use exe @func(params) = template "file.att" instead.

Importing Node Modules

Basic node imports:

import { basename } from node @path
var @name = @basename("/tmp/file.txt")
show @name

Namespace imports:

import { posix } from node @path
var @dir = @posix.dirname("/tmp/file.txt")
show @dir

Constructor expressions:

import { URL } from node @url
exe @site = new @URL("https://example.com/path?x=1")
show @site.hostname
show @site.pathname

Automatic wrapping:

  • Functions are wrapped to accept and return mlld values
  • Method binding is preserved (correct this context)
  • Classes support new @Constructor() syntax
  • Async iterables are wrapped as streams
  • Promises work transparently
  • Callback-style functions trigger warnings

Module resolution:

  • Uses Node.js module resolution from the importing file's directory
  • Supports both CommonJS and ES modules
  • Built-in modules: node @path, node @fs, node @url, etc.
  • NPM packages: Use package name without leading @ (e.g., node @chalk)

Import Types

Type Behavior Use Case
module Content-addressed cache Registry modules (default)
static Embedded at parse time Prompts, templates
live Always fresh Status APIs
cached(TTL) Time-based cache Feeds, configs
local Dev modules (llm/modules/) Development
templates Directory of .att files Template collections
import module { @api } from @corp/tools
import static { @prompt } from "./prompt.md"
import live <https://status.io> as @status
import cached(1h) <https://feed.xml> as @feed
import local { @dev } from @alice/experimental

Custom Resolvers

Built-in:

  • @author/module → Registry
  • @root/file → Project root (preferred)
  • @base/file → Project root (alias for @root)
  • ./file.mld → Local (with fuzzy extension matching)

Custom prefixes (mlld-config.json):

{
  "resolvers": {
    "prefixes": [
      {
        "prefix": "@lib/",
        "resolver": "LOCAL",
        "config": { "basePath": "./src/lib" }
      },
      {
        "prefix": "@company/",
        "resolver": "GITHUB",
        "config": {
          "repository": "company/private-modules",
          "branch": "main"
        }
      }
    ]
  }
}

mlld reads resolvers.prefixes for resolver mappings. It also reads top-level resolverPrefixes when present, and CLI writers persist the nested resolvers.prefixes shape.

Quick setup:

mlld alias --name notes --path ~/notes
mlld alias --name shared --path ../shared --global
mlld setup --github   # private repo wizard

Lock File

When you install modules, mlld creates mlld-lock.json to ensure reproducible imports. This file tracks exact versions and content hashes.

{
  "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"
    }
  }
}

Lock entry fields:

  • version - The exact version installed (from registry version.json)
  • resolved - Content hash used for cache lookup (SHA256)
  • source - Original module specifier from your imports
  • sourceUrl - URL where the module was fetched from
  • integrity - Content hash for verification (sha256:...)
  • fetchedAt - Timestamp when module was installed
  • registryVersion - Version from registry metadata (only for registry modules)

Behavior:

  • Auto-generated - Created/updated by mlld install
  • Version control - Commit to git for reproducible builds
  • Never edit manually - Use CLI commands to update
  • Registry-only validation - Lock file only enforces version matches for registry modules
  • Version pinning - version: "latest" updates to newest on mlld update; exact version (e.g. "1.2.0") stays pinned until manually changed

Module Philosophy

Module-first design: keep .mld files readable; move complexity into focused modules. Avoid "kitchen sink" modules or side effects on import.

Creating Modules

---
name: text-utils
author: alice
version: 1.0.0
about: String helpers
license: CC0
---

needs {
  js: []
}

exe @upper(s) = js { return s.toUpperCase() }
exe @trim(s) = js { return s.trim() }

export { @upper, @trim }

Frontmatter fields:

  • name - Module name (required for registry)
  • author - Your username (required for registry)
  • version - Semver version
  • about - Brief description
  • license - License (CC0 recommended)

Exporting from Modules

Modules only expose items listed in export { }. Unexported items stay private.

exe @greet(name) = `Hello, @name!`
exe @farewell(name) = `Goodbye, @name!`
var @_helper = "internal"

>> Only @greet and @farewell are visible to importers
export { @greet, @farewell }

Why explicit exports: Encapsulation. Importers see a clean API surface. Internal helpers, intermediate variables, and implementation details stay hidden. Rename or remove internals without breaking callers.

Wildcard export: Export everything (same as no export directive):

export { * }

Environment module pattern: Modules that wrap credentials export executables and let callers import the policy separately for credential configuration:

policy @p = {
  auth: {
    claude: { from: "keychain:mlld-env-{projectname}/claude-dev", as: "ANTHROPIC_API_KEY" }
  }
}

exe @spawn(prompt) = run cmd { claude -p "@prompt" } using auth:claude

export { @spawn }

Notes:

  • Accessing unexported items via namespace import raises a runtime error
  • Exported executables do not expose captured module internals through field access
  • Guards can be exported alongside variables
  • Without an export directive, all module-level items are auto-exported

Module Structure

Modules are directories with an entry point, manifest, and optional supporting files.

``` mymodule/ ├── index.mld # Entry point ├── module.yml # Manifest (or .yaml, .json) ├── README.md # Documentation └── lib/ # Optional supporting files ``` module.yml format: ```yaml name: myapp author: alice type: app # library | app | command | skill about: "Description" version: 1.0.0 license: CC0 ```

<metadata_sources>
Directory modules use two metadata sources:

Source Lives In Used For
Frontmatter Entry .mld file (--- ... ---) Runtime metadata (@fm, imported namespace .__meta__)
module.yml Module directory root Packaging metadata (type, publish/install metadata)

Keep shared identity fields (name, author, version, about) aligned between frontmatter and module.yml.
</metadata_sources>

<module_types>

Type Purpose Local Path Global Path
library Importable code llm/lib/{name}/ ~/.mlld/lib/{name}/
app Runnable scripts llm/run/{name}/ ~/.mlld/run/{name}/
command Claude slash cmd .claude/commands/{name}/ ~/.claude/commands/{name}/
skill Claude skill .claude/skills/{name}/ ~/.claude/skills/{name}/
</module_types>

<scaffold_commands>

mlld module app myapp              # Create app in llm/run/myapp/
mlld module library utils          # Create library in llm/lib/utils/
mlld module command review         # Create command in .claude/commands/review/
mlld module skill helper           # Create skill in .claude/skills/helper/
mlld module app myapp --global     # Create in ~/.mlld/run/myapp/

</scaffold_commands>

<run_apps>

mlld run myapp                     # Runs llm/run/myapp/index.mld
mlld run                           # Lists available scripts including apps

Use index.mld as the module entry point convention.
</run_apps>

<install_global>

mlld install @author/my-app --global    # Install to ~/.mlld/run/my-app/
mlld install @author/my-lib -g          # Install to ~/.mlld/lib/my-lib/

</install_global>

<packed_format>
Packed modules are single-file bundles created by mlld pack (future feature).
Use packed format for gist publishing; standard module format otherwise.
</packed_format>

Module Patterns

Module patterns:

>> Library module
exe @haiku(prompt) = @prompt | cmd { claude -p --model haiku }
exe @sonnet(prompt) = @prompt | cmd { claude -p --model sonnet }
export { @haiku, @sonnet }

>> Config/agent module
var @meta = { id: @fm.id, name: @fm.name }
var @prompts = { primary: @primaryPrompt, optional: @optionalPrompt }
export { @meta, @prompts }

>> Gate module
exe @gate(response, instruction, message) = [...]
export { @gate }

Local Development Modules

llm/modules/
├── my-utils.mld.md    # author: alice, name: experimental
└── helpers.mld        # author: bob, name: tools
import local { @helper } from @alice/experimental

Matched by frontmatter author and name fields.

Publishing

Publishing Public Modules

Prerequisites

  1. GitHub account - You'll authenticate via GitHub
  2. mlld CLI - Install via npm install -g mlld
  3. Module file - Your .mld/.mld.md file with frontmatter and needs

Required Metadata

---
name: my-tool
author: yourname
version: 1.0.0
about: Brief description of what this does
license: CC0
---

All fields required. License must be CC0.

Authentication

mlld auth login     # Opens GitHub OAuth flow
mlld auth status    # Check auth status
mlld auth logout    # Logout

Grants gist scope (create Gists for module source) and public_repo scope (create PRs to registry).

First-Time Module (PR Workflow)

mlld publish my-tool.mld.md
  1. Validation - Checks syntax, exports, metadata
  2. Source Creation - Creates Gist or references repo
  3. PR Creation - Opens PR to mlld-lang/registry
  4. Automated Review - LLM reviews for no hardcoded secrets, safe operations, real utility, proper licensing
  5. Manual Review (if needed) - Maintainer approval
  6. Merge - Module becomes available
  7. Publish Rights - You can update directly going forward

Module Updates (Direct Publish)

After first module is merged:

mlld publish my-tool.mld.md
  1. Version Bump - Prompts for patch/minor/major
  2. Validation - Same checks as first-time
  3. Direct Publish - No PR needed (if authenticated)
  4. Registry Update - New version available immediately

Force PR workflow: mlld publish --pr my-tool.mld.md

Module Source

GitHub Repository — If your module is in a git repo, mlld publish detects the repository URL, current commit SHA, module file path, and whether repo is clean. Source references the commit SHA, ensuring immutability.

Gist — If not in a repo, creates a GitHub Gist automatically with module content, versioned via Gist revisions.

Validation

Your module must pass validation before publishing:

  • Syntax - No syntax errors, no reserved word conflicts, valid directives
  • Exports - export directive present, exported names exist, no duplicates
  • Imports - Valid module references, no circular dependencies
  • Metadata - All required fields present, author matches GitHub username, license is CC0, version follows semver

Automated Review

LLM reviews check for no secrets, safe operations, real utility, proper licensing, accurate metadata. Review posts as PR comment with APPROVE, REQUEST_CHANGES, or COMMENT.

Push new commits to trigger re-review. Trusted authors in allowlist skip LLM review and auto-merge if CI passes.

Direct Publish via API

mlld publish my-tool.mld.md                    # Via CLI (recommended)

curl -X POST https://registry-api.mlld.org/api/publish \
  -H "Authorization: Bearer $TOKEN" \
  -F "module=@my-tool.mld.md"                  # Via API directly

Commands

mlld publish my-tool.mld.md                    # Publish to registry
mlld publish --pr my-tool.mld.md               # Force PR workflow
mlld publish --tag beta my-tool.mld.md         # Publish with tag
mlld publish --dry-run my-tool.mld.md          # Validate without publishing

Publishing Private Modules

For private or internal modules, use local imports or custom resolvers instead of the public registry.

Local Filesystem

Distribute modules via git or file sharing:

import { @helper } from "./shared/utils.mld"
import "./lib/internal" as @internal

Custom Resolvers

Configure custom @ prefixes for private registries or internal repos:

{
  "resolvers": {
    "@company": "https://internal-registry.company.com/modules"
  }
}
import { @auth } from @company/auth-utils

See mlld howto resolvers for resolver configuration.

Development Modules

Use llm/modules/ for in-development modules:

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

These are resolved from the local llm/modules/ directory without registry lookup.

Updating Modules

Check installed modules:

mlld ls                          # list all installed modules with versions

Update a specific module:

mlld update @alice/utils         # fetch latest version

Update all modules:

mlld update                      # updates everything in mlld-lock.json

Check module info:

mlld registry info @alice/utils  # show module details from registry

Version pinning:

The lock file (mlld-lock.json) tracks installed versions:

  • version: "latest" - updates to newest on mlld update
  • version: "1.2.0" - stays pinned until manually changed

After updating:

Verify new exports are available:

mlld validate your-file.mld      # check imports resolve

Versioning

Semantic Versioning

Follow semver (major.minor.patch):

  • 1.0.0 - Initial release
  • 1.0.1 - Bug fix (backward compatible)
  • 1.1.0 - New feature (backward compatible)
  • 2.0.0 - Breaking change

Version Tags

Publish with tags:

mlld publish --tag beta my-tool.mld.md

Import via tag:

import { @helper } from @alice/my-tool@beta
import { @helper } from @alice/utils@^1.0.0

Common tags:

  • latest - Most recent stable (default)
  • stable - Recommended version
  • beta - Beta testing
  • alpha - Alpha testing

Version Ranges

Specify ranges in mlld-config.json:

{
  "dependencies": {
    "@alice/my-tool": "^1.0.0",
    "@bob/utils": "~1.2.0",
    "@eve/lib": ">=1.0.0 <2.0.0"
  }
}

Lock file pins exact versions.

Version Resolution in Imports

Import Version
import ... from @alice/utils latest
import ... from @alice/utils@1.0.0 exact
import ... from @alice/utils@^1.0.0 compatible
import ... from @alice/utils@beta tag

Registry Metadata

Registry Structure

Modules are stored with version history:

registry/
└── modules/
    └── alice/
        └── my-tool/
            ├── metadata.json      # Core info, owners
            ├── 1.0.0.json         # Version 1.0.0
            ├── 1.0.1.json         # Version 1.0.1
            └── tags.json          # latest, stable, etc.

metadata.json

{
  "name": "my-tool",
  "author": "alice",
  "about": "Brief description",
  "owners": ["alice"],
  "maintainers": [],
  "created": "2024-01-01T00:00:00Z",
  "createdBy": 12345,
  "firstPublishPR": 123
}

{version}.json

{
  "version": "1.0.0",
  "needs": ["js", "sh"],
  "license": "CC0",
  "mlldVersion": ">=1.0.0",
  "source": {
    "type": "github",
    "url": "https://raw.githubusercontent.com/...",
    "contentHash": "sha256:abc123...",
    "repository": {
      "type": "git",
      "url": "https://github.com/alice/repo",
      "commit": "abc123",
      "path": "my-tool.mld.md"
    }
  },
  "dependencies": {
    "js": {
      "packages": ["lodash"]
    }
  },
  "keywords": ["utility", "automation"],
  "publishedAt": "2024-01-01T00:00:00Z",
  "publishedBy": 12345
}

Key fields:

  • publishedBy - GitHub user ID of the publisher (numeric ID, not username)
  • publishedAt - ISO timestamp when this version was published
  • source.type - Source type: github, gist, or private-repo
  • source.contentHash - SHA256 hash for content verification
  • source.repository - Git repository metadata (for github/private-repo sources)

tags.json

{
  "latest": "1.0.1",
  "stable": "1.0.1",
  "beta": "2.0.0-beta.1"
}

Registry API

## Resolve version
curl https://registry-api.mlld.org/api/resolve?module=@alice/my-tool

## Direct publish
curl -X POST https://registry-api.mlld.org/api/publish \
  -H "Authorization: Bearer $TOKEN" \
  -F "module=@my-tool.mld.md"

Ownership and Permissions

Module Owners

After your first PR merges, you become module owner:

  • Can publish updates directly (no PR needed)
  • Can add maintainers
  • Module namespaced under your GitHub username

Maintainers

Add collaborators to metadata.json:

{
  "owners": ["alice"],
  "maintainers": ["bob", "eve"]
}

Maintainers can also publish updates.

Organization Modules

Publish under org namespace:

---
author: company
name: auth-tool
---

Requires write access to @company in registry.