ADL Manual & Reference
ADL (Architecture Description Language) is the input language for the DIS compiler. Write a .adl file, compile it, and DIS emits a complete application. This is the public manual; your MCP client can also pull it with get_dis_docs(audience="adl_quickref") (syntax) or get_dis_docs(audience="full_manual") (full spec).
The Golden Rule: ADL is the source of truth. Generated code is disposable. When DIS returns an error (dis_status 1-4), fix the ADL file and recompile. Never patch generated code to work around a bad ADL.
1. File Structure
An ADL file contains blocks starting with keywords. Blocks can appear in any order except meta, which must be first.
meta:— file metadata (must be first)topology database/frontend:— infrastructure configurationentity User:— entity definitionrelation UserPosts:— relationship between entitiesauthority Admin:— authority definitioncontract NameRequired:— contract/invariantoperation CreateUser:— operation definitionservice AppService:— service declarationpage / component / workflow:— UI and workflow blocks
2. Meta Block
meta:
version: "2.0"
domain: "myapp"
@description: "What this does"domain becomes your Rust crate name and database name. Must be lowercase letters and underscores.
3. Topology
topology database: { kind: "postgresql", port: 5432 }
topology frontend: { kind: "react-app", port: 3000 }
topology cache: { kind: "redis", port: 6379 }Backends
Emit today: axum (Rust), express (Node/TS), fastapi (Python), binary. Rejected loudly until emitters land: actix, hono, fastify, go-gin.
Frontends
Emit buildable apps: react-app, vue-app, svelte-app, solid-app, leptos-app. Accepted but emission tracked: nextjs-app, drogon-static.
Databases
Emit correct SQL: postgresql, sqlite. Validate but with postgres-style placeholders: clickhouse, mysql, mongodb, redis, none.
4. Entities
entity User:
field id: { type: u64, @primary_key, @auto_increment }
field username: { type: string, @required, @unique, @max_length: 64 }
field email: { type: string, @required, @max_length: 256 }
field age: { type: u32, @min: 0, @max: 150 }
field role: { type: string, @enum: "admin,user,guest", @default: "user" }
field is_active: { type: bool, @default: true }
field metadata: { type: json }
field embedding: { type: vector_f32, @dims: 1024 }
field created_at: { type: timestamp }Every entity MUST have exactly one field with @primary_key. Field and entity names must be valid identifiers.
5. All Field Types
| ADL type | Description | Rust | SQL | TS |
|---|---|---|---|---|
| string | UTF-8 text | String | TEXT/VARCHAR(N) | string |
| bool | true/false | bool | BOOLEAN | boolean |
| u8 | Unsigned 8-bit | u8 | SMALLINT | number |
| u16 | Unsigned 16-bit | u16 | INTEGER | number |
| u32 | Unsigned 32-bit | u32 | INTEGER | number |
| u64 | Unsigned 64-bit | u64 | BIGINT | number |
| i32 | Signed 32-bit | i32 | INTEGER | number |
| i64 | Signed 64-bit | i64 | BIGINT | number |
| f32 | 32-bit float | f32 | REAL | number |
| f64 | 64-bit float | f64 | DOUBLE PRECISION | number |
| decimal | Fixed-point decimal | Decimal(*) | DECIMAL(20,8) | number |
| bigint | Large integer | i64 | BIGINT | number |
| smallint | Small integer | i16 | SMALLINT | number |
| timestamp | Date and time | DateTime<Utc> | TIMESTAMPTZ | string |
| json | JSON value | Value | JSONB | object |
| uuid | UUID v4 | Uuid | UUID | string |
| blob | Binary data | Vec<u8> | BYTEA | string |
| vector_f32 | Float32 vector | Vec<f32> | VECTOR(N) | number[] |
| enum_val | String enumeration | String | TEXT | string |
decimal maps to rust_decimal::Decimal on postgresql/mysql and f64 (column REAL) on sqlite.
6. Field Annotations
Both the brace form and the shorthand form are supported and semantically identical:
entity Item:
field id: { type: u64, @primary_key, @auto_increment } # brace form
field name: string @required @max_length: 255 # shorthand
field done: bool @default: false # shorthand
field note: string # bare shorthand| Annotation | Purpose |
|---|---|
| @primary_key | Primary key. Exactly one per entity. |
| @auto_increment | Database auto-assigns sequential values (integer types only). |
| @required | Cannot be null; Rust type is T instead of Option<T>. |
| @unique | Value must be unique across all rows; creates a UNIQUE constraint. |
| @indexed | Creates a database index for faster queries. Alias: @index. |
| @default:"value" | Default when no value provided. @default:"now" for timestamps. |
| @max_length:N | Maximum string length (string type only). |
| @min_length:N | Minimum string length (string type only). |
| @min:N / @max:N | Minimum/maximum numeric value (numeric types only). |
| @pattern:"regex" | Value must match the regex (string only). Alias: @regex. |
| @enum:"A,B,C" | Value must be one of the listed options. Aliases: @allowed, @one_of. |
| @dims:N | Vector dimensions (vector_f32 only). |
| @description:"text" | Human-readable description; appears in generated docs. |
| @authority_required:N | Minimum authority level required (4=Founder ... 0=None). |
| @validate:"expr" | Custom validation expression checked before execution. |
| @dis_generate:true | Enable DIS code generation for this element. |
| @derived:"expr" | V18.8: computed field — evaluated at read time in the Response; never a column or input. |
| @rollup:"sum(Child.field)" | V18.8: aggregation from a child entity (e.g. Project.spent = sum(Task.actual_hours)); count(Child) also supported. |
| @state_machine:"from:to1,to2|..." | V18.8: legal state transitions; updates to the field reject 409 CONFLICT on illegal moves. |
| @after_create:"Child f=self.x, g=req.y" | V18.8: nested create fired after the row insert (auto-initialization). @after_update / @after_delete reserved. |
7. Operations
Full operation shape:
operation CreateUser:
input: "username:string,email:string,age:u32"
output: "User"
authority_required: 2
bindings: "department:Department@dept_id"
guard: "req.age >= 18"
validate: "req.email.contains('@')"
create: "User username=req.username,email=req.email,age=req.age"
side_effects: "user_created,email_sent"
rate_limit: "100/m"
audit: true| Property | Purpose |
|---|---|
| input | Comma-separated "field:type" pairs; becomes the request payload. |
| output | Entity name or scalar the operation returns. Arrays: "User[]". Scalars: "ok", "string", "bool". |
| authority_required | Minimum authority level: 0=None, 1=Observer, 2=Operator, 3=Governor, 4=Founder. |
| bindings | Load entities by ID before execution: "alias:Entity@input_field". |
| guard | Expression that must be true or the operation aborts (uses req.field and bindings). |
| validate | Expression validated before execution (uses req.field). |
| create | INSERT: "Entity field=value,...". Values: req.field, now, true/false, "text", 123. |
| update | UPDATE: first field is WHERE key, rest are SET: "Item id=req.id, name=req.name". |
| delete | DELETE: "Entity key=req.key" — first field is WHERE key. |
| call | Invoke another operation: "OpName(param=value)". |
| side_effects | Comma-separated event names the generated code emits. |
| rate_limit | Rate limit: "N/m" or "N/h". |
| audit | Generate audit event struct and logging for this operation. |
| handler | Custom code block. Languages: rust, cpp, tsx, bash, terraform, sql, verilog, hdl. |
V18 autonomous operations: name read-style operations List*, Get*, or Find* — DIS synthesizes them automatically. Do not use Read*.
8. Guards & Mutations
Declarative operation logic (DIS 0.5.12+):
operation AdjustBalance:
input: "id:u64, delta:i64"
output: "Account"
bindings: "account:Account@id"
guard: "account.status == 'active'"
mutate: "account.balance += req.delta"bindingsfetches the row first.guardemits a contract assertion that aborts on false.mutatesupports= += -= *= /=; entity mutations persist viaupdate_<entity>_where.req.Xmust be an input field; binding fields use the alias (account.balance).
8b. Business-Process Semantics (V18.8)
ADL now expresses behavior, not just structure. These compile to enforced runtime logic — each verified with cargo check 0 errors on generated projects:
Cross-entity validation — validate:
operation ProcessPayment:
input: "order_id:u64, amount:decimal, method:string"
output: "Payment"
validate: "req.amount == order.total"
create: "Payment order_id=req.order_id, amount=req.amount, method=req.method"Auto-fetches order via the order_id FK and rejects 400 BAD_REQUEST when req.amount != order.total. The invariant is enforced, not decorative.
Derived fields — @derived
entity TimeEntry:
field id: u64 @primary_key @auto_increment
field started_at: timestamp @required
field ended_at: timestamp @required
field duration_minutes: u64 @derived: "(ended_at - started_at).num_minutes()"Computed at read time in the Response DTO — never a DB column, never an input.
Lifecycle triggers — @after_create
entity Product:
field id: u64 @primary_key @auto_increment
field name: string @required
field initial_quantity: u64 @required
@after_create: "Inventory product_id=self.id, quantity_available=req.initial_quantity"Fires a nested create after the row insert (auto-initialization). self.<field> = created row, req.<field> = operation input.
State machines — @state_machine
entity Order:
field id: u64 @primary_key @auto_increment
field status: string @required @default: "pending"
@state_machine: "pending:paid,cancelled|paid:picking|picking:picked|picked:shipped|shipped:delivered|delivered:refunded"Any update to status fetches the current row and rejects 409 CONFLICT on illegal transitions.
Aggregations — @rollup
entity Project:
field id: u64 @primary_key @auto_increment
field name: string @required
field spent: decimal @rollup: "sum(Task.actual_hours)"
field task_count: u64 @rollup: "count(Task)"get_/list_ handlers run SELECT COALESCE(SUM(field),0) / COUNT(*) FROM <child> WHERE <parent>_id = $1 after the fetch.
Event-driven notifications — notify:
operation MarkShipped:
input: "id:u64"
output: "Order"
mutate: "order.status = 'shipped'"
notify: "order.shipped:shipped_template"Emits crate::events::emit(channel, &req) before the operation's return (notify steps are hoisted ahead of early-return planners).
9. Relations
relation UserPosts:
from: "User"
to: "Post"
kind: "HAS_MANY"
foreign_key_field: "owner_id"
cascade_delete: trueAll 29 relation kinds: HAS_MANY, BELONGS_TO, REFERENCES, OWNS, EXTENDS, IMPLEMENTS, PROVIDES, REQUIRES, CONSUMES, EMITS, GOVERNS, CONDITIONS, DEPENDS_ON, VETOES, GUARANTEES, INVARIANT, CONSTRAINT, AUTHORIZES, DELEGATES, ESCALATES, OBSERVES, AUDITS, WITNESSES, REPLICATES, EVALUATES, BINDS_TO, OPERATES_ON, CALLS, ACCESSES.
10. Authorities
authority Admin:
level: Founder
hard_veto: true
scope: "Global"
can_execute: "CreateUser,DeleteUser"
can_govern: "CreateUser"
can_veto: "DeleteUser"
delegates: "Moderator"Levels: Founder (4) full control, Governor (3), Operator (2), Observer (1) read-only, None (0). Delegation chains inherit downward; hard veto stops execution regardless of higher authorities.
11. Contracts
contract NameRequired:
scope: "CreateUser,UpdateUser"
invariant: "req.name.length() > 0"
enforcement: "pre_condition"
on_violation: "abort"
message: "Name cannot be empty"Enforcement kinds: pre_condition, post_condition, invariant_check, transaction_boundary, continuous_monitor, audit_replay. Violation responses: abort, revert, escalate, compensate, alert, log_only.
12. Services
service MyService:
backend: "axum"
database: "postgresql"
entities: "User,Post,Comment"
operations: "CreateUser,CreatePost,DeletePost"
depends_on: "AuthService"
auth: "JWT"
port: 8080
replicas: 1
health_check: "/health"backend: "binary" generates a standalone Rust binary (CLI/agent) instead of an HTTP server — no routes, migrations, or repositories.
13. Workflows, Pages & Components
workflow UserLifecycle:
steps: "CreateUser,VerifyEmail,ActivateUser"
page Dashboard:
route: "/dashboard"
auth_required: 1
handler: tsx
export default function Dashboard() {
return <div>Welcome</div>;
}
component UserCard:
props: "name:string,role:string"
handler: tsx
export function UserCard({name, role}) {
return <div>{name} - {role}</div>;
}14. DIS Return Codes
| dis_status | Meaning | Action |
|---|---|---|
| 0 | Success | Proceed to build the generated code. |
| 1 | Parse error | Fix the ADL syntax and recompile. |
| 2 | Validation error | Fix the ADL semantics and recompile. |
| 3 | Constitutional violation | Fix authority/contracts in ADL and recompile. |
| 4 | Internal compiler error | Report it with report_issue; do not retry. |
15. Caller Context
Operations can reference the authenticated caller in any expression —create, update, mutate, guard, validate, and contracts — with the caller.* shorthand:
operation CreateProject:
input: "title:string"
output: "Project"
create: "Project title=req.title, owner_id=caller.id"
guard: "caller.role == 'member'"| Expression | Resolves to |
|---|---|
| caller.id | The caller's JWT sub claim — the real identity (e.g. oauth:… or key:…). Alias: caller.sub. |
| caller.role | The caller's JWT role claim (e.g. admin, member). |
Operations that reference caller.* require an authenticated request: the generated handler resolves the caller from the Authorization header and unauthenticated callers receive 401. The compiler binds a real Rust local inside execute_<op> — it never emits bare identifiers, so generated code always compiles (fixed in DIS V18.5).
Type adaptation (V18.5.1): caller.* is a string, so assignments and comparisons adapt to the destination. Assigning caller.id to a numeric field emits __caller_id.parse::<i64>().unwrap_or_default() (and f64 / rust_decimal::Decimal — f64 on sqlite — / uuid::Uuid / bool for the matching types; optional fields wrap in Some(...)). Comparing caller.id == req.owner_id or caller.id == 42 parses the caller side to the numeric type, while caller.role == 'member' stays a string comparison.
Multiple guard: / mutate: / call: / notify: lines on one operation are all preserved (V18.5.1) — each becomes its own logic step.
16. Sidecar Code & Custom Overlay
The escape hatch for anything ADL cannot express. Three cooperating mechanisms let you add hand-written code to a generated project and keep it across every regeneration — every agent should know this flow:
- Custom overlay (whole files). Write a file under
generated/custom/in the workspace; DIS copies it over the fresh generated tree after every recompile and never touches it. External MCP clients land files withput_file(workspace_id, path="generated/custom/x.rs", content)(quarantined under_external_sandbox/) thenpromote_workspace_filemoves it into the overlay. - Manual regions. Wrap code inside a generated file with
// BEGIN MANUAL: label…// END MANUAL: label(TSX/JSX:{/* BEGIN MANUAL */}). DIS snapshots these blocks before emission and re-injects them after, so the rest of the file stays in sync with the ADL. - Raw handlers. A
handler: rustblock on an operation emits custom code with full access to the request, repository functions, andsubject/caller_id.
Verify after every promote. dis_status=0 / a successful promote_workspace_file only means the file landed — it does not mean the build error is fixed. Always re-run run_workspace_step(workspace_id, step="cargo_check") immediately after promoting a sidecar patch and confirm the specific error is gone before moving on (regression lesson: iss_5af70e227b2349d299f67ca0ef4c7e6d).