Skip to main content

WSL Specification (native .wsl syntax)

This document defines the concrete syntax, grammar, CST (Concrete Syntax Tree), AST (Abstract Syntax Tree), and validation rules for the Kuetix Workflow Specific Language (WSL) as used by the engine.

WSL is the canonical authoring format for workflows (replacing prior JSON examples). The engine already supports loading .wsl files.


1. Language overview

A WSL file consists of optional headers (module, import, extends, const) followed by one or more workflow declarations. Each workflow contains state blocks connected by transitions (on success -> ..., on error -> ...) and ends (end ok or end error).

Key elements:

  • module declares a namespaced module name.
  • import brings other modules/namespaces into scope.
  • extends indicates inheritance of resolvers/configs.
  • const { ... } declares key/value constants accessible as $constants.*.
  • workflow <Name> { ... } declares a workflow with a start: <State> and one or more state blocks.
  • state <Name>(<Params>?) { action <ActionPath>(<args>) [as <Alias>]; on success|error -> <NextState>(<Args>?); end ok|error }.
  • Expressions support strings, numbers, booleans, references ($a.b.c), template strings ("<<expr>>"), object literals, and built-in filters via |Type.

2. Examples

Hello world (from runtime/workflows/wsl_hello_world/example.wsl):

module example

import services/common

const {
event: "greet",
description: "A simple hello world workflow example",
version: "1.0.0"
}

workflow wsl_hello_world {
start: Hello

state Hello {
action converse/speak.Say(on: message, v: {response: $constants.event, statusCode: 202}) as ResponseAsValue
on success -> Response(ResponseAsValue)
}

state Response(ResponseAsValue) {
action services/common/response.ResponseValue(message: $ResponseAsValue.message, statusCode: $ResponseAsValue.message.statusCode|int)
end ok
}
}

Integer assertion test pattern (from tests/.../assert_eq_test.wsl):

module tests.example

extends "resolvers"

workflow wsl_example_assert_int_test {
start: Integer

state Integer {
action assert.Integer(value: "<<b|int>>", eq: "<<a|int>>", op: "eq")
on success -> FinishTrue
on error -> FinishFalse
}

state FinishTrue {
action response/True(statusCode: 200)
on success -> LastResponse
on error -> LastResponse
}

state FinishFalse {
action response/False(statusCode: 400)
on success -> LastResponse
on error -> LastResponse
}

state LastResponse {
action response/ResponseLastSuccess()
end ok
}
}

Converted login flow (illustrative):

module auth.login
import services/common

workflow user_login {
start: Request

state Request {
action parameters/Request(type: "login") as Req
on success -> Lookup(Req)
}

state Lookup(Req) {
action workflow/repositorium/user/get(id: "<<helpers.Id(Req.Email)>>") as User
on success -> CheckPassword(Req, User)
on error -> Fail
}

state CheckPassword(Req, User) {
action login/CheckPassword(request: Req, user: User)
on success -> EncryptedId(User)
on error -> Invalid
}

state EncryptedId(User) {
action login/EncryptedId(user: User) as Enc
on success -> GenerateToken(User, Enc)
}

state GenerateToken(User, Enc) {
action login/GenerateToken(user: User) as Token
on success -> Persist(User)
on error -> TokenGenerationFailed
}

state Persist(User) {
action workflow/repositorium/user/set(id: $User.ID, entity: "user")
on success -> Respond(User, Token)
on error -> Respond(User, Token)
}

state Respond(User, Token) {
action response/ResponseEntity(entityName: "user", loginResponse: Token)
end ok
}

state Invalid {
action response/ResponseError(message: "Invalid credentials", code: 401)
end error
}

state TokenGenerationFailed {
action response/ResponseError(message: "Failed to generate access or refresh token", code: 500)
end error
}

state Fail {
action response/ResponseError(message: "Lookup failed", code: 404)
end error
}
}

3. Grammar (EBNF-like)

Document := HeaderDecl* WorkflowDecl+
HeaderDecl := ModuleDecl | ImportDecl | ExtendsDecl | ConstDecl

ModuleDecl := "module" Identifier ("." Identifier)*
ImportDecl := "import" Path
ExtendsDecl := "extends" String
ConstDecl := "const" ObjectLiteral

WorkflowDecl := (WorkflowType | "workflow") Identifier "{" WS* StartDecl StateDecl+ "}"
WorkflowType := Identifier // e.g., "feature", "solution", "project", "application", etc.
StartDecl := "start:" WS* Identifier ("(" ArgRef ("," ArgRef)* ")")?
StateDecl := "state" Identifier ("(" ArgRef ("," ArgRef)* ")")? WS* "{" WS* StateBody WS* "}"
StateBody := ActionStmt WS* (OnClause WS*)* (EndStmt)?

ActionStmt := "action" WS* ActionPath "(" ArgList? ")" (WS* "as" WS* Identifier)?
OnClause := "on" WS* Outcome (WS* "when" WS* Expr)? WS* "->" WS* NextTarget
Outcome := "success" | "error"
NextTarget := (Identifier | "_") ("(" ArgRef ("," ArgRef)* ")")?
EndStmt := "end" WS+ ("ok" | "error")

ArgList := NamedArg ("," NamedArg)*
NamedArg := Identifier ":" WS* Expr
ArgRef := Identifier | DollarRef | TemplateExpr

Expr := String
| Number
| Boolean
| DollarRef
| TemplateExpr
| ObjectLiteral
| ArrayLiteral
| Path
| Expr Cast
Cast := "|" TypeName

**Note on Filters:** The `Cast` production in the grammar represents built-in filters that transform or convert values. These filters (e.g., `|int`, `|string`, `|bool`) are applied at runtime to modify values during property resolution.

DollarRef := "$" (Identifier ("." Identifier)* | SpecialDollarVar)
SpecialDollarVar := DollarSymbol+
DollarSymbol := "!" | "@" | "#" | "%" | "^" | "&" | "*" | "_" | "-" | "+" | "=" | "~" | "?"
TemplateExpr := '"' "<<" TemplateBody ">>" '"'
TemplateBody := TemplateAtom (TemplateOp TemplateAtom)* (Cast)?
TemplateAtom := Identifier ("." Identifier)* | Number | String
TemplateOp := ("+" | "-" | "*" | "/" | "|" Identifier)

ActionPath := Path
Path := Identifier ("/" Identifier)* ("." Identifier)?
TypeName := Identifier ("." Identifier)*

ObjectLiteral := "{" WS* (ObjPair ("," ObjPair)*)? WS* "}"
ObjPair := Identifier ":" WS* Expr
ArrayLiteral := "[" WS* (Expr ("," Expr)*)? WS* "]"

ArrayLiteral := "[" WS* (Expr ("," Expr)*)? WS* "]"

Identifier := [A-Za-z_][A-Za-z0-9_]*
String := '"' (\\'"' | ~'"')* '"'
Number := [0-9]+ ('.' [0-9]+)?
Boolean := "true" | "false"

WS := (Space | Newline | Comment)+
Space := ' ' | '\t'
Newline := '\n' | '\r' | '\r\n'
Comment := '//' [^\n]* | '/*' .*? '*/'

4. CST (Concrete Syntax Tree)

CST nodes mirror grammar rules and preserve tokens/trivia for tooling:

Document { headers: HeaderDecl[], workflows: WorkflowDecl[] }
ModuleDecl { name: Identifier[] }
ImportDecl { path: Path }
ExtendsDecl { path: String }
ConstDecl { object: ObjectLiteral }
WorkflowDecl { type: WorkflowType, name: Identifier, start: StartDecl, states: StateDecl[] }
WorkflowType { token: "workflow" | "feature" | "solution" | Identifier }
StartDecl { name: Identifier, args: ArgRef[] }
StateDecl { name: Identifier, params: ArgRef[], body: StateBody }
StateBody { action: ActionStmt, on: OnClause[], end?: EndStmt }
ActionStmt { path: ActionPath, args: NamedArg[], alias?: Identifier }
OnClause { outcome: 'success'|'error', target: NextTarget }
NextTarget { name: Identifier, args: ArgRef[] }
EndStmt { status: 'ok'|'error' }
NamedArg { name: Identifier, value: Expr }
Expr union: StringLiteral | NumberLiteral | BooleanLiteral | DollarRef | TemplateExpr | ObjectLiteral | ArrayLiteral | PathExpr | CastExpr
TemplateSeq { atoms: TemplateAtom[], ops: TemplateOp[], cast?: TypeName }

All nodes include loc (source spans) and optional trivia.


5. AST (Abstract Syntax Tree) and validation

Semantic model for execution:

Module { name: string, imports: Import[], extends?: string, consts: map<string, Value>, workflows: Workflow[] }
Workflow { name: string, type: string, start: string, states: map<string, State> }
State { name: string, params: string[], action: ActionCall, onSuccess?: Target, onError?: Target, terminal?: 'ok'|'error' }
ActionCall { path: ActionPath, args: map<string, Value>, alias?: string }
Target { state: string, args: Value[] }
ActionPath { segments: string[], method?: string }
Value union: String | Number | Boolean | Ref(path[]) | Template(parts[], cast?) | Casted(value, type) | Object(fields) | Array(elements[])

Desugaring rules:

  • Normalize ActionPath into {segments, method}.
  • Convert named args to a map; detect duplicates.
  • Resolve $a.b.c references into Ref([a,b,c]).
  • Carry as <Alias> into subsequent state calls as positional args.
  • Flatten const block into Module.consts.

Validation invariants:

  • Workflow has a single start state and that state exists.
  • State names are unique.
  • Transitions point to existing states; argument arity matches target parameters.
  • end terminates a state and must be last in its block.

6. Workflow Types (workflow, feature, solution)

Overview

WSL supports multiple workflow types beyond the standard workflow keyword. This allows for more semantic and descriptive definitions that match your application architecture.

Supported Types:

  • workflow - Standard workflow (single process flow)
  • feature - Feature-level flow that can orchestrate multiple workflows
  • solution - Solution-level flow that can orchestrate features and workflows
  • Custom types - Any identifier can be used as a workflow type

Evolution

The workflow type system has evolved over time to support more sophisticated orchestration patterns:

  1. Initial Implementation - Only workflow keyword was supported for defining workflows
  2. Custom Type Tokens - Engine was updated to accept any identifier as a workflow type, allowing custom semantic types
  3. Standardization - Standard types were refined: project was renamed to feature, and application was renamed to solution for clearer semantics
  4. Separate Runners - Dedicated execution runners were implemented for each type with shared context support, enabling proper orchestration

Syntax

workflow my_workflow {
start: Initial
state Initial {
action some/action.Do()
end ok
}
}

feature my_feature {
start: RunWorkflow
state RunWorkflow {
action workflow simple_workflow // Executes a workflow
on success -> Complete
}
state Complete {
end ok
}
}

solution my_solution {
start: RunFeature
state RunFeature {
action feature my_feature // Executes a feature
on success -> RunWorkflow
}
state RunWorkflow {
action workflow standalone_workflow
on success -> Complete
}
state Complete {
end ok
}
}

Type Hierarchy

solution
├── feature
│ ├── workflow
│ └── workflow
└── workflow
  • Solutions can orchestrate both features and workflows
  • Features can orchestrate multiple workflows
  • Workflows execute individual process flows
  • All levels can share context through WorkerSessionContext

Path Syntax for Nested Execution

You can reference specific flows using path syntax:

feature orchestrator {
start: RunSpecific

state RunSpecific {
// Run specific workflow from a WSL file
action workflow namespace/path.workflow_name
on success -> Complete
}

state Complete {
end ok
}
}

Accessing Workflow Type in Code

flow := engine.GetFlow()
workflowType := flow.Type // Returns "workflow", "feature", "solution", etc.

switch workflowType {
case "feature":
// Handle feature-specific logic
case "solution":
// Handle solution-specific logic
case "workflow":
// Handle standard workflow logic
}

Custom Types

Any identifier can be used as a workflow type:

microservice auth_service {
start: Authenticate
state Authenticate {
action auth/verify.CheckCredentials()
on success -> Authorized
on error -> Unauthorized
}
state Authorized {
end ok
}
state Unauthorized {
end error
}
}

Custom types are treated like workflows but can be processed differently based on the type field.


7. Nested Arrays and Objects

Overview

WSL supports nested arrays and objects in constants and action arguments, enabling complex data structures to be defined inline.

Evolution

WSL was enhanced to support complex nested data structures:

  • Arrays: Full support for nested arrays with mixed types
  • Objects: Nested object literals with arbitrary depth
  • Parsing: Recursive parsing for complex structures

Array Syntax

Simple Arrays

const {
colors: ["red", "green", "blue"],
numbers: [1, 2, 3, 4, 5],
flags: [true, false, true]
}

Nested Arrays

const {
matrix: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
mixed: [
["a", "b"],
[1, 2],
[true, false]
]
}

Arrays with Objects

const {
users: [
{name: "Alice", age: 30, active: true},
{name: "Bob", age: 25, active: false},
{name: "Charlie", age: 35, active: true}
]
}

Object Syntax

Simple Objects

const {
config: {
host: "localhost",
port: 8080,
ssl: true
}
}

Nested Objects

const {
database: {
connection: {
host: "db.example.com",
port: 5432,
credentials: {
username: "admin",
password: "secret"
}
},
pool: {
min: 5,
max: 20
}
}
}

Objects with Arrays

const {
server: {
name: "api-server",
endpoints: ["/api/v1", "/api/v2", "/health"],
ports: [8080, 8443],
config: {
timeout: 30,
retries: 3,
headers: ["Content-Type", "Authorization"]
}
}
}

Complex Nested Structures

const {
application: {
name: "MyApp",
version: "1.0.0",
modules: [
{
name: "auth",
enabled: true,
config: {
providers: ["local", "oauth"],
tokens: {
access: {ttl: 3600},
refresh: {ttl: 86400}
}
}
},
{
name: "api",
enabled: true,
routes: [
{path: "/users", methods: ["GET", "POST"]},
{path: "/posts", methods: ["GET", "POST", "PUT", "DELETE"]}
]
}
]
}
}

Using Nested Data in Actions

Access nested data using dot notation:

workflow process_data {
start: ProcessUsers

state ProcessUsers {
action data/process.HandleUsers(
users: $constants.users,
config: $constants.database.connection,
endpoints: $constants.server.endpoints
)
end ok
}
}

Array Indexing

workflow access_array {
start: GetFirst

state GetFirst {
action data/get.Value(
color: $constants.colors.0, // Access first element: "red"
user: $constants.users.0.name, // Access nested: "Alice"
cell: $constants.matrix.1.2 // Access 2D array: 6
)
end ok
}
}

Type Support in Nested Structures

All WSL value types are supported in nested structures:

  • Strings: "text"
  • Numbers: 42, 3.14
  • Booleans: true, false
  • References: $variable
  • Objects: {key: value}
  • Arrays: [element1, element2]

Parsing Details

The parser handles:

  • Recursive descent: Arrays and objects can be nested to arbitrary depth
  • Mixed types: Arrays can contain mixed types (strings, numbers, objects, arrays)
  • Whitespace: Flexible whitespace handling
  • Trailing commas: Supported in arrays and objects
  • Empty structures: Empty arrays [] and objects {} are valid

8. Implicit Transition Target (_)

The underscore _ can be used as a transition target in on success, on error, or on else branches. It resolves to the next state in lexical order within the workflow.

When _ appears in the on success branch of the last state in a workflow, it implicitly marks that state as a terminal success (end ok) instead of referencing a following state.

workflow sequential {
start: A

state A {
action step/a.Run()
on success -> _ // resolves to B
}

state B {
action step/b.Run()
on success -> _ // last state: implicit "end ok"
}
}

Rules:

  • _ in a non-trailing state resolves to the next state's name.
  • _ in the on success branch of the last state sets end ok.
  • _ in a non-success branch (on error, on else) of the last state is an error.
  • _ works in both WSL, SWSL, and JSON transition formats.

9. Special $ Variables

WSL supports symbol-rich $-prefixed variables beyond standard identifiers. When a variable starts with $, its name may contain special characters:

! @ # % ^ & * _ - + = ~ ?

This enables built-in placeholders:

VariableDescription
$@All arguments / full context
$?Last execution result / status
$^Parent context / caller reference

These variables can be used anywhere a $-reference is valid — in action arguments, template expressions, and transition conditions.


10. Migration (JSON → .wsl)

See docs/MIGRATION_WSL_JSON_TO_WSL.md for field-by-field mapping and examples.


11. Implementation notes

  • The engine recognizes .wsl files and converts them to an internal schema (WSLLoadToSchema).
  • Literal parsing helpers support object/primitive args; templates "<<...>>" and built-in filters (e.g., |int, |string) are already used in fixtures.
  • A future parser can implement CST→AST→Schema faithfully to this spec; until then, keep examples within the supported subset used in the repository.