Flow
Declarative computation expressed as data structures.
What is Flow?
In MEL, Flow is the body of an action: the patch, when, fail, and effect statements that describe the next Snapshot transition.
action addTodo(title: string) {
onceIntent {
patch todos = append(todos, {
id: $runtime.random.uuid,
title: trim(title),
completed: false
})
}
}Underneath that MEL source, Flow is a data structure that Core interprets. App code normally stays at the MEL and app.action.<name>.submit(...) level.
Flows are not Turing-complete. They always terminate in finite steps. This guarantees predictable resource usage and enables static analysis. For unbounded iteration, Host controls the loop.
The key insight: there is no suspended Flow context. When Flow declares an Effect, computation stops. Host executes the effect, applies patches, then calls compute() again. The Flow runs from the beginning, checking Snapshot to see what is already done.
Key Properties
- Data, not code: Flows are JSON-serializable structures.
- Always terminate: No unbounded loops or recursion.
- Re-entry safe: Must work correctly when run multiple times.
- Pure: Same Snapshot + Intent + Context + Flow always produces same result.
Example
MEL (Recommended)
domain TodoDomain {
type Todo = {
id: string,
title: string,
completed: boolean,
syncStatus: "pending" | "synced"
}
state {
todos: Array<Todo> = []
}
action addTodo(title: string)
dispatchable when trim(title) != "" {
onceIntent {
// Optimistic update
patch todos = append(todos, {
id: $runtime.random.uuid,
title: trim(title),
completed: false,
syncStatus: "pending"
})
// API call
effect api.createTodo({ title: title })
}
}
}Common MEL Patterns
Re-entry Safe Flow with onceIntent
// RIGHT: Uses onceIntent for automatic re-entry safety
action submit() {
onceIntent {
patch count = count + 1
effect api.submit({})
}
}Re-entry Safe Flow with Manual Guard
// RIGHT: State-guarded
action submit(timestamp: number) {
when submittedAt == null {
patch submittedAt = timestamp
effect api.submit({})
}
}Unsafe Flow (WRONG)
// WRONG: Runs every compute cycle - infinite loop!
action submit() {
patch count = count + 1
effect api.submit({})
}Conditional Branching
action checkAccess() {
when user.isAdmin {
patch access = "full"
}
when !user.isAdmin {
patch access = "limited"
}
}Validation
action createUser(email: string)
dispatchable when trim(email) != "" {
onceIntent {
effect api.createUser({ email: email })
}
}For lower-level Flow examples you may still see explicit fail statements:
action createUser(email: string) {
when trim(email) == "" {
fail "MISSING_EMAIL"
}
onceIntent {
effect api.createUser({ email: email })
}
}Status-Based Guards
action load() {
when status == "idle" {
patch status = "loading"
effect api.load({})
}
}For Tool Authors And Deep Debugging
You can skip the rest of this page while learning app development. It is useful when building tools, inspecting compiler output, or debugging Core-level behavior.
Compiled JSON
MEL compiles to JSON Flow structures that Core interprets:
{
"kind": "seq",
"steps": [
{
"kind": "if",
"cond": { "kind": "eq", "left": { "kind": "len", "arg": { "kind": "input", "path": "title" } }, "right": 0 },
"then": { "kind": "fail", "code": "EMPTY_TITLE" }
},
{
"kind": "if",
"cond": { "kind": "causalGuard", "key": "addTodo_0" },
"then": {
"kind": "seq",
"steps": [
{
"kind": "patch",
"op": "set",
"path": "todos",
"value": { "kind": "append", "arr": { "kind": "get", "path": "todos" }, "item": "..." }
},
{
"kind": "effect",
"type": "api.createTodo",
"params": { "title": { "kind": "input", "path": "title" } }
}
]
}
}
]
}FlowNode Structure
type FlowNode =
| { kind: 'seq'; steps: readonly FlowNode[] }
| { kind: 'if'; cond: ExprNode; then: FlowNode; else?: FlowNode }
| { kind: 'patch'; op: 'set' | 'unset' | 'merge'; path: string; value?: ExprNode }
| { kind: 'effect'; type: string; params: Record<string, ExprNode> }
| { kind: 'call'; flow: string }
| { kind: 'halt'; reason?: string }
| { kind: 'fail'; code: string; message?: ExprNode };Flow Node Types
| Node | Purpose | MEL Syntax |
|---|---|---|
seq | Execute steps in order | Statements in order |
if | Conditional branching | when condition { ... } |
patch | State mutation (set, unset, merge) | patch field = value |
effect | Declare external operation | effect type({ params }) |
call | Invoke another flow | (Internal) |
halt | Normal termination | stop |
fail | Error termination | fail "CODE" |
How Core Interprets Flow
Intent arrives
↓
Core.compute(schema, snapshot, intent, context)
↓
Flow evaluation begins
↓
┌─────────────────────────────────────┐
│ For each FlowNode: │
│ - seq: process steps in order │
│ - if: evaluate condition, branch │
│ - patch: collect patch operation │
│ - effect: add to requirements │
│ - fail: set error, halt │
└─────────────────────────────────────┘
↓
Returns: { snapshot', patches, requirements, trace }
↓
If requirements exist:
Host executes effects
Host applies patches
Host calls compute() again
↓
If no requirements:
Flow completeSee Also
- Effect - External operations declared in Flow
- Snapshot - State that Flow reads and modifies
- Intent - What triggers Flow execution
- Re-entry Safe Flows - Guard patterns
- MEL Syntax - Complete language reference