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 Part | Purpose |
---|---|
Data Model DSL | Describes entity structure, validation, and lifecycle logic |
Form DSL | Defines 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
Symbol | Meaning |
---|---|
@ | 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 |
EMPTY | Declares 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
orsnake_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 Case | DSL Use |
---|---|
Admin panel forms | Define layout, behavior, actions |
Backend schema syncing | Model shared fields and constraints |
Visual form builder export | Generate DSL from drag-and-drop UI |
DSL-to-code generation | Create 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"
Field | Description |
---|---|
Name | Identifier of the entity |
Version | Semantic version for tracking changes |
Label | Optional 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:
Field | Description |
---|---|
type | STR, INT, BOOL, EMAIL, DATETIME, DECIMAL, ENUM |
required | Field is mandatory |
nullable | Allows null (if not required) |
readonly | Cannot be edited |
primary_key | Marks the ID field |
auto | Auto-generated (e.g. ID, timestamps) |
default | Default value |
in | Must match values from a COLLECTION |
ref | Foreign key to another entity |
computed | Read-only field using expression (e.g. CONCAT) |
min , max , min_length , max_length | Numeric 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 Phase | Description |
---|---|
ON UPDATE | Applies to modifications |
ON DELETE | Applies before deletion |
ON CREATE | Optional (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"
Field | Description |
---|---|
Name | Identifier of the form entity |
Version | Semantic version |
Label | Optional 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 inLAYOUT
,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
Key | Description |
---|---|
#id | UI element ID |
on | Event name (click, change, etc.) |
call | Method in the context |
with | Parameters 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
Operator | Meaning | Example |
---|---|---|
= | 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 |
IS | Match or compare (semantic alias for = ) | @active IS TRUE |
IS EMPTY | Field is empty, null, or blank | @notes IS EMPTY |
HAS CHANGES ON | Field has been modified | HAS CHANGES ON account |
Logical Operators
Operator | Description | Example |
---|---|---|
AND | Both sides must be true | @active = true AND @age > 18 |
OR | Either side must be true | @role = "admin" OR @role = "manager" |
NOT | Inverts the condition | NOT @user.active |
Combine with parentheses to group logic:
(@role = "admin" OR @role = "manager") AND @active = true
Functions & Helpers
Function | Description | Example |
---|---|---|
CONCAT(...) | Joins values into a string | CONCAT(name, " - ", type) |
LENGTH OF | Returns length of a list or string | LENGTH OF @tags > 0 |
EXISTS Entity WHERE | Checks if a record exists | EXISTS User WHERE email = @input.email |
NOW | Returns current date/time | created_at: default: NOW |
THIS | Refers 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
andSIDE_EFFECTS
for cross-field and cross-entity reactions
Name Things Intentionally
Entity/Field | Naming Convention |
---|---|
DSL Keywords | UPPER_SNAKE_CASE |
Entity names | PascalCase |
Fields | snake_case |
Conditions | camelCase 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
, andACTIONS
~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
Field | Purpose |
---|---|
PROVIDER | Defines how the data should be fetched (rest, file, function, etc.) |
CONFIG | Provider-specific configuration for fetching data |
RETURNS | Declares 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
Type | Description |
---|---|
OBJECT | A JSON object |
LIST<OBJECT> | An array of JSON objects |
TEXT | Plain text |
IMAGE | An image file (png, jpg, svg) |
SVG | An SVG specifically |
FILE | Any raw file |
ANY | No 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
Field | Purpose |
---|---|
PROVIDER | Execution strategy (REST, FUNCTION, etc.) |
CONFIG | Provider-specific configuration (method, url, body, handler, etc.) |
RETURNS | Declares the expected return type (optional but recommended) |
MODE | fetch for a single result (default), stream for multiple/continuous results |
MODE Options
Value | Description |
---|---|
fetch | Single response (one-time result) |
stream | Continuous 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 requestbody
— not a DSL config option. TheMODE: 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
Section | Purpose |
---|---|
DATASOURCES | Read-only data fetching |
OPERATIONS | Actions that change or trigger side effects |
PROVIDER: REST | Remote API calls using REST API |
PROVIDER: FUNCTION | Local system logic execution |
MODE: fetch | Single result |
MODE: stream | Continuous / 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
Benefit | Description |
---|---|
Organization | Keeps related DSL definitions together |
Encapsulation | Prevents naming collisions by scoping features per module |
Reusability | Modules can be imported, exported, and shared between projects |
Separation of Concern | Group features by domain or functionality |
Namespacing | Allows safe cross-module references like Accounts.User |
Standard Structure | Encourages consistent project layout |
Version Control | Modules 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:
Folder | Purpose |
---|---|
datasources | DSL files for external or internal data providers |
entities | DSL files defining data structures |
operations | DSL files for side-effect actions and mutations |
forms | DSL files for UI forms and layouts |
templates | Optional reusable UI blocks |
collections | Optional enums or static lists |
guards | Optional validation or access logic |
Naming Convention for DSL Files
DSL Type | Filename Pattern |
---|---|
Entity | User.entity.dsl |
Datasource | Users.datasources.dsl |
Operation | CreateUser.operation.dsl |
Form | User.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
Feature | Purpose |
---|---|
Module Registry | Central place to publish & discover reusable modules |
Configuration Overrides | Environment or project-specific adjustments per module |
Lifecycle Hooks | onLoad , 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)