Low code – DSL

What is this DSL?

This DSL (Domain-Specific Language) is designed to model structured data entities and declarative user interfaces for low-code and enterprise applications. It allows developers and non-developers alike to define complex data models and UI forms in a highly readable, versionable, and platform-agnostic way.

It is divided into two tightly integrated parts:

  • Data Model DSL: Defines structured entities, their fields, validation rules, triggers, side effects, guards, and relationships.
  • Form DSL: Describes interactive user interfaces built around those data models, including layout, conditions, actions, styles, and asynchronous behavior.

Together, these DSLs form the foundation of a visual development platform that prioritizes:

  • Clean, declarative syntax
  • High-level reusability through templates
  • Predictable behavior via data-driven logic
  • Strong separation of concerns

Dual DSL Structure

DSL PartPurpose
Data Model DSLDescribes entity structure, validation, and lifecycle logic
Form DSLDefines visual layout, interaction rules, and view behavior

This separation allows each part to evolve independently, while maintaining a synchronized understanding of the data being displayed and manipulated in the UI.

Who is this for?

This DSL is designed for:

  • Low-code platform builders who want predictable, configurable forms and data structures
  • Enterprise developers modeling complex systems like financial services, logistics, or compliance
  • UI authors who want to declaratively control behavior without deep code
  • Design systems that benefit from parameterized, reusable, and conditional layout blocks

When and Why Use It

You should use this DSL when:

  • You need shared models between backend and frontend systems
  • You want UI behavior to match business logic (e.g., show a field if a condition is met)
  • You want to model forms without imperative code
  • You need dynamic layout, templating, validation, or async logic — declaratively

This DSL is especially suited for:

  • Form builders
  • Data management interfaces
  • Admin tools
  • Complex business UIs with conditional workflows

Key Design Principles

  • Readability over terseness: Favor syntax that reads naturally
  • State-driven behavior: UI logic is a function of the current state
  • Composable units: Templating allows UI reuse and scoping
  • Low-code friendly: Designed to be understood without programming knowledge
  • Tooling-aware: Syntax is structured for easy parsing and visualization

Examples at a Glance

A sample Data Model DSL definition:

ETITY, Person, 1.0.1-stable, "Person entity"

PROPERTIES:
  name:
    type: STR
    required: true

  age:
    type: INT
    required: true
    min: 18
    max: 65

  active:
    type: BOOL
    default: true

GUARDS:
  prevent_edit_if_inactive:
    ON UPDATE
    IF active IS FALSE
    THEN BLOCK WITH "Cannot edit inactive person"

And a simple Form DSL for editing it the dataset:

FORM, PersonForm, 1.0.0, "Edit Person Form"

PARAMETERS:
  person: Person

STATE:
  person: @@person

LAYOUT:
  @person.name
  @person.age
  @person.active

VIEW_LOGIC:
  #person.name:
    readonly: @person.active = false

Data Sources DSL:

DATASOURCES:
  people:
    PROVIDER: rest
    CONFIG:
      url: /api/people
    RETURNS: LIST<OBJECT>

  userAvatar:
    PROVIDER: file
    CONFIG:
      path: /assets/user.png
    RETURNS: IMAGE

  companyInfo:
    PROVIDER: local
    CONFIG:
      key: company-data
    RETURNS: OBJECT

  svgIcon:
    PROVIDER: file
    CONFIG:
      path: /icons/logo.svg
    RETURNS: SVG
    
  #simple function example  
  generateUUID:
    PROVIDER: function
    CONFIG:
      handler: generateUUID
    RETURNS: TEXT    
    
  #parameterized example
  userDetails:
    PROVIDER: function
    CONFIG:
      handler: getUserDetails
      ARGS:
        userId: @person.id
    RETURNS: Person <- pointing to the entity name, but in some cases OBJECT is fine.    

This example defines:

  • A person model with fields and a guard condition
  • A UI form with data binding and dynamic behavior

1. Getting Started

This section introduces the foundational syntax and principles used in both the Data Mode DSL and the Form DSL. Whether you’re modeling backend entities or defining dynamic UI behavior, these rules form the bedrock of DSL structure and interpretation.

DSL Syntax Fundamentals

The DSL is declarative, indentation-based, and structured with keywords and sections. It follows a predictable, YAML-like format that emphasizes readability over verbosity.

Each block starts with a keyword (ENTITY, PROPERTIES, LAYOUT, etc.) and is followed by a colon. Properties inside a block are indented, forming a tree-like structure.

ENTITY: MyEntity, 1.0.0, "Description here"

PROPERTIES:
  name:
    type: STR
    required: true

Reserved Symbols and Keywords

SymbolMeaning
@Refers to a STATE variable (usually data model fields)
@@Refers to a PARAMETER passed into the form/template
?Indicates a named condition (e.g. isAdmin?)
#Refers to a UI element by its id
EMPTYDeclares an empty array (used instead of [])
~Instantiates a TEMPLATE block

Indentation, Casing, and Quoting Rules

✅ Indentation

  • Indentation defines nesting
  • Use 2 or 4 spaces consistently, or 1 tab

✅ Case conventions

  • DSL keywords: UPPER_SNAKE_CASE (e.g. VIEW_LOGIC, SIDE_EFFECTS)
  • Field names: snake_case (e.g. created_at)
  • Conditions: camelCase or snake_case are allowed

✅ Quoting

  • All string literals (including IDs, labels, and attributes) must be in quotes
  • References (@person.name, @@canEdit, etc.) are never quoted
id: "fullName"
label: "Save Account"

How to Write and Parse a DSL File

DSL files are written in plain text with the .dsl extension (or .form.dsl, .data.dsl, etc. for clarity). Each file typically represents one entity or form.

Example:

ENTITY: Customer, 1.0.0, "Customer record"

PROPERTIES:
  id:
    type: INT
    auto: true
    primary_key: true

Parsing is straightforward:

  • Tokenize based on : and indentation
  • Interpret each block using a recursive-descent structure
  • Validate against DSL grammar (types, references, nesting, etc.)

Tooling and Usage Scenarios

You can use this DSL with:

  • Custom UI frameworks or low-code platforms
  • Domain modeling tools
  • Form engines or headless backends
  • DSL interpreters and live renderers (coming soon)

Usage Scenarios:

Use CaseDSL Use
Admin panel formsDefine layout, behavior, actions
Backend schema syncingModel shared fields and constraints
Visual form builder exportGenerate DSL from drag-and-drop UI
DSL-to-code generationCreate React/Vue templates, APIs

✅ You now understand:

  • The basic syntax of both DSLs
  • How to reference data and structure layout
  • The role of reserved symbols
  • How DSLs are parsed and used in tools

2. Data Model DSL

The Data Model DSL defines the structure, rules, and lifecycle logic of your application’s data entities. This includes fields (called properties), enums, validations, computed values, and cross-entity side effects.

Everything declared here is used by the Form DSL to build dynamic, predictable, and consistent user interfaces.

2.1 ENTITY

Every model starts with an ENTITY declaration.

ENTITY: Account, 1.0.0, "A financial account"
FieldDescription
NameIdentifier of the entity
VersionSemantic version for tracking changes
LabelOptional human-friendly description

2.2 COLLECTIONS

COLLECTIONS define reusable named value lists:

  • Enums (with labels)
  • Arrays of literals
  • Arrays of objects

Example: Enums and Value Lists

COLLECTIONS:
  roles: ENUM INT
    0 = user
    1 = admin
    2 = superadmin

  currencies: ARRAY<STR> = ["USD", "EUR", "GBP"]

  value_pairs: ARRAY<OBJECT>
    { id: 1, label: "One" }
    { id: 2, label: "Two" }
    
  people: FROM DATASOURCE people

2.3 PROPERTIES

Defines the fields of the entity. Each property has:

FieldDescription
typeSTR, INT, BOOL, EMAIL, DATETIME, DECIMAL, ENUM
requiredField is mandatory
nullableAllows null (if not required)
readonlyCannot be edited
primary_keyMarks the ID field
autoAuto-generated (e.g. ID, timestamps)
defaultDefault value
inMust match values from a COLLECTION
refForeign key to another entity
computedRead-only field using expression (e.g. CONCAT)
min, max, min_length, max_lengthNumeric or string validation

Example

PROPERTIES:
  id:
    type: INT
    auto: true
    primary_key: true
    readonly: true

  name:
    type: STR
    required: true
    max_length: 100

  currency:
    type: STR
    in: currencies

  balance:
    type: DECIMAL
    default: 0.00
    readonly: true

2.4 TRIGGERS

Triggers define automatic actions within the entity when a condition is met.

TRIGGERS:
  on_deactivation:
    IF active IS FALSE
    THEN SET notes TO "Account closed"
  • Triggers run after data changes
  • Can be chained with AND
  • Do not block changes — only modify other fields

2.5 GUARDS

Guards define validation rules that block operations like updates or deletes.

GUARDS:
  prevent_change_if_inactive:
    ON UPDATE
    IF active IS FALSE AND NOT (active CHANGES)
    THEN BLOCK WITH "Inactive accounts cannot be changed"
Guard PhaseDescription
ON UPDATEApplies to modifications
ON DELETEApplies before deletion
ON CREATEOptional (for insert checks)

You can use conditions like:

  • Field comparisons
  • CHANGES, NOT, EXISTS, HAS CHANGES ON
  • Logical chaining with AND/OR

2.6 SIDE_EFFECTS

Side effects define actions that affect other entities.

SIDE_EFFECTS:
  ON DELETE:
    FOR Transaction WHERE account_id == THIS
    SET account_id TO NULL
  • Runs after the main operation
  • Can use THIS to refer to the deleted/updated item
  • Typically used for:
    • Cleanup
    • Cascading nulls
    • Audit log injection

✅ A Full Entity Example

ENTITY: Account, 1.0.0, "Financial account"

COLLECTIONS:
  types: ENUM STR
    savings = "Savings"
    checking = "Checking"
    credit = "Credit"

PROPERTIES:
  id:
    type: INT
    auto: true
    primary_key: true

  name:
    type: STR
    required: true

  type:
    type: ENUM types
    required: true

  balance:
    type: DECIMAL
    readonly: true
    default: 0.00

  active:
    type: BOOL
    default: true

TRIGGERS:
  on_close:
    IF active IS FALSE
    THEN SET name TO CONCAT(name, " [Closed]")

GUARDS:
  block_edits_when_closed:
    ON UPDATE
    IF active IS FALSE AND NOT (active CHANGES)
    THEN BLOCK WITH "Cannot modify closed account"

SIDE_EFFECTS:
  ON DELETE:
    FOR Transaction WHERE account_id == THIS
    SET account_id TO NULL

3. Form DSL

The Form DSL defines declarative, reactive user interfaces built on top of your data model. It describes layout, logic, actions, and interactivity in a way that is:

  • Data-driven
  • Stateless (until rendered)
  • Easy to bind to model fields
  • Ideal for low-code and no-code platforms

3.1 ENTITY

Each UI definition begins with an ENTITY:

ENTITY: AccountForm, 1.0.0, "Edit financial accounts"
FieldDescription
NameIdentifier of the form entity
VersionSemantic version
LabelOptional human-readable name

3.2 PARAMETERS

The PARAMETERS section defines inputs the form receives from its parent or platform. These could be:

  • A model object (Account)
  • A collection (COLLECTION OF)
  • A boolean flag, function, or string

You can also set defaults — EMPTY for collections, or static values.

PARAMETERS:
  account: Account
  transactions: COLLECTION OF Transaction = EMPTY
  canEdit: BOOL = true

These values are referenced in STATE using @@paramName.

3.3 STATE

STATE is the local data context for the form. You define:

  • What model or objects are used
  • Any internal computed or derived fields
STATE:
  account: @@account
  canEdit: @@canEdit
  context: PermissionContext
  people : FROM DATASOURCE people

Use @stateName to reference state in LAYOUT, VIEW_LOGIC, ACTIONS, etc.

3.4 CONDITIONS

Reusable boolean expressions.

CONDITIONS:
  isAdmin: @context.role = "admin"
  canSave: @account.active = true AND HAS CHANGES ON account
  mustFillNotes: @account.type = "savings" AND @account.notes IS EMPTY

Use condition names with ? when referencing them:

#saveBtn:
  hidden: NOT canSave?

3.5 LAYOUT

Defines the structure of the UI. Supports:

  • HORIZONTAL_STACK, HORIZONTAL_GRID, COLUMN, DIV, TEXT, BUTTON
  • Direct binding to @model.field
  • Attributes like id, label, placeholder, width, height
  • Conditionals: IF, ELSE IF, ELSE
  • Loops: FOR @collection AS item:
LAYOUT:
  HORIZONTAL_STACK gap=1:
    @account.name
    @account.type

  IF @account.active:
    BUTTON:
      id: "deactivateBtn"
      label: "Deactivate"

  ELSE:
    TEXT: "Account is closed"

3.6 TEMPLATES

Reusable UI blocks that can include layout, logic, styles, and actions.

TEMPLATES:
  transaction_row:
    PARAMETERS:
      tx: Transaction
      canDelete: BOOL = false

    STATE:
      rowId: CONCAT("tx-", @tx.id)

    LAYOUT:
      DIV:
        id: @rowId
        content: @tx.description

        BUTTON:
          id: "removeBtn"
          label: "Remove"

Templates support:

  • PARAMETERS with default values
  • Local STATE (derived from parameters)
  • Full LAYOUT, STYLE, ACTIONS, VIEW_LOGIC

3.7 SLOTS

Templates can expose named insertion points using SLOT:.

TEMPLATES:
  card_layout:
    PARAMETERS:
      title: STR

    LAYOUT:
      DIV:
        class: "card"
        HEADER: content: @title

        SLOT: content

        SLOT: actions
          WHEN isAdmin?

In the parent:

~card_layout:
  title: "Admin Panel"

  IN SLOT content:
    TEXT: "Welcome, admin."

  IN SLOT actions:
    BUTTON: label: "Settings"

3.8 VIEW_LOGIC

Control attributes of UI elements with reactive logic.

VIEW_LOGIC:
  #saveBtn:
    enabled: canSave?
    hidden: NOT isAdmin?
    tooltip:
      WHEN canSave? THEN: "Click to Save"
      WHEN NOT isAdmin? THEN: "Only admins can save"
      ELSE: "Save disabled"

Supports:

  • enabled, disabled, readonly, hidden, visible, tooltip, required
  • Conditional attributes via WHEN ... THEN ... / ELSE
  • Direct condition references with ?

3.9 STYLE

Define styling dynamically or statically.

STYLE:
  #accountName:
    class:
      WHEN @account.active = false THEN: "text-muted"
      ELSE: "text-bold"

  #card:
    background: VAR(cl-surface)
    color: VAR(cl-foreground)

3.10 ACTIONS

Attach behavior to UI events (e.g., button clicks).

ACTIONS:
  #saveBtn:
    on: click
    call: context.saveAccount
    with:
      id: @account.id
KeyDescription
#idUI element ID
onEvent name (click, change, etc.)
callMethod in the context
withParameters to pass (from state/params)

You can also target wildcard IDs (#remove-*) for templated buttons.

4. Expressions

Expressions are used throughout both DSLs to drive logic, filtering, validation, and conditional behavior. You’ll find them in:

  • CONDITIONS
  • VIEW_LOGIC
  • GUARDS, TRIGGERS
  • SIDE_EFFECTS
  • ACTIONS (parameter mapping)
  • ASYNC blocks

The syntax is designed to be readable, composable, and consistent across the entire platform.

Expression Grammar Overview

<expression> ::= <value>
               | <reference>
               | <comparison>
               | <logic>
               | <function_call>
               | ( <expression> )

<comparison> ::= <left> OP <right>
<logic>      ::= <expression> LOGIC_OP <expression>
<function_call> ::= FUNC_NAME( args... )

Supported Operators

OperatorMeaningExample
=Equality@account.type = "savings"
!=Not equal@user.role != "admin"
>Greater than@account.balance > 0
<Less than@person.age < 65
>=, <=Greater/Less than or equal@score >= 50
ISMatch or compare (semantic alias for =)@active IS TRUE
IS EMPTYField is empty, null, or blank@notes IS EMPTY
HAS CHANGES ONField has been modifiedHAS CHANGES ON account

Logical Operators

OperatorDescriptionExample
ANDBoth sides must be true@active = true AND @age > 18
OREither side must be true@role = "admin" OR @role = "manager"
NOTInverts the conditionNOT @user.active

Combine with parentheses to group logic:
(@role = "admin" OR @role = "manager") AND @active = true

Functions & Helpers

FunctionDescriptionExample
CONCAT(...)Joins values into a stringCONCAT(name, " - ", type)
LENGTH OFReturns length of a list or stringLENGTH OF @tags > 0
EXISTS Entity WHEREChecks if a record existsEXISTS User WHERE email = @input.email
NOWReturns current date/timecreated_at: default: NOW
THISRefers to the current entity (in guards/triggers)manager == THIS

Conditional Syntax

WHEN / THEN / ELSE

Used in VIEW_LOGIC or style declarations:

tooltip:
  WHEN canSave? THEN: "Click to save"
  WHEN NOT canEdit? THEN: "You cannot edit"
  ELSE: "Save is disabled"

IF / ELSE IF / ELSE

Used in LAYOUT, SIDE_EFFECTS, GUARDS, etc.

IF @active:
  TEXT: "Active"

ELSE IF @pending:
  TEXT: "Pending"

ELSE:
  TEXT: "Inactive"

Examples in Context

CONDITIONS:
  canSubmit: @form.status = "draft" AND HAS CHANGES ON form

VIEW_LOGIC:
  #submitBtn:
    enabled: canSubmit?
    tooltip:
      WHEN canSubmit? THEN: "Click to submit"
      ELSE: "No changes or already submitted"

5. Best Practices

Designing scalable, maintainable DSL-based systems means more than just writing valid syntax — it’s about intentional structure, clean logic, and reusability. This section captures best practices gathered from real-world usage.

Model Your Domain First

Before writing the Form DSL, define your data clearly in the Data Model DSL. It’s the source of truth.

  • Ensure properties have clear types and constraints
  • Use COLLECTIONS to centralize repeated lists and enums
  • Define computed fields early (like full_name) so UI can reference them
  • Use GUARDS for enforcing rules and preventing invalid states
  • Use TRIGGERS and SIDE_EFFECTS for cross-field and cross-entity reactions

Name Things Intentionally

Entity/FieldNaming Convention
DSL KeywordsUPPER_SNAKE_CASE
Entity namesPascalCase
Fieldssnake_case
ConditionscamelCase with ? when referenced

Avoid abbreviations unless industry-standard (id, url, sku, etc.).

CONDITIONS:
  canSubmit: @form.status = "draft" AND HAS CHANGES ON form

Prefer Named Conditions

Use the CONDITIONS section to centralize logic instead of repeating raw expressions:

✅ Clear and reusable:

enabled: canSave?

🚫 Hard to debug and maintain:

enabled: @person.active AND HAS CHANGES ON person

Use Templates for Modular Layout

  • Create small, focused templates (button_row, audit_entry, section_card)
  • Pass in only what you need via PARAMETERS
  • Use SLOTS for flexible injection points
  • Keep templates self-contained with their own STATE, VIEW_LOGIC, STYLE, and ACTIONS
~transaction_card:
  tx: tx
  canDelete: true

Use Slots to Avoid Deep Nesting

Slots allow composition without clutter:

TEMPLATES:
  card:
    SLOT: content
    SLOT: footer WHEN isAdmin?

Using Templates + Slots

Define Template

TEMPLATES:
  form_card:
    PARAMETERS:
      title: STR

    LAYOUT:
      DIV:
        class: "card"
        HEADER: content: @title
        SLOT: content

Use Template

~form_card:
  title: "Edit Person"

  IN SLOT content:
    @person.name
    @person.active

Keep Layout and Logic Separate

Avoid mixing too much logic into the layout:

✅ Good:

LAYOUT:
  @account.name
  @account.currency

VIEW_LOGIC:
  #account.name:
    readonly: NOT canEdit?

🚫 Problematic:

LAYOUT:
  @account.name:
    readonly: NOT canEdit?

Keeping logic in VIEW_LOGIC makes the layout easier to read and change.

Chain Conditions Carefully

Use WHEN / THEN / ELSE or IF / ELSE sparingly and intentionally. Keep condition blocks short.

tooltip:
  WHEN isAdmin? THEN: "Click to save"
  ELSE: "You don’t have permission"

Avoid deeply nested IF chains in layout — refactor into a condition or a TEMPLATE instead.

Use Style Conditions for UX Feedback

Make forms feel alive with style-driven feedback:

STYLE:
  #firstName:
    class:
      WHEN @user.active = false THEN: "input-disabled"
      ELSE: "input-default"

Validate with a Test Harness

Set up a minimal parser to:

  • Validate references (@, @@, ?)
  • Ensure types and enums match model
  • Simulate condition evaluation and slot rendering

Tools can help, but a well-structured DSL makes validation easier in the first place.

✅ Final Tips

  • Break large forms into multiple templates
  • Use TEMPLATES + SLOTS as your “components”
  • Avoid using string literals where COLLECTIONS can help
  • Document your DSL as you go — make it explain itself

DSL Architecture Diagram

+---------------------+
|   Data Model DSL    |
+---------------------+
| ENTITY              |
| ├── COLLECTIONS     |
| ├── PROPERTIES      |
| ├── TRIGGERS        |
| ├── GUARDS          |
| └── SIDE_EFFECTS    |
+---------------------+
          |
          v
+---------------------+
|   Form DSL          |
+---------------------+
| ENTITY              |
| ├── PARAMETERS      |   --> External inputs
| ├── STATE           |   --> Internal form state
| ├── CONDITIONS      |   --> Named reusable logic (with `?`)
| ├── LAYOUT          |   --> Form layout and fields
| │   ├── IF/ELSE     |   --> Branching
| │   └── FOR         |   --> Loops
| ├── TEMPLATES       |   --> Reusable scoped components
| │   ├── SLOTS       |   --> Injected layout blocks
| │   └── STATE       |
| ├── VIEW_LOGIC      |   --> Dynamic attributes (readonly, visible...)
| ├── STYLE           |   --> Classes, colors, rules
| ├── ACTIONS         |   --> Events (click, submit...)
+---------------------+

References:
  - @field         → Refers to STATE
  - @@parameter    → Refers to PARAMETER
  - #elementId     → Refers to UI element
  - condition?     → Named boolean logic

6. Datasources

DATASOURCES

The DATASOURCES section defines external or dynamic data that the view can use. A datasource describes where data comes from, how it should be fetched, and what type of result is expected.

All datasources are asynchronous by default and are resolved automatically by the runtime when they are referenced in STATE or VIEW_LOGIC. This allows clean separation between data fetching and view logic while providing predictable behavior across different providers.

Structure

DATASOURCES:
  name:
    PROVIDER: type
    CONFIG:
      key: value
    RETURNS: resultType
FieldPurpose
PROVIDERDefines how the data should be fetched (rest, file, function, etc.)
CONFIGProvider-specific configuration for fetching data
RETURNSDeclares the expected shape of the returned data

Example

DATASOURCES:
  users:
    PROVIDER: rest
    CONFIG:
      url: /api/users
    RETURNS: LIST<OBJECT>

  avatar:
    PROVIDER: file
    CONFIG:
      path: /assets/user.png
    RETURNS: IMAGE

  generateUUID:
    PROVIDER: function
    CONFIG:
      handler: generateUUID
    RETURNS: TEXT

Supported Result Types

TypeDescription
OBJECTA JSON object
LIST<OBJECT>An array of JSON objects
TEXTPlain text
IMAGEAn image file (png, jpg, svg)
SVGAn SVG specifically
FILEAny raw file
ANYNo shape enforcement (use with caution)

Usage Example in STATE

STATE:
  userList: FROM DATASOURCE users
  userAvatar: FROM DATASOURCE avatar
  myId: FROM DATASOURCE generateUUID

7. Operations

The OPERATIONS section defines actions that perform side-effects within the system. These can include typical CRUD operations, calling remote REST APIs, executing local functions, or handling streamed content like AI responses.

Unlike DATASOURCES (used for reading data), OPERATIONS are designed for actions — writing, updating, deleting, or executing remote/local processes.

Structure

EditOPERATIONS:
  name:
    PROVIDER: type
    CONFIG:
      key: value
    RETURNS: resultType
    MODE: fetch | stream
FieldPurpose
PROVIDERExecution strategy (REST, FUNCTION, etc.)
CONFIGProvider-specific configuration (method, url, body, handler, etc.)
RETURNSDeclares the expected return type (optional but recommended)
MODEfetch for a single result (default), stream for multiple/continuous results

MODE Options

ValueDescription
fetchSingle response (one-time result)
streamContinuous or multiple results over time

Example 1 — CRUD using REST (fetch mode)

OPERATIONS:
  createUser:
    PROVIDER: REST
    CONFIG:
      method: POST
      url: /api/users
      body:
        name: @form.name
        email: @form.email
    RETURNS: OBJECT
    MODE: fetch

Example 2 — Ollama AI Chat using REST (stream mode)

OPERATIONS:
  chatWithOllama:
    PROVIDER: REST
    CONFIG:
      method: POST
      url: http://localhost:11434/api/generate
      body:
        model: llama3
        prompt: @chat.prompt
        stream: true
    RETURNS: TEXT
    MODE: stream

Note: In Ollama, stream is a field inside the request body — not a DSL config option. The MODE: stream in the DSL tells the engine to handle the operation as a continuous response.

Example 3 — Execute Local Function (fetch mode)

OPERATIONS:
  calculateDiscount:
    PROVIDER: FUNCTION
    CONFIG:
      handler: calculateDiscountForUser
      ARGS:
        userId: @user.id
        total: @cart.total
    RETURNS: DECIMAL
    MODE: fetch

Example 4 — Execute Local Function (stream mode)

OPERATIONS:
  watchDirectory:
    PROVIDER: FUNCTION
    CONFIG:
      handler: watchDirectory
      ARGS:
        path: /tmp/uploads
    RETURNS: TEXT
    MODE: stream

Usage Example in ACTIONS

ACTIONS:
  onSave:
    CALL: createUser
    ARGS:
      name: @form.name
      email: @form.email

  onSendMessage:
    CALL: chatWithOllama
    ARGS:
      prompt: @chat.message
      
  onCalculate:
    CALL: calculateDiscount
    ARGS:
      userId: @user.id
      total: @cart.total

Note

SectionPurpose
DATASOURCESRead-only data fetching
OPERATIONSActions that change or trigger side effects
PROVIDER: RESTRemote API calls using REST API
PROVIDER: FUNCTIONLocal system logic execution
MODE: fetchSingle result
MODE: streamContinuous / Streaming result

8. MODULES

Modules provide a way to group, organize, and reuse related DSL features within a project. They help structure large projects while remaining lightweight enough for small projects to use without overhead.

Every project starts with a default module called Main. For smaller projects, this may be the only module required. Larger projects can create additional modules to separate concerns, promote reuse, or enable shared feature development.

Purpose of Modules

BenefitDescription
OrganizationKeeps related DSL definitions together
EncapsulationPrevents naming collisions by scoping features per module
ReusabilityModules can be imported, exported, and shared between projects
Separation of ConcernGroup features by domain or functionality
NamespacingAllows safe cross-module references like Accounts.User
Standard StructureEncourages consistent project layout
Version ControlModules can define their own version for dependency management

Module Definition Example (meta)

MODULE: Accounts
DESCRIPTION: "Handles all user and security features"
VERSION: 1.0.0
DEPENDENCIES:
  - Core: ^2.0.0

Default Folders in a Module

When creating a new module, the following folders are created automatically:

FolderPurpose
datasourcesDSL files for external or internal data providers
entitiesDSL files defining data structures
operationsDSL files for side-effect actions and mutations
formsDSL files for UI forms and layouts
templatesOptional reusable UI blocks
collectionsOptional enums or static lists
guardsOptional validation or access logic

Naming Convention for DSL Files

DSL TypeFilename Pattern
EntityUser.entity.dsl
DatasourceUsers.datasources.dsl
OperationCreateUser.operation.dsl
FormUser.form.dsl

Example Folder Structure (Accounts Module)

modules/
  accounts/
    datasources/
      Users.datasources.dsl
    entities/
      User.entity.dsl
    operations/
      CreateUser.operation.dsl
      UpdateUser.operation.dsl
    forms/
      User.form.dsl
      UserList.form.dsl

We are intentionally keeping files separate from entities.

The reason for this is flexibility — a form, for example, might work with multiple entities at the same time. If we were to associate a form directly with a single entity, it raises the question: which entity should be considered the owner of that form?

By keeping them separate, we avoid this problem entirely. Ownership becomes irrelevant. Instead, relationships between forms and entities are made explicit through metadata, allowing you to easily query which forms make use of a specific entity.

Modules can declare dependencies that must exist within the project for the module to function correctly.

While it’s ideal for a module to be completely self-contained with zero dependencies, this isn’t always practical in more complex systems. When dependencies are unavoidable, they should be explicitly defined within the module.

This allows the compiler to validate that all required dependencies are available before the module is activated, ensuring predictable behavior and preventing runtime errors.

Cross-Module Referencing

When referencing a DSL item from another module:

STATE:
  users: FROM DATASOURCE Accounts.Users

Module Import / Export

Modules can be:

  • Exported to be shared across projects
  • Imported into new or existing projects
  • Versioned to allow for safe upgrades and maintenance

Future Ideas

FeaturePurpose
Module RegistryCentral place to publish & discover reusable modules
Configuration OverridesEnvironment or project-specific adjustments per module
Lifecycle HooksonLoad, onUnload logic for initializing data or cleaning up resources, useful for module swapping at runtime

Example Gallery

Here’s a curated set of example scenarios using both the Data Model DSL and Form DSL together — starting simple and scaling up.

Minimal Example: Person

Data Model DSL

ENTITY: Person, 1.0.0, "Basic person record"

PROPERTIES:
  id:
    type: INT
    auto: true
    primary_key: true

  name:
    type: STR
    required: true

  active:
    type: BOOL
    default: true

Form DSL

FORM: PersonForm, 1.0.0, "Edit person"

PARAMETERS:
  person: Person

STATE:
  person: @@person

LAYOUT:
  @person.name
  @person.active

Conditions and View Logic

CONDITIONS:
  canSave: @person.active = true AND HAS CHANGES ON person

VIEW_LOGIC:
  #saveBtn:
    enabled: canSave?
    tooltip:
      WHEN canSave? THEN: "Click to save"
      ELSE: "Cannot save inactive user"

Full Example

# Full DSL example for an account management system
# This example demonstrates a simple account management system with transaction handling.
# The system allows users to view account details, manage transactions, and close accounts.

## DATA MODEL
ENTITY: Account, 1.0.0, "Financial Account"

COLLECTIONS:
  types: ENUM STR
    savings = "Savings"
    checking = "Checking"
    credit = "Credit"

  currencies: ARRAY<STR> = ["USD", "EUR", "GBP"]

PROPERTIES:
  id:
    type        : INT
    required    : true
    auto        : true
    primary_key : true
    readonly    : true

  account_number:
    type        : STR
    required    : true
    unique      : true

  name:
    type        : STR
    required    : true
    max_length  : 100

  type:
    type        : ENUM types
    required    : true

  balance:
    type        : DECIMAL
    default     : 0.00
    readonly    : true

  currency:
    type        : STR
    in          : currencies
    default     : "USD"

  active:
    type        : BOOL
    default     : true

  owner_id:
    type        : INT
    ref         : Person.id
    required    : true

  created_at:
    type        : DATETIME
    default     : NOW
    readonly    : true

TRIGGERS:
  on_deactivate:
    IF active IS FALSE
    THEN SET name TO CONCAT(name, " [Closed]")

GUARDS:
  prevent_update_if_inactive:
    ON UPDATE
    IF active IS FALSE AND NOT (active CHANGES)
    THEN BLOCK WITH "Cannot modify closed accounts"

  prevent_delete_with_balance:
    ON DELETE
    IF balance > 0
    THEN BLOCK WITH "Cannot delete account with non-zero balance"

SIDE_EFFECTS:
  ON DELETE:
    FOR Transaction WHERE account_id == THIS
    SET account_id TO NULL


ENTITY: Transaction, 1.0.0, "Account Transaction"

PROPERTIES:
  id:
    type        : INT
    required    : true
    auto        : true
    primary_key : true

  account_id:
    type        : INT
    ref         : Account.id
    required    : true

  date:
    type        : DATETIME
    required    : true

  description:
    type        : STR
    required    : true

  amount:
    type        : DECIMAL
    required    : true

  type:
    type        : STR
    required    : true
    values      : ["credit", "debit"]

  created_by:
    type        : STR
    required    : true


## FORM DEFINITION
ENTITY: Account, 1.0.0, "Account Form"

PARAMETERS:
  account: Account
  transactions: COLLECTION OF Transaction = EMPTY
  canEdit: BOOL = true
  loadTransactions: FUNC

STATE:
  account: @@account
  transactions: FROM DATASOURCE translations
  context: PermissionContext

CONDITIONS:
  isAdmin: @context.role = "admin"
  isSavings: @account.type = "savings"
  canDelete: @account.balance = 0
  hasTransactions: LENGTH OF @transactions > 0
  isInactive: @account.active = false

TEMPLATES:
  transaction_row:
    PARAMETERS:
      tx: Transaction
      canRemove: BOOL = false

    STATE:
      rowId: CONCAT("tx-", @tx.id)
      buttonId: CONCAT("remove-", @tx.id)

    LAYOUT:
      HORIZONTAL_STACK gap=2:
        DIV: content: @tx.date
        DIV: content: @tx.description
        DIV: content: @tx.amount
        DIV: content: @tx.type
        BUTTON:
          id: @buttonId
          label: "Remove"

    VIEW_LOGIC:
      #@buttonId:
        hidden: NOT canRemove

    ACTIONS:
      #@buttonId:
        on: click
        call: @context.removeTransaction
        with:
          id: @tx.id

  empty_tx_list:
    LAYOUT:
      DIV: content: "No transactions available"

  account_card:
    PARAMETERS:
      title: STR

    LAYOUT:
      DIV:
        class: "card"

        HEADER: content: @title

        SLOT: content

        SLOT: actions
          WHEN canEdit?

LAYOUT:
  ~account_card:
    title: "Account Details"

    IN SLOT content:
      HORIZONTAL_GRID gap=2:
        COLUMN width=50%:
          @account.account_number
          @account.name

        COLUMN width=50%:
          @account.type
          @account.currency

      @account.owner_id
      @account.active

    IN SLOT actions:
      BUTTON:
        id: "closeBtn"
        label: "Close Account"

  IF @account.active:
      FOR @transactions AS tx:
        ~transaction_row:
          tx: tx
          canRemove: isAdmin?

  ELSE IF isInactive?:
    ~empty_tx_list

  ELSE:
    DIV: content: "Account not found or inaccessible."

VIEW_LOGIC:
  #closeBtn:
    HIDDEN: NOT canEdit?
    TOOLTIP:
      WHEN isInactive? THEN: "Account already closed"
      ELSE: "Click to close account"

  @account.name:
    READONLY: NOT canEdit?

STYLE:
  #closeBtn:
    CLASS:
      WHEN isInactive? THEN: "btn-disabled"
      ELSE: "btn-warning"

  @account.name:
    BACKGROUND: VAR(cl-input-bg)

ACTIONS:
  #closeBtn:
    ON: click
    CALL: context.closeAccount
    WITH:
      id: @account.id

© caperaven.co.za aka (Johan Rabie)