Language

Modules

Modules group declarations into bounded contexts, and exports decide what other modules may reference.

A module is a bounded context: a named group of declarations. Every declaration belongs to exactly one module, modules can nest, and the module hierarchy is what the IDE uses to zoom in and out of a model. A module keeps its declarations private unless it lists them in export.

Declaring a module

module sales {
  export Order

  enum OrderStatus {
    draft
    placed
  }

  entity Order {
    reference: String unique
    status: OrderStatus default draft
  }
}

A module contains any mix of declarations from every layer, plus export lines and nested modules. The qualified name of the module is its path: sales here.

Exports

export lists the direct members of the module that other modules may reference. A module can have several export lines; the names add up.

export Order, OrderSummary, OpenOrders
export PlaceOrder
  • Each name must be a declaration directly inside the module. Exporting a name that does not exist is an error: Module sales has no member "Missing" to export.
  • Exporting a declaration also exports everything inside it, by dotted path. Exporting Order makes sales.Order and sales.Order.status visible to other modules, which lets another module define a field of type sales.Order or a machine on sales.Order.status.
  • Exports are explicit. Nothing becomes visible to other modules just because it exists.

References across modules

Another module references an exported declaration by its module path, followed by the dotted path inside the module: crm.Account, crm.Clerk, sales.Order.status.

This works across files too. All files of a workspace load together, so sales.appling can reference crm.Account declared in crm.appling. Two modules may reference each other; only the layer rules apply, not an order between modules.

module crm {
  export Account, Clerk

  role Clerk

  entity Account {
    name: String unique
    last_order: sales.Order?
  }
}

module sales {
  export Order

  entity Order {
    reference: String unique
    customer: crm.Account
    policy read { actor.is(crm.Clerk) }
  }
}
flowchart TB
  subgraph m0["crm"]
    n0["Clerk<br/><small>role</small>"]:::cross
    n1["Account<br/><small>entity</small>"]:::domain
  end
  subgraph m1["sales"]
    n2["Order<br/><small>entity</small>"]:::domain
  end
  n1 -->|"type"| n2
  n2 -->|"type"| n1
  n2 -->|"uses"| n0
  classDef domain fill:#3d2510,stroke:#3d2510,color:#f7f2ea
  classDef behavior fill:#6b3f12,stroke:#6b3f12,color:#f7f2ea
  classDef application fill:#a85f17,stroke:#a85f17,color:#f7f2ea
  classDef interface fill:#d98a1c,stroke:#d98a1c,color:#0c0a08
  classDef experience fill:#f2b632,stroke:#f2b632,color:#0c0a08
  classDef cross fill:#1b1712,stroke:#7a6e5e,color:#f7f2ea
The semantic graph Appling builds from this modelL1 DomainCross cutting

appling graph aggregates these edges into module to module dependencies. For the model above, sales depends on crm (the customer field type and the role in the policy) and crm depends on sales (the last_order field type).

Nested modules

A module can contain modules. The path of a nested module joins the names with dots: module b inside module a is a.b, and its declaration X is a.b.X.

module a {
  export Report

  entity Report {
    title: String
  }

  module b {
    export Draft

    entity Draft {
      report: Report
    }
  }

  entity Archive {
    draft: a.b.Draft
  }
}
  • Inside a nested module, the declarations of the enclosing modules are visible by their plain name (Report above).
  • The enclosing module sees the nested module’s declarations only through its exports, by full path (a.b.Draft). The relative path b.Draft does not resolve.
  • What a nested module exports is visible to every module by its full path, whether or not the parent exports the nested module.
  • When a parent exports a nested module by name (export b), everything inside that nested module becomes visible by path, including declarations the nested module does not export itself.

Visibility rules

The scope provider resolves a reference written in a module like this:

Where the target isHow you write itNeeds an export
Same modulePlain name: OrderNo
Inside a declaration of the same moduleDotted path: Order.statusNo
An enclosing modulePlain name: ReportNo
Another module, any fileModule path: crm.Account, sales.Order.status, a.b.DraftYes

A module cannot reference its own unexported declarations by their full path: inside sales, write Order, not sales.Order. Exported declarations resolve by their full path everywhere, including inside their own module.

Two module blocks with the same name are separate scopes: their declarations do not see each other without an export. Keep each module in one block.

Some positions look up names locally instead of through the module scope:

PositionResolves against
picks of an endpoint, list or detail ({ reference, status })The fields of the projection after as (or of the detail projection)
sort in a queryThe fields of the query’s entity
set in an effectThe fields of the action’s entity, or of the machine’s entity inside a transition
on <Error> do in a use case stepThe errors declared by the called action or external operation
else <Error> after requiresThe errors declared by the same action
policy ... fieldsThe fields of the entity that holds the policy

Names inside expressions are not references. They resolve by their position, as described in expressions. Two of them reach across modules:

  • actor.is(crm.Clerk) looks the role or actor up with the same rules as a reference, so a role from another module must be exported.
  • actor.<field> reads a field declared by any actor in the workspace, and tenant stands for the entity of the single tenant in the workspace. Neither needs an export.