SID

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 configuration
  • entity User: — entity definition
  • relation UserPosts: — relationship between entities
  • authority Admin: — authority definition
  • contract NameRequired: — contract/invariant
  • operation CreateUser: — operation definition
  • service AppService: — service declaration
  • page / 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 typeDescriptionRustSQLTS
stringUTF-8 textStringTEXT/VARCHAR(N)string
booltrue/falseboolBOOLEANboolean
u8Unsigned 8-bitu8SMALLINTnumber
u16Unsigned 16-bitu16INTEGERnumber
u32Unsigned 32-bitu32INTEGERnumber
u64Unsigned 64-bitu64BIGINTnumber
i32Signed 32-biti32INTEGERnumber
i64Signed 64-biti64BIGINTnumber
f3232-bit floatf32REALnumber
f6464-bit floatf64DOUBLE PRECISIONnumber
decimalFixed-point decimalDecimal(*) DECIMAL(20,8)number
bigintLarge integeri64BIGINTnumber
smallintSmall integeri16SMALLINTnumber
timestampDate and timeDateTime<Utc>TIMESTAMPTZstring
jsonJSON valueValueJSONBobject
uuidUUID v4UuidUUIDstring
blobBinary dataVec<u8>BYTEAstring
vector_f32Float32 vectorVec<f32>VECTOR(N)number[]
enum_valString enumerationStringTEXTstring

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
AnnotationPurpose
@primary_keyPrimary key. Exactly one per entity.
@auto_incrementDatabase auto-assigns sequential values (integer types only).
@requiredCannot be null; Rust type is T instead of Option<T>.
@uniqueValue must be unique across all rows; creates a UNIQUE constraint.
@indexedCreates a database index for faster queries. Alias: @index.
@default:"value"Default when no value provided. @default:"now" for timestamps.
@max_length:NMaximum string length (string type only).
@min_length:NMinimum string length (string type only).
@min:N / @max:NMinimum/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:NVector dimensions (vector_f32 only).
@description:"text"Human-readable description; appears in generated docs.
@authority_required:NMinimum authority level required (4=Founder ... 0=None).
@validate:"expr"Custom validation expression checked before execution.
@dis_generate:trueEnable 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
PropertyPurpose
inputComma-separated "field:type" pairs; becomes the request payload.
outputEntity name or scalar the operation returns. Arrays: "User[]". Scalars: "ok", "string", "bool".
authority_requiredMinimum authority level: 0=None, 1=Observer, 2=Operator, 3=Governor, 4=Founder.
bindingsLoad entities by ID before execution: "alias:Entity@input_field".
guardExpression that must be true or the operation aborts (uses req.field and bindings).
validateExpression validated before execution (uses req.field).
createINSERT: "Entity field=value,...". Values: req.field, now, true/false, "text", 123.
updateUPDATE: first field is WHERE key, rest are SET: "Item id=req.id, name=req.name".
deleteDELETE: "Entity key=req.key" — first field is WHERE key.
callInvoke another operation: "OpName(param=value)".
side_effectsComma-separated event names the generated code emits.
rate_limitRate limit: "N/m" or "N/h".
auditGenerate audit event struct and logging for this operation.
handlerCustom 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"
  • bindings fetches the row first.
  • guard emits a contract assertion that aborts on false.
  • mutate supports = += -= *= /=; entity mutations persist via update_<entity>_where.
  • req.X must 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: true

All 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_statusMeaningAction
0SuccessProceed to build the generated code.
1Parse errorFix the ADL syntax and recompile.
2Validation errorFix the ADL semantics and recompile.
3Constitutional violationFix authority/contracts in ADL and recompile.
4Internal compiler errorReport 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'"
ExpressionResolves to
caller.idThe caller's JWT sub claim — the real identity (e.g. oauth:… or key:…). Alias: caller.sub.
caller.roleThe 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::Decimalf64 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:

  1. 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 with put_file(workspace_id, path="generated/custom/x.rs", content) (quarantined under _external_sandbox/) then promote_workspace_file moves it into the overlay.
  2. 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.
  3. Raw handlers. A handler: rust block on an operation emits custom code with full access to the request, repository functions, and subject / 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).