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:
moduledeclares a namespaced module name.importbrings other modules/namespaces into scope.extendsindicates inheritance of resolvers/configs.const { ... }declares key/value constants accessible as$constants.*.workflow <Name> { ... }declares a workflow with astart: <State>and one or morestateblocks.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
ActionPathinto{segments, method}. - Convert named args to a map; detect duplicates.
- Resolve
$a.b.creferences intoRef([a,b,c]). - Carry
as <Alias>into subsequent state calls as positional args. - Flatten
constblock intoModule.consts.
Validation invariants:
- Workflow has a single
startstate and that state exists. - State names are unique.
- Transitions point to existing states; argument arity matches target parameters.
endterminates 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 workflowssolution- 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:
- Initial Implementation - Only
workflowkeyword was supported for defining workflows - Custom Type Tokens - Engine was updated to accept any identifier as a workflow type, allowing custom semantic types
- Standardization - Standard types were refined:
projectwas renamed tofeature, andapplicationwas renamed tosolutionfor clearer semantics - 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 theon successbranch of the last state setsend 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:
| Variable | Description |
|---|---|
$@ | 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
.wslfiles 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.