···72727373#### Semicolons
74747575-- **Records** do NOT have semicolons after the closing brace `}`
7676-- All other definitions require semicolons:
7777- - `use` statements end with `;`
7878- - `token` definitions end with `;`
7979- - `inline type` definitions end with `;`
8080- - `def type` definitions end with `;`
8181- - `query` definitions end with `;`
8282- - `procedure` definitions end with `;`
8383- - `subscription` definitions end with `;`
7575+All definitions require semicolons:
7676+- `record` definitions end with `};`
7777+- `use` statements end with `;`
7878+- `token` definitions end with `;`
7979+- `inline type` definitions end with `;`
8080+- `def type` definitions end with `;`
8181+- `query` definitions end with `;`
8282+- `procedure` definitions end with `;`
8383+- `subscription` definitions end with `;`
84848585#### Commas
8686
+1
website/content/docs/_index.md
···33description = "Complete guide to MLF"
44sort_by = "weight"
55template = "section.html"
66+redirect_to = "/docs/getting-started/"
67+++
7889MLF (Matt's Lexicon Format) is a human-friendly DSL for writing ATProto Lexicons. This documentation will help you learn the language, use the CLI tools, and integrate MLF into your projects.
···11++++
22+title = "Your First Lexicon"
33+weight = 1
44++++
55+66+Welcome to MLF! Let's create your first lexicon by defining a simple record.
77+88+## A Basic Record
99+1010+Here's a complete MLF file that defines a user profile:
1111+1212+```mlf
1313+/// A user profile
1414+record profile {
1515+ /// The user's display name
1616+ name: string,
1717+ /// The user's email address
1818+ email: string,
1919+ /// When the account was created
2020+ createdAt: Datetime,
2121+}
2222+```
2323+2424+This defines a `profile` record with three fields: `name`, `email`, and `createdAt`.
2525+2626+## File Naming and Namespaces
2727+2828+The file path determines the lexicon namespace. If you save this as:
2929+3030+```
3131+com/example/forum/profile.mlf
3232+```
3333+3434+Then the namespace will be `com.example.forum.profile`, and the full identifier for this record is `com.example.forum.profile`.
3535+3636+The namespace comes from the file path, not from any declaration in the file.
3737+3838+## What Gets Generated
3939+4040+When you compile this MLF file, it generates a JSON lexicon:
4141+4242+```json
4343+{
4444+ "lexicon": 1,
4545+ "id": "com.example.forum.profile",
4646+ "defs": {
4747+ "main": {
4848+ "type": "record",
4949+ "description": "A user profile",
5050+ "key": "tid",
5151+ "record": {
5252+ "type": "object",
5353+ "required": ["name", "email", "createdAt"],
5454+ "properties": {
5555+ "name": {
5656+ "type": "string",
5757+ "description": "The user's display name"
5858+ },
5959+ "email": {
6060+ "type": "string",
6161+ "description": "The user's email address"
6262+ },
6363+ "createdAt": {
6464+ "type": "string",
6565+ "format": "datetime",
6666+ "description": "When the account was created"
6767+ }
6868+ }
6969+ }
7070+ }
7171+ }
7272+}
7373+```
7474+7575+The MLF syntax is much cleaner and easier to read!
7676+7777+## Comments
7878+7979+MLF supports three types of comments:
8080+8181+**Documentation comments** (`///`) appear in the generated lexicon:
8282+```mlf
8383+/// This comment appears in generated docs
8484+record example {
8585+ /// This field comment also appears
8686+ field: string,
8787+}
8888+```
8989+9090+**Regular comments** (`//`) are for internal notes only:
9191+```mlf
9292+// This is a note to yourself, won't appear in output
9393+record example {
9494+ field: string, // Inline comments work too
9595+}
9696+```
9797+9898+**Hash comments** (`#`) at the start of a file are ignored (useful for shebangs):
9999+```mlf
100100+#!/usr/bin/env mlf
101101+# This line is ignored
102102+103103+record example {
104104+ field: string,
105105+}
106106+```
107107+108108+## Complete Example
109109+110110+Here's a minimal, complete lexicon for a forum post:
111111+112112+**File: `com/example/forum/post.mlf`**
113113+```mlf
114114+/// A forum post
115115+record post {
116116+ /// Post title
117117+ title: string,
118118+ /// Post content
119119+ body: string,
120120+ /// Post author's DID
121121+ author: Did,
122122+ /// When the post was published
123123+ publishedAt: Datetime,
124124+}
125125+```
126126+127127+This creates the lexicon `com.example.forum.post` with a single record definition.
128128+129129+## What's Next?
130130+131131+Now that you understand the basics, let's learn about fields in more detail.
+198
website/content/docs/language-guide/02-fields.md
···11++++
22+title = "Fields"
33+weight = 2
44++++
55+66+Fields are the building blocks of records. Let's explore the different types of fields you can define.
77+88+## Required vs Optional Fields
99+1010+By default, all fields are required. Use `?` to make a field optional:
1111+1212+```mlf
1313+record user {
1414+ name: string, // Required - must be provided
1515+ bio?: string, // Optional - can be omitted
1616+ email: string, // Required
1717+ website?: string, // Optional
1818+}
1919+```
2020+2121+## Primitive Types
2222+2323+MLF supports several primitive types:
2424+2525+**Strings:**
2626+```mlf
2727+record example {
2828+ name: string,
2929+}
3030+```
3131+3232+**Integers** (64-bit signed):
3333+```mlf
3434+record example {
3535+ count: integer,
3636+ age: integer,
3737+}
3838+```
3939+4040+**Numbers** (double-precision floats):
4141+```mlf
4242+record example {
4343+ price: number,
4444+ rating: number,
4545+}
4646+```
4747+4848+**Booleans:**
4949+```mlf
5050+record example {
5151+ isActive: boolean,
5252+ verified: boolean,
5353+}
5454+```
5555+5656+**Binary data:**
5757+```mlf
5858+record example {
5959+ data: bytes, // Raw byte array
6060+ image: blob, // Binary with metadata (MIME type, size)
6161+}
6262+```
6363+6464+**Unknown** (for forward compatibility):
6565+```mlf
6666+record example {
6767+ metadata: unknown, // Can be any value
6868+}
6969+```
7070+7171+**Null:**
7272+```mlf
7373+record example {
7474+ nothing: null, // Always null (rarely used)
7575+}
7676+```
7777+7878+## Special Format Types
7979+8080+MLF provides built-in types for common formats:
8181+8282+```mlf
8383+record post {
8484+ author: Did, // Decentralized Identifier (did:*)
8585+ uri: AtUri, // AT Protocol URI (at://...)
8686+ timestamp: Datetime, // ISO 8601 datetime
8787+ website: Uri, // Generic URI
8888+ contentHash: Cid, // Content Identifier
8989+ handle: Handle, // Handle (domain name)
9090+ language: Language, // BCP 47 language code
9191+}
9292+```
9393+9494+These are actually inline type aliases defined in the prelude, but you can use them as if they were primitive types.
9595+9696+## Objects
9797+9898+Define inline object types with curly braces:
9999+100100+```mlf
101101+record post {
102102+ author: {
103103+ did: Did,
104104+ handle: Handle,
105105+ name: string,
106106+ },
107107+}
108108+```
109109+110110+Objects can be nested:
111111+112112+```mlf
113113+record profile {
114114+ location: {
115115+ city: string,
116116+ coordinates: {
117117+ lat: number,
118118+ lng: number,
119119+ },
120120+ },
121121+}
122122+```
123123+124124+## Arrays
125125+126126+Add `[]` after any type to make it an array:
127127+128128+```mlf
129129+record post {
130130+ tags: string[], // Array of strings
131131+ images: Uri[], // Array of URIs
132132+ counts: integer[], // Array of integers
133133+}
134134+```
135135+136136+Arrays of objects:
137137+138138+```mlf
139139+record post {
140140+ authors: {
141141+ did: Did,
142142+ role: string,
143143+ }[],
144144+}
145145+```
146146+147147+Nested arrays:
148148+149149+```mlf
150150+record matrix {
151151+ grid: integer[][], // Array of arrays
152152+}
153153+```
154154+155155+## Complete Example
156156+157157+Here's a complete record showing all field types:
158158+159159+**File: `com/example/forum/post.mlf`**
160160+```mlf
161161+/// A forum post
162162+record post {
163163+ /// Post text content
164164+ text: string,
165165+166166+ /// Post author
167167+ author: Did,
168168+169169+ /// When the post was created
170170+ createdAt: Datetime,
171171+172172+ /// Optional reply count
173173+ replyCount?: integer,
174174+175175+ /// Whether the post is pinned
176176+ isPinned: boolean,
177177+178178+ /// Optional geographic location
179179+ location?: {
180180+ name: string,
181181+ lat: number,
182182+ lng: number,
183183+ },
184184+185185+ /// Tags on this post
186186+ tags: string[],
187187+188188+ /// Optional embedded images
189189+ images?: Uri[],
190190+191191+ /// Arbitrary metadata
192192+ metadata: unknown,
193193+}
194194+```
195195+196196+## What's Next?
197197+198198+Now that you understand fields, let's learn how to add validation rules with constraints.
···11++++
22+title = "Custom Types"
33+weight = 4
44++++
55+66+As you build lexicons, you'll find yourself repeating the same constraints and object shapes. Custom types help you avoid duplication.
77+88+## Inline Types
99+1010+Inline types are like macros or type aliases - they expand at the point of use and never appear in the generated lexicon.
1111+1212+**Without inline types:**
1313+```mlf
1414+record user {
1515+ name: string constrained {
1616+ minGraphemes: 1,
1717+ maxGraphemes: 100,
1818+ },
1919+ displayName: string constrained {
2020+ minGraphemes: 1,
2121+ maxGraphemes: 100,
2222+ },
2323+}
2424+```
2525+2626+**With inline types:**
2727+```mlf
2828+inline type ShortText = string constrained {
2929+ minGraphemes: 1,
3030+ maxGraphemes: 100,
3131+};
3232+3333+record user {
3434+ name: ShortText,
3535+ displayName: ShortText,
3636+}
3737+```
3838+3939+When compiled, `ShortText` is replaced with the full constraint definition. It's purely for convenience.
4040+4141+**More examples:**
4242+```mlf
4343+inline type PositiveInt = integer constrained {
4444+ minimum: 0,
4545+};
4646+4747+inline type EmailAddress = string constrained {
4848+ format: "email",
4949+ maxLength: 254,
5050+};
5151+5252+inline type UserId = Did; // Simple alias
5353+5454+record account {
5555+ id: UserId,
5656+ email: EmailAddress,
5757+ loginCount: PositiveInt,
5858+}
5959+```
6060+6161+Inline types can define objects too:
6262+6363+```mlf
6464+inline type Coordinates = {
6565+ lat: number,
6666+ lng: number,
6767+};
6868+6969+record location {
7070+ coords: Coordinates,
7171+}
7272+```
7373+7474+## Def Types
7575+7676+When you want a type to be **shared and referenced by name** in the generated lexicon, use `def type`:
7777+7878+```mlf
7979+def type author = {
8080+ did: Did,
8181+ handle: Handle,
8282+ displayName?: string,
8383+};
8484+8585+record post {
8686+ author: author,
8787+}
8888+8989+record comment {
9090+ author: author,
9191+}
9292+```
9393+9494+In the generated lexicon, `author` appears as a named definition that both `post` and `comment` reference.
9595+9696+**Key difference:**
9797+- `inline type` - expands inline, doesn't appear in output
9898+- `def type` - becomes a named definition, referenced by name
9999+100100+## When to Use Each
101101+102102+**Use inline types for:**
103103+- Type aliases (`inline type UserId = Did;`)
104104+- Reusable constraint patterns
105105+- Types that should be transparent in the output
106106+- Simple wrappers
107107+108108+**Use def types for:**
109109+- Complex objects used multiple times
110110+- Types that form part of your API contract
111111+- Types you want to reference from other files
112112+- Types that should have their own documentation
113113+114114+## Example Comparison
115115+116116+**Inline type (expands everywhere):**
117117+```mlf
118118+inline type ShortString = string constrained {
119119+ maxGraphemes: 100,
120120+};
121121+122122+record post {
123123+ title: ShortString, // Expands to: string constrained { maxGraphemes: 100 }
124124+}
125125+```
126126+127127+**Def type (referenced by name):**
128128+```mlf
129129+def type postRef = {
130130+ uri: AtUri,
131131+ cid: Cid,
132132+};
133133+134134+record reply {
135135+ replyTo: postRef, // References: #postRef in lexicon
136136+}
137137+```
138138+139139+## Complete Example
140140+141141+Here's a complete file showing both types:
142142+143143+**File: `com/example/forum/thread.mlf`**
144144+```mlf
145145+// Inline types for common patterns
146146+inline type ShortText = string constrained {
147147+ minGraphemes: 1,
148148+ maxGraphemes: 200,
149149+};
150150+151151+inline type LongText = string constrained {
152152+ maxGraphemes: 50000,
153153+};
154154+155155+// Def type for shared object
156156+def type author = {
157157+ did: Did,
158158+ handle: Handle,
159159+ displayName?: ShortText,
160160+};
161161+162162+/// A forum thread
163163+record thread {
164164+ /// Thread title
165165+ title: ShortText,
166166+167167+ /// Thread body
168168+ body: LongText,
169169+170170+ /// Thread author
171171+ author: author,
172172+173173+ /// When created
174174+ createdAt: Datetime,
175175+}
176176+177177+/// A reply to a thread
178178+record reply {
179179+ /// Reply text
180180+ text: LongText,
181181+182182+ /// Reply author (reuses author type)
183183+ author: author,
184184+185185+ /// When created
186186+ createdAt: Datetime,
187187+}
188188+```
189189+190190+In the generated lexicon:
191191+- `ShortText` and `LongText` don't appear - they're expanded inline
192192+- `author` appears as a named definition in the `defs` block
193193+- Both `thread` and `reply` reference `#author`
194194+195195+## What's Next?
196196+197197+Now that you can create reusable types, let's learn about unions for accepting multiple types.
+168
website/content/docs/language-guide/05-unions.md
···11++++
22+title = "Unions"
33+weight = 5
44++++
55+66+Unions allow a field to accept multiple types. MLF supports both closed unions (fixed set of types) and open unions (allowing unknown types).
77+88+## Closed Unions
99+1010+Use the pipe operator `|` to create a union of types:
1111+1212+```mlf
1313+def type textPost = {
1414+ text: string,
1515+};
1616+1717+def type imagePost = {
1818+ image: Uri,
1919+ caption?: string,
2020+};
2121+2222+def type videoPost = {
2323+ video: Uri,
2424+ duration: integer,
2525+};
2626+2727+record post {
2828+ content: textPost | imagePost | videoPost,
2929+}
3030+```
3131+3232+The `content` field must be one of these three types. No other types are accepted.
3333+3434+## Open Unions
3535+3636+Add `| _` to allow unknown types for forward compatibility:
3737+3838+```mlf
3939+record post {
4040+ content: textPost | imagePost | _,
4141+}
4242+```
4343+4444+Now the system knows about `textPost` and `imagePost`, but will also accept unknown types it hasn't seen before. This is useful when you expect the union to grow in the future.
4545+4646+## Why Use Open Unions?
4747+4848+Open unions help with forward compatibility:
4949+5050+```mlf
5151+// Version 1: Only text and images
5252+record post {
5353+ content: textPost | imagePost | _,
5454+}
5555+5656+// Version 2: Add video support
5757+// Old clients still work because of the `_`
5858+def type videoPost = {
5959+ video: Uri,
6060+};
6161+6262+record post {
6363+ content: textPost | imagePost | videoPost | _,
6464+}
6565+```
6666+6767+Old clients that don't know about `videoPost` can still handle the lexicon because of the open union.
6868+6969+## Unions with Inline Objects
7070+7171+You don't need to define types separately:
7272+7373+```mlf
7474+record embed {
7575+ content: {
7676+ text: string,
7777+ } | {
7878+ image: Uri,
7979+ } | {
8080+ link: Uri,
8181+ title: string,
8282+ },
8383+}
8484+```
8585+8686+Though defining types separately is often cleaner.
8787+8888+## Unions in Arrays
8989+9090+Unions work in arrays:
9191+9292+```mlf
9393+def type mention = {
9494+ did: Did,
9595+ start: integer,
9696+ end: integer,
9797+};
9898+9999+def type link = {
100100+ uri: Uri,
101101+ start: integer,
102102+ end: integer,
103103+};
104104+105105+def type tag = {
106106+ name: string,
107107+ start: integer,
108108+ end: integer,
109109+};
110110+111111+record post {
112112+ text: string,
113113+ facets: (mention | link | tag)[],
114114+}
115115+```
116116+117117+The `facets` array can contain any mix of mentions, links, and tags.
118118+119119+## Complete Example
120120+121121+Here's a complete forum post system with unions:
122122+123123+**File: `com/example/forum/post.mlf`**
124124+```mlf
125125+/// Text content
126126+def type textContent = {
127127+ text: string constrained {
128128+ maxGraphemes: 2000,
129129+ },
130130+};
131131+132132+/// Image content
133133+def type imageContent = {
134134+ url: Uri,
135135+ width: integer,
136136+ height: integer,
137137+ alt?: string,
138138+};
139139+140140+/// File attachment
141141+def type fileContent = {
142142+ url: Uri,
143143+ filename: string,
144144+ size: integer,
145145+ mimeType: string,
146146+};
147147+148148+/// A forum post with different content types
149149+record post {
150150+ /// Post author
151151+ author: Did,
152152+153153+ /// Post content (text, image, or file)
154154+ content: textContent | imageContent | fileContent | _,
155155+156156+ /// When the post was created
157157+ createdAt: Datetime,
158158+159159+ /// Optional reply reference
160160+ replyTo?: AtUri,
161161+}
162162+```
163163+164164+The `| _` at the end means future content types can be added without breaking old clients.
165165+166166+## What's Next?
167167+168168+Now that you understand unions, let's learn about tokens for named constants.
+245
website/content/docs/language-guide/06-tokens.md
···11++++
22+title = "Tokens"
33+weight = 6
44++++
55+66+Tokens are named constants that can be used in enums, known values, defaults, and unions. They make your lexicons more maintainable and self-documenting.
77+88+## Defining Tokens
99+1010+Tokens are simple named values with documentation:
1111+1212+```mlf
1313+/// Open state
1414+token open;
1515+1616+/// Closed state
1717+token closed;
1818+1919+record issue {
2020+ state: string constrained {
2121+ enum: [open, closed],
2222+ },
2323+}
2424+```
2525+2626+Notice we reference tokens without quotes: `[open, closed]`, not `["open", "closed"]`.
2727+2828+## Tokens in Constraints
2929+3030+Tokens work in enum and knownValues constraints:
3131+3232+```mlf
3333+/// Public visibility
3434+token public;
3535+3636+/// Private visibility
3737+token private;
3838+3939+/// Unlisted visibility
4040+token unlisted;
4141+4242+record post {
4343+ visibility: string constrained {
4444+ enum: [public, private, unlisted],
4545+ default: public,
4646+ },
4747+}
4848+```
4949+5050+## Tokens in Unions
5151+5252+Tokens can be used directly in unions:
5353+5454+```mlf
5555+/// Success status
5656+token success;
5757+5858+/// Error status
5959+token error;
6060+6161+/// Pending status
6262+token pending;
6363+6464+record result {
6565+ status: success | error | pending,
6666+}
6767+```
6868+6969+This creates a union where the field must be one of these three token values.
7070+7171+## How Tokens Become Strings
7272+7373+**Important:** In the generated lexicon, tokens are converted to **fully qualified NSID string literals**.
7474+7575+If you define this in `com/example/forum/post.mlf`:
7676+7777+```mlf
7878+token draft;
7979+token published;
8080+8181+record post {
8282+ status: string constrained {
8383+ enum: [draft, published],
8484+ },
8585+}
8686+```
8787+8888+The generated lexicon will have:
8989+9090+```json
9191+{
9292+ "status": {
9393+ "type": "string",
9494+ "enum": ["com.example.forum.post#draft", "com.example.forum.post#published"]
9595+ }
9696+}
9797+```
9898+9999+The tokens become fully qualified: `com.example.forum.post#draft` and `com.example.forum.post#published`.
100100+101101+## Why Use Tokens?
102102+103103+**Without tokens:**
104104+```mlf
105105+record post {
106106+ state: string constrained {
107107+ knownValues: ["draft", "published", "archived"],
108108+ },
109109+}
110110+```
111111+112112+- No documentation for individual values
113113+- Easy to typo
114114+- Hard to reuse across files
115115+116116+**With tokens:**
117117+```mlf
118118+/// Draft state - not yet published
119119+token draft;
120120+121121+/// Published state - visible to all
122122+token published;
123123+124124+/// Archived state - no longer active
125125+token archived;
126126+127127+record post {
128128+ state: string constrained {
129129+ knownValues: [draft, published, archived],
130130+ },
131131+}
132132+```
133133+134134+- Each value is documented
135135+- No typos (references are checked)
136136+- Can be reused across definitions
137137+- More maintainable
138138+139139+## Reusing Tokens
140140+141141+Define tokens once and use them everywhere:
142142+143143+```mlf
144144+token active;
145145+token inactive;
146146+token suspended;
147147+148148+record user {
149149+ status: string constrained {
150150+ enum: [active, inactive, suspended],
151151+ },
152152+}
153153+154154+record account {
155155+ status: string constrained {
156156+ enum: [active, inactive],
157157+ },
158158+}
159159+```
160160+161161+## Tokens vs String Literals
162162+163163+You can mix tokens and string literals:
164164+165165+```mlf
166166+token published;
167167+token archived;
168168+169169+record post {
170170+ status: string constrained {
171171+ knownValues: [published, archived, "draft"], // Mix of token and literal
172172+ },
173173+}
174174+```
175175+176176+But using tokens consistently is cleaner.
177177+178178+## Complete Example
179179+180180+Here's a complete forum thread system using tokens:
181181+182182+**File: `com/example/forum/thread.mlf`**
183183+```mlf
184184+/// Thread is open for replies
185185+token open;
186186+187187+/// Thread is closed
188188+token closed;
189189+190190+/// Thread is pinned
191191+token pinned;
192192+193193+/// Thread is locked
194194+token locked;
195195+196196+/// High priority
197197+token high;
198198+199199+/// Medium priority
200200+token medium;
201201+202202+/// Low priority
203203+token low;
204204+205205+/// A thread in a forum
206206+record thread {
207207+ /// Thread title
208208+ title: string constrained {
209209+ minGraphemes: 1,
210210+ maxGraphemes: 200,
211211+ },
212212+213213+ /// Thread content
214214+ body?: string constrained {
215215+ maxGraphemes: 5000,
216216+ },
217217+218218+ /// Thread status
219219+ status: string constrained {
220220+ enum: [open, closed, pinned, locked],
221221+ default: open,
222222+ },
223223+224224+ /// Thread priority
225225+ priority: string constrained {
226226+ enum: [high, medium, low],
227227+ default: medium,
228228+ },
229229+230230+ /// Thread author
231231+ author: Did,
232232+233233+ /// When thread was created
234234+ createdAt: Datetime,
235235+}
236236+```
237237+238238+In the generated lexicon:
239239+- `open` becomes `com.example.forum.thread#open`
240240+- `closed` becomes `com.example.forum.thread#closed`
241241+- And so on...
242242+243243+## What's Next?
244244+245245+Now that you understand data types, let's learn about XRPC operations: queries, procedures, and subscriptions.
+345
website/content/docs/language-guide/07-xrpc.md
···11++++
22+title = "XRPC"
33+weight = 7
44++++
55+66+So far we've defined data structures with records. Now let's define operations using XRPC (Cross-organizational RPC).
77+88+## XRPC Structure
99+1010+All XRPC definitions follow this pattern:
1111+1212+```
1313+<keyword> <name>(<input>):<output>
1414+```
1515+1616+Where:
1717+- **keyword** - `query`, `procedure`, or `subscription`
1818+- **name** - The operation name
1919+- **input** - Parameters in parentheses
2020+- **output** - Return type after `:`
2121+2222+## Queries
2323+2424+Queries are **read-only operations** that use HTTP GET. They retrieve data without modifying state.
2525+2626+**Basic query:**
2727+```mlf
2828+/// Get a user profile
2929+query getProfile(
3030+ actor: Did
3131+):{
3232+ did: Did,
3333+ handle: Handle,
3434+ displayName?: string,
3535+};
3636+```
3737+3838+**Query with optional parameters:**
3939+```mlf
4040+/// Search for posts
4141+query searchPosts(
4242+ q: string,
4343+ limit?: integer constrained {
4444+ minimum: 1,
4545+ maximum: 100,
4646+ default: 25,
4747+ },
4848+ cursor?: string
4949+):{
5050+ posts: post[],
5151+ cursor?: string,
5252+};
5353+```
5454+5555+**Query returning a record:**
5656+```mlf
5757+record profile {
5858+ did: Did,
5959+ handle: Handle,
6060+ displayName?: string,
6161+}
6262+6363+query getProfile(
6464+ actor: Did
6565+):profile;
6666+```
6767+6868+## Procedures
6969+7070+Procedures are **write operations** that use HTTP POST. They create, update, or delete data.
7171+7272+**Basic procedure:**
7373+```mlf
7474+/// Create a new post
7575+procedure createPost(
7676+ text: string constrained {
7777+ minGraphemes: 1,
7878+ maxGraphemes: 500,
7979+ }
8080+):{
8181+ uri: AtUri,
8282+ cid: Cid,
8383+};
8484+```
8585+8686+**Procedure with multiple parameters:**
8787+```mlf
8888+/// Update a user profile
8989+procedure updateProfile(
9090+ displayName?: string,
9191+ bio?: string,
9292+ avatar?: blob
9393+):{
9494+ success: boolean,
9595+};
9696+```
9797+9898+**Delete procedure:**
9999+```mlf
100100+/// Delete a post
101101+procedure deletePost(
102102+ uri: AtUri
103103+):{
104104+ success: boolean,
105105+};
106106+```
107107+108108+## Subscriptions
109109+110110+Subscriptions are **real-time event streams** over WebSocket. They push updates to clients as events occur.
111111+112112+**Basic subscription:**
113113+```mlf
114114+/// Subscribe to new posts
115115+subscription subscribePosts():post;
116116+```
117117+118118+**Subscription with parameters:**
119119+```mlf
120120+/// Subscribe to posts from specific users
121121+subscription subscribePosts(
122122+ authors?: Did[]
123123+):post;
124124+```
125125+126126+**Subscription with multiple message types:**
127127+```mlf
128128+def type postCreated = {
129129+ post: post,
130130+};
131131+132132+def type postDeleted = {
133133+ uri: AtUri,
134134+};
135135+136136+def type postUpdated = {
137137+ post: post,
138138+};
139139+140140+/// Subscribe to post events
141141+subscription subscribePostEvents():postCreated | postDeleted | postUpdated;
142142+```
143143+144144+**Resumable subscription with cursor:**
145145+```mlf
146146+/// Subscribe to repository events
147147+subscription subscribeRepos(
148148+ cursor?: integer
149149+):commit | identity | tombstone;
150150+```
151151+152152+The `cursor` parameter lets clients resume from where they left off.
153153+154154+## Differences Between Operations
155155+156156+| Feature | Query | Procedure | Subscription |
157157+|---------|-------|-----------|--------------|
158158+| HTTP Method | GET | POST | WebSocket |
159159+| Purpose | Read data | Write data | Real-time events |
160160+| Idempotent | Yes | Usually no | N/A |
161161+| Errors | `| error` | `| error` | Error frames |
162162+163163+## Error Handling
164164+165165+Queries and procedures can specify errors using `| error`:
166166+167167+```mlf
168168+query getPost(
169169+ uri: AtUri
170170+):post | error {
171171+ /// Post not found
172172+ NotFound,
173173+ /// No permission to view
174174+ Forbidden,
175175+};
176176+```
177177+178178+**Procedure with errors:**
179179+```mlf
180180+procedure createPost(
181181+ text: string
182182+):{
183183+ uri: AtUri,
184184+ cid: Cid,
185185+} | error {
186186+ /// Text exceeds maximum length
187187+ TextTooLong,
188188+ /// User is rate limited
189189+ RateLimited,
190190+ /// User not authenticated
191191+ Unauthorized,
192192+};
193193+```
194194+195195+Each error should have a doc comment explaining when it occurs.
196196+197197+**Note:** Subscriptions don't use `| error` - errors are sent as special message frames over the WebSocket connection.
198198+199199+## Parameters
200200+201201+Parameters can have constraints just like record fields:
202202+203203+```mlf
204204+query searchPosts(
205205+ /// Search query (1-200 characters)
206206+ q: string constrained {
207207+ minLength: 1,
208208+ maxLength: 200,
209209+ },
210210+211211+ /// Results per page
212212+ limit?: integer constrained {
213213+ minimum: 1,
214214+ maximum: 100,
215215+ default: 25,
216216+ }
217217+):{
218218+ posts: post[],
219219+}
220220+```
221221+222222+## Return Types
223223+224224+Operations can return:
225225+226226+**Inline objects:**
227227+```mlf
228228+query getStats():{
229229+ posts: integer,
230230+ followers: integer,
231231+};
232232+```
233233+234234+**Named records:**
235235+```mlf
236236+query getProfile(did: Did):profile;
237237+```
238238+239239+**Unions:**
240240+```mlf
241241+query getPost(uri: AtUri):post | deleted;
242242+```
243243+244244+## Complete Example
245245+246246+Here's a complete API for a forum:
247247+248248+**File: `com/example/forum/post.mlf`**
249249+```mlf
250250+/// A forum post
251251+record post {
252252+ /// Post title
253253+ title: string constrained {
254254+ minGraphemes: 1,
255255+ maxGraphemes: 200,
256256+ },
257257+258258+ /// Post content
259259+ body: string constrained {
260260+ maxGraphemes: 50000,
261261+ },
262262+263263+ /// Post author
264264+ author: Did,
265265+266266+ /// When published
267267+ publishedAt: Datetime,
268268+}
269269+270270+/// Get a single post
271271+query getPost(
272272+ /// Post URI
273273+ uri: AtUri
274274+):post | error {
275275+ /// Post not found
276276+ NotFound,
277277+ /// Post is private
278278+ Forbidden,
279279+}
280280+281281+/// List posts by author
282282+query listPosts(
283283+ /// Author DID
284284+ author: Did,
285285+286286+ /// Results per page
287287+ limit?: integer constrained {
288288+ minimum: 1,
289289+ maximum: 100,
290290+ default: 25,
291291+ },
292292+293293+ /// Pagination cursor
294294+ cursor?: string
295295+):{
296296+ posts: post[],
297297+ cursor?: string,
298298+};
299299+300300+/// Create a new post
301301+procedure createPost(
302302+ /// Post title
303303+ title: string constrained {
304304+ minGraphemes: 1,
305305+ maxGraphemes: 200,
306306+ },
307307+308308+ /// Post body
309309+ body: string constrained {
310310+ maxGraphemes: 50000,
311311+ }
312312+):{
313313+ uri: AtUri,
314314+ cid: Cid,
315315+ post: post,
316316+} | error {
317317+ /// User not authenticated
318318+ Unauthorized,
319319+ /// Title or body invalid
320320+ InvalidInput,
321321+};
322322+323323+/// Delete a post
324324+procedure deletePost(
325325+ /// Post URI to delete
326326+ uri: AtUri
327327+):{
328328+ success: boolean,
329329+} | error {
330330+ /// Post not found
331331+ NotFound,
332332+ /// User doesn't own this post
333333+ Forbidden,
334334+};
335335+336336+/// Subscribe to new posts
337337+subscription subscribePosts(
338338+ /// Optional author filter
339339+ author?: Did
340340+):post;
341341+```
342342+343343+## What's Next?
344344+345345+Now that you can define complete APIs, let's learn how to import definitions from other files.
+226
website/content/docs/language-guide/08-imports.md
···11++++
22+title = "Imports"
33+weight = 8
44++++
55+66+As your schemas grow, you'll want to split them across multiple files and reuse definitions. The `use` statement lets you import definitions from other files.
77+88+## Basic Import
99+1010+Import a definition from another file:
1111+1212+```mlf
1313+use com.example.forum.profile;
1414+1515+record post {
1616+ author: profile,
1717+}
1818+```
1919+2020+This imports the `profile` record from `com/example/forum/profile.mlf`.
2121+2222+## How Imports Work
2323+2424+The namespace matches the file path:
2525+2626+| File Path | Namespace | Import Statement |
2727+|-----------|-----------|------------------|
2828+| `com/example/forum/user.mlf` | `com.example.forum.user` | `use com.example.forum.user;` |
2929+| `com/example/forum/post.mlf` | `com.example.forum.post` | `use com.example.forum.post;` |
3030+3131+## What Can Be Imported
3232+3333+You can import **data-shaped definitions**:
3434+3535+- ✅ **Records** - `record user { ... }`
3636+- ✅ **Def types** - `def type author = { ... }`
3737+- ✅ **Tokens** - `token public;`
3838+3939+You **cannot** import XRPC operations:
4040+4141+- ❌ **Queries** - `query getUser(...)`
4242+- ❌ **Procedures** - `procedure createUser(...)`
4343+- ❌ **Subscriptions** - `subscription subscribeUsers(...)`
4444+4545+**Note:** You also cannot import inline types - they're file-local only.
4646+4747+## Using Imported Definitions
4848+4949+Once imported, reference the definition by its name:
5050+5151+```mlf
5252+use com.example.forum.author;
5353+use com.example.forum.postRef;
5454+5555+record comment {
5656+ text: string,
5757+ author: author,
5858+ replyTo: postRef,
5959+}
6060+```
6161+6262+## Multiple Imports
6363+6464+Import multiple definitions with separate `use` statements:
6565+6666+```mlf
6767+use com.example.forum.author;
6868+use com.example.forum.timestamp;
6969+use com.example.forum.location;
7070+7171+record post {
7272+ author: author,
7373+ createdAt: timestamp,
7474+ location?: location,
7575+}
7676+```
7777+7878+## Organizing Files
7979+8080+Common organization patterns:
8181+8282+**By feature:**
8383+```
8484+com/
8585+ example/
8686+ forum/
8787+ user.mlf
8888+ post.mlf
8989+ comment.mlf
9090+```
9191+9292+**Shared types:**
9393+```
9494+com/
9595+ example/
9696+ forum/
9797+ author.mlf
9898+ postRef.mlf
9999+ post.mlf
100100+ comment.mlf
101101+```
102102+103103+## Avoiding Circular Dependencies
104104+105105+Don't create circular imports:
106106+107107+```mlf
108108+// user.mlf
109109+use com.example.forum.post;
110110+111111+record user {
112112+ recentPost?: post, // References post
113113+}
114114+```
115115+116116+```mlf
117117+// post.mlf
118118+use com.example.forum.user;
119119+120120+record post {
121121+ author: user, // References user - CIRCULAR!
122122+}
123123+```
124124+125125+**Solution:** Use a shared reference type:
126126+127127+```mlf
128128+// userRef.mlf
129129+def type userRef = {
130130+ did: Did,
131131+ handle: Handle,
132132+};
133133+```
134134+135135+```mlf
136136+// post.mlf
137137+use com.example.forum.userRef;
138138+139139+record post {
140140+ author: userRef, // No circular dependency
141141+}
142142+```
143143+144144+## Complete Example
145145+146146+Here's a well-organized multi-file lexicon:
147147+148148+**File: `com/example/forum/author.mlf`**
149149+```mlf
150150+/// Basic author information
151151+def type author = {
152152+ did: Did,
153153+ handle: Handle,
154154+ displayName?: string,
155155+};
156156+```
157157+158158+**File: `com/example/forum/postRef.mlf`**
159159+```mlf
160160+/// Reference to a post
161161+def type postRef = {
162162+ uri: AtUri,
163163+ cid: Cid,
164164+};
165165+```
166166+167167+**File: `com/example/forum/post.mlf`**
168168+```mlf
169169+use com.example.forum.author;
170170+use com.example.forum.postRef;
171171+172172+/// A forum post
173173+record post {
174174+ /// Post text
175175+ text: string constrained {
176176+ minGraphemes: 1,
177177+ maxGraphemes: 500,
178178+ },
179179+180180+ /// Post author
181181+ author: author,
182182+183183+ /// Optional reply reference
184184+ replyTo?: postRef,
185185+186186+ /// When created
187187+ createdAt: Datetime,
188188+}
189189+190190+/// Get a post by URI
191191+query getPost(
192192+ uri: AtUri
193193+):post | error {
194194+ NotFound,
195195+};
196196+```
197197+198198+**File: `com/example/forum/comment.mlf`**
199199+```mlf
200200+use com.example.forum.author;
201201+use com.example.forum.postRef;
202202+203203+/// A comment on a post
204204+record comment {
205205+ /// Comment text
206206+ text: string constrained {
207207+ minGraphemes: 1,
208208+ maxGraphemes: 500,
209209+ },
210210+211211+ /// Comment author
212212+ author: author,
213213+214214+ /// Post being commented on
215215+ post: postRef,
216216+217217+ /// When created
218218+ createdAt: Datetime,
219219+}
220220+```
221221+222222+Both `post.mlf` and `comment.mlf` import and reuse the same `author` and `postRef` types.
223223+224224+## What's Next?
225225+226226+Finally, let's learn about the prelude - built-in types available in every file.
+196
website/content/docs/language-guide/09-prelude.md
···11++++
22+title = "Prelude"
33+weight = 9
44++++
55+66+The prelude is a set of definitions automatically available in every MLF file. You don't need to import them - they're always there.
77+88+## String Format Types
99+1010+The prelude provides inline type aliases for common string formats:
1111+1212+```mlf
1313+// These are defined in the prelude - you can use them anywhere
1414+record example {
1515+ id: Did, // Decentralized Identifier (did:*)
1616+ uri: AtUri, // AT Protocol URI (at://...)
1717+ timestamp: Datetime, // ISO 8601 datetime
1818+ website: Uri, // Generic URI
1919+ hash: Cid, // Content Identifier
2020+ username: Handle, // Handle (domain name)
2121+ lang: Language, // BCP 47 language code
2222+}
2323+```
2424+2525+## All Prelude Types
2626+2727+Here are all the format types in the prelude:
2828+2929+| Type | Description | Example |
3030+|------|-------------|---------|
3131+| `Did` | Decentralized Identifier | `did:plc:abc123...` |
3232+| `AtUri` | AT Protocol URI | `at://did:plc:abc/app.bsky.feed.post/123` |
3333+| `AtIdentifier` | DID or Handle | `did:plc:abc` or `alice.com` |
3434+| `Handle` | Domain name handle | `alice.com` |
3535+| `Datetime` | ISO 8601 datetime | `2024-01-15T10:30:00Z` |
3636+| `Uri` | Generic URI | `https://example.com` |
3737+| `Cid` | Content Identifier (IPFS) | `bafyrei...` |
3838+| `Nsid` | Namespaced Identifier | `com.example.post` |
3939+| `Tid` | Timestamp Identifier | `3l2p5g7...` |
4040+| `RecordKey` | Record key in a repo | `3l2p5g7...` |
4141+| `Language` | BCP 47 language tag | `en`, `en-US`, `ja` |
4242+4343+## How They Work
4444+4545+These types are implemented as inline types with format constraints:
4646+4747+```mlf
4848+// Simplified version of what's in the prelude
4949+inline type Did = string constrained {
5050+ format: "did",
5151+};
5252+5353+inline type Datetime = string constrained {
5454+ format: "datetime",
5555+};
5656+5757+inline type Uri = string constrained {
5858+ format: "uri",
5959+};
6060+```
6161+6262+When you use `Datetime` in your record, it expands to `string` with `format: "datetime"`.
6363+6464+## Future: ATProto Types
6565+6666+In the future, the prelude will also include all `com.atproto.*` definitions:
6767+6868+```mlf
6969+// Eventually, these will be in the prelude
7070+record myPost {
7171+ // Reference standard ATProto types without importing
7272+ repo: com.atproto.sync.repo,
7373+ commit: com.atproto.sync.commit,
7474+}
7575+```
7676+7777+This will make it easier to reference standard ATProto types without manual imports.
7878+7979+## Using Prelude Types
8080+8181+You can use prelude types anywhere:
8282+8383+**In records:**
8484+```mlf
8585+record post {
8686+ uri: AtUri,
8787+ author: Did,
8888+ createdAt: Datetime,
8989+}
9090+```
9191+9292+**In queries:**
9393+```mlf
9494+query getPost(
9595+ uri: AtUri
9696+):post;
9797+```
9898+9999+**In constraints:**
100100+```mlf
101101+record event {
102102+ participants: Did[] constrained {
103103+ maxLength: 100,
104104+ },
105105+}
106106+```
107107+108108+**In custom types:**
109109+```mlf
110110+def type reference = {
111111+ uri: AtUri,
112112+ cid: Cid,
113113+};
114114+```
115115+116116+## Complete Example
117117+118118+Here's a complete lexicon using various prelude types:
119119+120120+**File: `com/example/forum/profile.mlf`**
121121+```mlf
122122+/// A user profile
123123+record profile {
124124+ /// User's DID
125125+ did: Did,
126126+127127+ /// User's handle
128128+ handle: Handle,
129129+130130+ /// Display name
131131+ displayName?: string constrained {
132132+ maxGraphemes: 64,
133133+ },
134134+135135+ /// Profile description
136136+ description?: string constrained {
137137+ maxGraphemes: 256,
138138+ },
139139+140140+ /// Avatar image URI
141141+ avatar?: Uri,
142142+143143+ /// Website link
144144+ website?: Uri,
145145+146146+ /// Account creation date
147147+ createdAt: Datetime,
148148+149149+ /// Preferred language
150150+ language?: Language,
151151+}
152152+153153+/// Get a profile by DID or handle
154154+query getProfile(
155155+ /// User identifier (DID or handle)
156156+ actor: AtIdentifier
157157+):profile | error {
158158+ /// Profile not found
159159+ NotFound,
160160+};
161161+162162+/// Update your profile
163163+procedure updateProfile(
164164+ /// New display name
165165+ displayName?: string,
166166+167167+ /// New description
168168+ description?: string,
169169+170170+ /// New avatar URI
171171+ avatar?: Uri,
172172+173173+ /// New website URI
174174+ website?: Uri
175175+):{
176176+ uri: AtUri,
177177+ cid: Cid,
178178+ profile: profile,
179179+} | error {
180180+ /// User not authenticated
181181+ Unauthorized,
182182+};
183183+```
184184+185185+All the format types (`Did`, `Handle`, `Uri`, `Datetime`, `Language`, `AtIdentifier`, `AtUri`, `Cid`) are from the prelude - no imports needed.
186186+187187+## Summary
188188+189189+The prelude provides:
190190+- ✅ String format types for common patterns
191191+- ✅ No imports needed - always available
192192+- ✅ Eventually will include all `com.atproto.*` types
193193+194194+You've now learned all the core features of MLF! You can define records, add constraints, create custom types, use unions and tokens, define XRPC operations, import from other files, and use prelude types.
195195+196196+Check out the [Playground](/playground/) to experiment with MLF, or read the [CLI documentation](/docs/cli/) to learn how to compile your lexicons.
+11
website/content/docs/language-guide/_index.md
···11++++
22+title = "Language Guide"
33+description = "A comprehensive guide to the MLF language"
44+weight = 2
55+sort_by = "weight"
66+template = "section.html"
77++++
88+99+This guide will teach you MLF from the ground up, starting with simple examples and gradually introducing more advanced features.
1010+1111+Each section builds on the previous ones, so we recommend reading them in order if you're new to MLF.
-593
website/content/docs/syntax.md
···11-+++
22-title = "Language Syntax"
33-description = "Complete reference for MLF syntax and features"
44-weight = 2
55-+++
66-77-## File Structure
88-99-### File Extension
1010-- `.mlf` - MLF source files
1111-1212-### Shebang (Optional)
1313-```mlf
1414-#!/usr/bin/env mlf
1515-```
1616-1717-### File Naming Convention
1818-The file path determines the lexicon NSID. Files should follow the lexicon NSID structure:
1919-- `com.example.forum.thread.mlf` → Lexicon NSID: `com.example.forum.thread`
2020-- `com.example.user.profile.mlf` → Lexicon NSID: `com.example.user.profile`
2121-2222-The lexicon NSID is derived solely from the filename, not from any internal declarations.
2323-2424-## Basic Structure
2525-2626-Every MLF file can contain:
2727-2828-- Use statements (imports)
2929-- Type definitions (record, inline type, def type, token, query, procedure, subscription)
3030-3131-## Syntax Rules
3232-3333-### Semicolons
3434-3535-- **Records** do NOT have semicolons after the closing brace `}`
3636-- All other definitions require semicolons:
3737- - `use` statements end with `;`
3838- - `token` definitions end with `;`
3939- - `inline type` definitions end with `;`
4040- - `def type` definitions end with `;`
4141- - `query` definitions end with `;`
4242- - `procedure` definitions end with `;`
4343- - `subscription` definitions end with `;`
4444-4545-### Commas
4646-4747-Commas are **required** between items, with **trailing commas allowed**:
4848-4949-**Record fields:**
5050-```mlf
5151-record example {
5252- field1: string,
5353- field2: integer, // trailing comma allowed
5454-}
5555-```
5656-5757-**Constraints:**
5858-```mlf
5959-title: string constrained {
6060- maxLength: 200,
6161- minLength: 1, // trailing comma allowed
6262-}
6363-```
6464-6565-**Error definitions:**
6666-```mlf
6767-query getThread(): thread | error {
6868- NotFound,
6969- BadRequest, // trailing comma allowed
7070-}
7171-```
7272-7373-## Primitive Types
7474-7575-- `null` - Null value
7676-- `boolean` - True/false
7777-- `integer` - 64-bit integer
7878-- `number` - Double-precision float
7979-- `string` - UTF-8 string
8080-- `bytes` - Byte array
8181-- `blob` - Binary large object with metadata
8282-- `unknown` - Any value (for forward compatibility)
8383-8484-## Special String Formats
8585-8686-These are defined in the prelude and available everywhere:
8787-8888-- `Did` - Decentralized Identifier (did:*)
8989-- `AtUri` - AT-URI (at://...)
9090-- `AtIdentifier` - Either a DID or Handle
9191-- `Handle` - Handle identifier (domain name)
9292-- `Datetime` - ISO 8601 datetime
9393-- `Uri` - Generic URI
9494-- `Cid` - Content Identifier
9595-- `Nsid` - Namespaced Identifier
9696-- `Tid` - Timestamp Identifier
9797-- `RecordKey` - Record key
9898-- `Language` - BCP 47 language code
9999-100100-## Records
101101-102102-Records define structured data types stored in repositories:
103103-104104-```mlf
105105-/// A forum thread
106106-record thread {
107107- /// Thread title
108108- title: string constrained {
109109- maxLength: 200
110110- minLength: 1
111111- }
112112- /// Thread body
113113- body?: string // Optional field
114114- /// Thread creation timestamp
115115- createdAt: Datetime
116116-}
117117-```
118118-119119-## Type Definitions
120120-121121-MLF supports two kinds of type definitions:
122122-123123-### Inline Types
124124-125125-Expanded at the point of use, never appear in generated lexicon defs:
126126-127127-```mlf
128128-inline type AtIdentifier = string constrained {
129129- format "at-identifier"
130130-};
131131-```
132132-133133-### Def Types
134134-135135-Become named definitions in the lexicon's defs block:
136136-137137-```mlf
138138-def type replyRef = {
139139- root: AtUri
140140- parent: AtUri
141141-};
142142-143143-record thread {
144144- reply?: replyRef
145145-}
146146-```
147147-148148-Use `inline type` for type aliases that should be expanded inline (like primitive type wrappers). Use `def type` for types that should be referenced by name in the generated lexicon.
149149-150150-## Tokens
151151-152152-Tokens are named constants used in enums and unions:
153153-154154-```mlf
155155-/// Open state
156156-token open;
157157-158158-/// Closed state
159159-token closed;
160160-161161-record issue {
162162- state: string constrained {
163163- knownValues: [open, closed]
164164- default: "open"
165165- }
166166-}
167167-```
168168-169169-Tokens must have doc comments describing their purpose.
170170-171171-## Constrained Types
172172-173173-Add validation constraints to types:
174174-175175-```mlf
176176-title: string constrained {
177177- maxLength: 200
178178- minLength: 1
179179-}
180180-181181-age: integer constrained {
182182- minimum: 0
183183- maximum: 150
184184-}
185185-186186-status: string constrained {
187187- enum: ["draft", "published", "archived"]
188188-}
189189-```
190190-191191-### String Constraints
192192-193193-- `maxLength` / `minLength` - Length in bytes
194194-- `maxGraphemes` / `minGraphemes` - Length in grapheme clusters
195195-- `format` - Format validation (datetime, uri, did, handle, etc.)
196196-- `enum` - Allowed values (closed set) - accepts string literals or token references
197197-- `knownValues` - Known values (extensible set) - accepts string literals or token references
198198-- `default` - Default value
199199-200200-**enum, knownValues, and default** can use either literals or references:
201201-```mlf
202202-// String literals
203203-status: string constrained {
204204- knownValues: ["open", "closed", "pending"]
205205- default: "open"
206206-}
207207-208208-// References to named items (tokens, aliases, records, etc.)
209209-token open;
210210-token closed;
211211-212212-status: string constrained {
213213- knownValues: [open, closed] // References tokens defined above
214214- default: open // References the token
215215-}
216216-```
217217-218218-### Integer Constraints
219219-220220-- `minimum` / `maximum` - Min/max values
221221-- `enum` - Allowed values
222222-- `default` - Default value
223223-224224-### Array Constraints
225225-226226-```mlf
227227-tags: string[] constrained {
228228- minLength: 1
229229- maxLength: 10
230230-}
231231-```
232232-233233-### Blob Constraints
234234-235235-```mlf
236236-avatar: blob constrained {
237237- accept: ["image/png", "image/jpeg"]
238238- maxSize: 1000000 // bytes
239239-}
240240-```
241241-242242-### Boolean Constraints
243243-244244-```mlf
245245-field: boolean constrained {
246246- default: false
247247-}
248248-```
249249-250250-### Constraint Refinement
251251-252252-Constraints can only make types **more restrictive**, never less restrictive:
253253-254254-```mlf
255255-def type shortString = string constrained {
256256- maxLength: 100
257257-};
258258-259259-record post {
260260- // Valid: 50 is more restrictive than 100
261261- title: shortString constrained {
262262- maxLength: 50
263263- }
264264-}
265265-```
266266-267267-**Refinement rules:**
268268-- Numeric bounds: `minimum` can only increase, `maximum` can only decrease
269269-- Length bounds: `minLength`/`minGraphemes` can only increase, `maxLength`/`maxGraphemes` can only decrease
270270-- Enums: Can only restrict to a subset
271271-- Format: Cannot change once specified
272272-273273-## Arrays
274274-275275-```mlf
276276-tags: string[]
277277-278278-items: string[] constrained {
279279- minLength: 1,
280280- maxLength: 10,
281281-}
282282-```
283283-284284-## Unions
285285-286286-Use the pipe operator `|`:
287287-288288-```mlf
289289-// Closed union (only these types)
290290-content: text | image | video
291291-292292-// Union of tokens
293293-state: open | closed | pending
294294-```
295295-296296-Open unions (allowing unknown types) use `_`:
297297-298298-```mlf
299299-// Open union (can include unknown types)
300300-content: text | image | _
301301-```
302302-303303-## Objects
304304-305305-Inline object types:
306306-307307-```mlf
308308-metadata: {
309309- version: integer
310310- timestamp: Datetime
311311-}
312312-```
313313-314314-## Queries
315315-316316-Queries are read-only HTTP endpoints (GET):
317317-318318-```mlf
319319-/// Get a user profile
320320-query getProfile(
321321- /// The actor's DID or handle
322322- actor: AtIdentifier
323323-): profile;
324324-```
325325-326326-With errors:
327327-328328-```mlf
329329-query getThread(
330330- uri: AtUri
331331-): thread | error {
332332- /// Thread not found
333333- NotFound
334334- /// Invalid request
335335- BadRequest
336336-};
337337-```
338338-339339-## Procedures
340340-341341-Procedures are write operations (POST):
342342-343343-```mlf
344344-/// Create a new thread
345345-procedure createThread(
346346- title: string
347347- body: string
348348-): {
349349- uri: AtUri
350350- cid: Cid
351351-} | error {
352352- /// Title too long
353353- TitleTooLong
354354-};
355355-```
356356-357357-## Subscriptions
358358-359359-Subscriptions are WebSocket-based event streams:
360360-361361-```mlf
362362-/// Subscribe to repository events
363363-subscription subscribeRepos(
364364- /// Optional cursor for resuming
365365- cursor?: integer
366366-): commit | identity | handle;
367367-```
368368-369369-Message types must be defined as def types or records:
370370-371371-```mlf
372372-/// Commit message
373373-def type commit = {
374374- seq: integer
375375- repo: Did
376376- commit: Cid
377377- time: Datetime
378378-};
379379-380380-/// Identity message
381381-def type identity = {
382382- did: Did
383383- handle: Handle
384384-};
385385-```
386386-387387-## Comments
388388-389389-### Documentation Comments
390390-391391-Use `///` for documentation (appears in generated docs/code):
392392-393393-```mlf
394394-/// A forum thread
395395-record thread {
396396- /// Thread title
397397- title: string
398398-}
399399-```
400400-401401-### Regular Comments
402402-403403-Use `//` for comments that won't appear in output:
404404-405405-```mlf
406406-// This is a regular comment
407407-record example {
408408- field: string // inline comment
409409-}
410410-```
411411-412412-## Annotations
413413-414414-Annotations use `@` and provide metadata for external tooling:
415415-416416-### Simple Annotation
417417-```mlf
418418-@deprecated
419419-record oldRecord {
420420- field: string
421421-}
422422-```
423423-424424-### Positional Arguments
425425-```mlf
426426-@since(1, 2, 0)
427427-@doc("https://example.com/docs")
428428-record example {
429429- field: string
430430-}
431431-```
432432-433433-### Named Arguments
434434-```mlf
435435-@validate(min: 0, max: 100, strict: true)
436436-@table(name: "threads", indexes: "did,createdAt")
437437-record thread {
438438- @indexed
439439- did: Did
440440-441441- @sensitive(pii: true)
442442- title: string
443443-}
444444-```
445445-446446-Annotations can be placed on records, inline types, def types, tokens, queries, procedures, subscriptions, and fields.
447447-448448-## Imports
449449-450450-Import definitions from other lexicons:
451451-452452-```mlf
453453-// Single import
454454-use com.example.user.profile;
455455-456456-// Multiple imports
457457-use com.example.forum.{thread, reply};
458458-459459-// Alias import
460460-use com.example.user as User;
461461-462462-// Wildcard import
463463-use com.example.forum.*;
464464-465465-// Import with alias
466466-use com.example.forum.{thread as ForumThread};
467467-```
468468-469469-After importing, use the short name:
470470-471471-```mlf
472472-use com.example.user.profile;
473473-474474-record thread {
475475- author: profile // Instead of com.example.user.profile
476476-}
477477-```
478478-479479-## References
480480-481481-Reference local or external definitions:
482482-483483-```mlf
484484-// Local reference (same file)
485485-record thread {
486486- author: author // References 'def type author' in same file
487487-}
488488-489489-// Cross-file reference
490490-record thread {
491491- profile: com.example.user.profile // References com/example/user/profile.mlf
492492-}
493493-```
494494-495495-All references use dotted notation.
496496-497497-## Optional Fields
498498-499499-Use `?` to mark fields as optional:
500500-501501-```mlf
502502-record thread {
503503- title: string // Required
504504- body?: string // Optional
505505- tags?: string[] // Optional array
506506-}
507507-```
508508-509509-## Raw Identifiers
510510-511511-Use backticks to escape reserved keywords when you need to use them as identifiers:
512512-513513-```mlf
514514-def type `record` = {
515515- `record`: com.atproto.repo.strongRef
516516- `error`: string
517517-};
518518-```
519519-520520-This is useful when working with existing schemas that use MLF keywords as field or type names.
521521-522522-## Format Strings
523523-524524-Available format strings for constrained strings:
525525-526526-- `datetime` - ISO 8601 datetime
527527-- `uri` - URI (RFC 3986)
528528-- `at-uri` - AT-URI (ATProto)
529529-- `did` - Decentralized Identifier
530530-- `handle` - ATProto handle
531531-- `nsid` - Namespaced Identifier
532532-- `cid` - Content Identifier
533533-- `at-identifier` - DID or handle
534534-- `language` - BCP 47 language tag
535535-- `tid` - Timestamp ID
536536-- `record-key` - Record key
537537-538538-## Complete Example
539539-540540-```mlf
541541-#!/usr/bin/env mlf
542542-543543-use com.example.user.profile;
544544-545545-/// Open state
546546-token open;
547547-548548-/// Closed state
549549-token closed;
550550-551551-/// A forum thread
552552-record thread {
553553- /// Thread title
554554- title: string constrained {
555555- minGraphemes: 1
556556- maxGraphemes: 200
557557- }
558558- /// Thread body (markdown)
559559- body?: string constrained {
560560- maxGraphemes: 10000
561561- }
562562- /// Thread state
563563- state: string constrained {
564564- knownValues: [open, closed]
565565- default: "open"
566566- }
567567- /// Author profile
568568- author: profile
569569- /// Creation timestamp
570570- createdAt: Datetime
571571-}
572572-573573-/// Get a thread by URI
574574-query getThread(
575575- /// Thread AT-URI
576576- uri: AtUri
577577-): thread | error {
578578- /// Thread not found
579579- NotFound
580580-};
581581-582582-/// Create a new thread
583583-procedure createThread(
584584- title: string
585585- body?: string
586586-): {
587587- uri: AtUri
588588- cid: Cid
589589-} | error {
590590- /// Title too long
591591- TitleTooLong
592592-};
593593-```