Language

Language overview

Syntax basics, the five layers, the reference rules between them and how every element gets its qualified name.

An Appling model is a set of .appling files. Each file holds one or more module blocks, and each module declares the parts of an application: data, behavior, application logic, interfaces and user experience. This page covers the syntax shared by all declarations and the rules that tie them together. The other language pages describe each layer in detail.

Files and modules

A file contains only module blocks. Everything else lives inside a module.

// Order management.
module sales {
  export Order

  enum OrderStatus {
    draft
    placed
  }

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

All files you pass to appling check (or that the IDE opens) load as one workspace, so modules in different files can reference each other through their exports. See modules.

Inside a module, declarations appear in any order. A reference may point to a declaration that comes later in the file.

Comments

FormMeaning
// textLine comment
/* text */Block comment
/** text */Doc comment: a block comment right before a declaration. The language server shows it on hover, after the kind, qualified name and layer of the element.

appling fmt keeps every comment and only normalizes the whitespace around it.

module sales {
  /** An order placed by a customer. */
  entity Order {
    // Unique per customer, printed on the invoice.
    reference: String unique
  }
}

Names and literals

Identifiers start with a letter or an underscore, followed by letters, digits or underscores: Order, placed_at, _internal.

Many keywords are also valid names, so fields such as state, from or input stay legal:

state, input, value, from, to, on, for, step, list, detail, form, route, run, actions, fields, role, event, action, query, page, timeout, sort, when, create, read, update, delete, per, flow, actor, tenant, set, op, human, system, service, asc, desc, remove, rename, retype, roles, second, minute, hour.

Every other keyword (module, entity, machine, policy, true, null and so on) is reserved.

LiteralFormExample
StringDouble quotes, backslash escapes"Total", "say \"hi\""
NumberDigits with an optional fraction50, 0.5
Booltrue, falsetrue
Nullnullnull
ListSquare brackets[placed, paid]

Numbers have no sign; write -1 as the unary minus applied to 1. See expressions.

Canonical form

Every model has exactly one canonical text form: two space indentation, one declaration per line, clauses such as effect or where on their own line. appling fmt rewrites files into it, and the editor’s format command does the same. Running it twice changes nothing.

Layers

Each declaration belongs to exactly one layer. The keyword decides the layer: an entity is always domain, an action is always behavior, wherever it appears. Nested parts (fields, invariants, transitions, endpoints) belong to the layer of the declaration that contains them.

LayerDescribesKeywordsPage
Cross cuttingStructure and identitymodule, export, role, actor, tenant, migrationmodules, access, migrations
L1 DomainWhat existsenum, value, entity with fields, invariant, policydomain
L2 BehaviorWhat can happenevent, action, machine, jobbehavior
L3 ApplicationHow capabilities are organizedquery, projection, external, usecaseapplication
L4 InterfacesHow programs reach theminterface <protocol> with endpointsinterfaces
L5 ExperienceWhat people seepage with list, detail, form; navigation, flow, messagesexperience

Each concept is defined once, in the layer that owns it, and referenced everywhere else. A rule that must always hold is an invariant on the entity (L1); a rule that must hold for one action to run is a requires precondition on that action (L2).

Reference rules

References follow the layer order. Lower layers never reference higher ones, and interfaces and pages are siblings that never reference each other. The validator rejects every other edge.

flowchart TD
  L5[L5 Experience] --> L3[L3 Application]
  L5 -->|actions| L2[L2 Behavior]
  L5 -->|enums, value types| L1[L1 Domain]
  L4[L4 Interfaces] --> L3
  L4 -->|actions| L2
  L4 -->|enums, value types| L1
  L3 --> L2
  L3 --> L1
  L2 --> L1
  X[Cross cutting] --> L1
FromMay reference
Any layerIts own layer and cross cutting declarations
Cross cuttingL1 (an actor field of an entity type, a tenant on an entity)
L1 DomainNothing above it
L2 BehaviorL1
L3 ApplicationL1, L2
L4 InterfacesL3, L2 actions, L1 enums and value types
L5 ExperienceL3, L2 actions, L1 enums and value types

L4 and L5 never reference an entity directly. They reach entity data only through L3 projections. Names inside expressions are not references in this sense; they resolve by position, as described in expressions.

A rejected reference is reported as <layer> element <Kind> may not reference <layer> element <Kind> "<name>"., with the layers spelled cross, domain, behavior, application, interface and experience. Most reference positions accept only certain kinds of declarations anyway (an endpoint targets a query, an action or a use case), so the grammar already rules out many edges before this check runs.

Qualified names

Every element is identified by its qualified name: the module path followed by the names of the declarations that contain it.

ElementQualified name
Modulesales
Nested modulea.b
Entitysales.Order
Fieldsales.Order.status
Enum valuesales.OrderStatus.draft
Action inputsales.Cancel.reason
Declared errorsales.PlaceOrder.EmptyOrder
External operationsales.Payments.Charge

There are no separate ids. Tools such as the graph, the IDE and the edit operations address elements by these names, and renames are recorded as migration steps so tools can follow an element to its new name.

Content keys of unnamed elements

Some elements have no name. Most examples below come from sales.appling. Unnamed elements get a key derived from their content, written in brackets after their kind.

ElementKeyExample
Invariantinvariant["<label>"]sales.Order.invariant["total never negative"]
Policypolicy[<operations> fields <fields>]sales.Order.policy[read fields total], sales.Order.policy[create,update,delete], sales.PlaceOrder.policy[]
Transitiontransition[<from>-><to>]sales.OrderFlow.transition[draft,placed->cancelled]
Endpointendpoint[<verb> <path>]sales.PublicApi.endpoint[GET /orders]
Set effecteffect[set <field>]sales.PlaceOrder.effect[set placed_at]
Create effecteffect[create <entity>]sales.Reorder.effect[create Order]
Emit effecteffect[emit <event>]sales.OrderFlow.transition[draft->placed].effect[emit OrderPlaced]

When two siblings share a key, an ordinal is appended to the second and later ones: policy[read]#2.

Other unnamed parts, such as preconditions, sort clauses, use case steps, page components or message entries, are not elements of their own. They are properties of the element that contains them.

Language pages

PageCovers
Modulesmodule, nested modules, export, references across modules and files
Accessrole, actor, tenant, policies, actor.is(...), narrowing
Domainenum, value, entity, field types and modifiers, invariant, policy
Behaviorevent, action, machine, job
Applicationquery, projection, external, usecase
Interfacesinterface, endpoints, auth, limits, CORS
Experiencepage, list, detail, form, navigation, flow, messages
ExpressionsThe CEL subset, typing, name resolution, builtins
Migrationsmigration, rename, retype, remove, release blocks
ValidationEvery rule the validator enforces