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
thiscontext) - 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 onmlld 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 versionabout- Brief descriptionlicense- 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
exportdirective, all module-level items are auto-exported
Module Structure
Modules are directories with an entry point, manifest, and optional supporting files.
<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
- GitHub account - You'll authenticate via GitHub
- mlld CLI - Install via
npm install -g mlld - Module file - Your
.mld/.mld.mdfile with frontmatter andneeds
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
- Validation - Checks syntax, exports, metadata
- Source Creation - Creates Gist or references repo
- PR Creation - Opens PR to mlld-lang/registry
- Automated Review - LLM reviews for no hardcoded secrets, safe operations, real utility, proper licensing
- Manual Review (if needed) - Maintainer approval
- Merge - Module becomes available
- Publish Rights - You can update directly going forward
Module Updates (Direct Publish)
After first module is merged:
mlld publish my-tool.mld.md
- Version Bump - Prompts for patch/minor/major
- Validation - Same checks as first-time
- Direct Publish - No PR needed (if authenticated)
- 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 -
exportdirective 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 onmlld updateversion: "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 versionbeta- Beta testingalpha- 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, orprivate-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.