Skip to content

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.

mel
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
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 ​

mel
// 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 ​

mel
// RIGHT: State-guarded
action submit(timestamp: number) {
  when submittedAt == null {
    patch submittedAt = timestamp
    effect api.submit({})
  }
}

Unsafe Flow (WRONG) ​

mel
// WRONG: Runs every compute cycle - infinite loop!
action submit() {
  patch count = count + 1
  effect api.submit({})
}

Conditional Branching ​

mel
action checkAccess() {
  when user.isAdmin {
    patch access = "full"
  }
  when !user.isAdmin {
    patch access = "limited"
  }
}

Validation ​

mel
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:

mel
action createUser(email: string) {
  when trim(email) == "" {
    fail "MISSING_EMAIL"
  }

  onceIntent {
    effect api.createUser({ email: email })
  }
}

Status-Based Guards ​

mel
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:

json
{
  "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 ​

typescript
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 ​

NodePurposeMEL Syntax
seqExecute steps in orderStatements in order
ifConditional branchingwhen condition { ... }
patchState mutation (set, unset, merge)patch field = value
effectDeclare external operationeffect type({ params })
callInvoke another flow(Internal)
haltNormal terminationstop
failError terminationfail "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 complete

See Also ​