Flow
Sources: docs-original/GLOSSARY.md, packages/core/docs/SPEC.md, packages/core/docs/FDR.md Status: Core Concept
What is Flow?
Definition: A declarative description of a computation sequence. Flows are evaluated by Core and may produce Patches, Effects, or both.
In simple terms: Flow is "how to respond to an Intent."
Core Principles
1. Flows do NOT execute; they describe.
2. Flows do NOT return values; they modify Snapshot.
3. Flows are NOT Turing-complete; they always terminate.
4. Flows are data, not code.
5. There is no suspended Flow context.Structure
type FlowNode =
// Sequencing
| { kind: 'seq'; steps: readonly FlowNode[] }
// Conditional
| { kind: 'if'; cond: ExprNode; then: FlowNode; else?: FlowNode }
// State mutation
| { kind: 'patch'; op: 'set' | 'unset' | 'merge'; path: SemanticPath; value?: ExprNode }
// Requirement declaration
| { kind: 'effect'; type: string; params: Record<string, ExprNode> }
// Flow composition
| { kind: 'call'; flow: string }
// Termination
| { kind: 'halt'; reason?: string }
// Error
| { kind: 'fail'; code: string; message?: ExprNode };Flow Node Types
seq (Sequential Execution)
Executes steps in order. Each step sees the Snapshot as modified by previous steps.
{
"kind": "seq",
"steps": [
{ "kind": "patch", "op": "set", "path": "a", "value": { "kind": "lit", "value": 1 } },
{ "kind": "patch", "op": "set", "path": "b", "value": { "kind": "get", "path": "a" } }
]
}
// Result: a = 1, b = 1if (Conditional Branching)
Evaluates condition against current Snapshot. Executes then or else branch.
{
"kind": "if",
"cond": { "kind": "get", "path": "user.isAdmin" },
"then": { "kind": "patch", "op": "set", "path": "access", "value": { "kind": "lit", "value": "full" } },
"else": { "kind": "patch", "op": "set", "path": "access", "value": { "kind": "lit", "value": "limited" } }
}patch (State Mutation)
Declares a state change. Three operations:
| Operation | Description | Example |
|---|---|---|
set | Set value at path | { op: "set", path: "count", value: 5 } |
unset | Remove value at path | { op: "unset", path: "temp" } |
merge | Shallow merge object at path | { op: "merge", path: "user", value: {...} } |
effect (Requirement Declaration)
Declares that an external operation is required.
Critical: Effects are NOT executed by Core. They are declarations recorded in Snapshot.
{
"kind": "effect",
"type": "api:createTodo",
"params": {
"title": { "kind": "get", "path": "input.title" }
}
}When Core encounters an effect node:
- Core records a Requirement in
snapshot.system.pendingRequirements - Core terminates the current computation
- Core returns with
status: 'pending'
There is no suspended execution context.
The Host is responsible for:
- Executing the effect (IO, network, etc.)
- Applying result Patches to Snapshot
- Calling
compute()again with the new Snapshot
All continuity is expressed exclusively through Snapshot.
call (Flow Composition)
Invokes another Flow by name. Enables composition.
{
"kind": "call",
"flow": "shared.validateInput"
}Important: call does NOT pass arguments or return values.
- The called Flow reads from the same Snapshot
- The called Flow writes to the same Snapshot
- There is no parameter passing mechanism
If you need to pass context to a called Flow, write it to Snapshot first:
{
"kind": "seq",
"steps": [
{ "kind": "patch", "op": "set", "path": "system.callContext",
"value": { "kind": "get", "path": "input" } },
{ "kind": "call", "flow": "shared.processWithContext" }
]
}halt (Normal Termination)
Stops Flow execution normally. Not an error.
{
"kind": "if",
"cond": { "kind": "get", "path": "alreadyProcessed" },
"then": { "kind": "halt", "reason": "Already processed, skipping" }
}fail (Error Termination)
Stops Flow execution with an error. The error is recorded in Snapshot.
{
"kind": "if",
"cond": { "kind": "not", "arg": { "kind": "get", "path": "computed.isValid" } },
"then": {
"kind": "fail",
"code": "VALIDATION_ERROR",
"message": { "kind": "lit", "value": "Input validation failed" }
}
}Why Flows Are Not Turing-Complete
From FDR-006:
FlowSpec does NOT include unbounded loops (while, for, recursion).
Rationale
By limiting expressiveness:
- Guaranteed Termination: All Flows finish in finite steps
- Static Analysis: Can verify properties without execution
- Complete Traces: Trace is always finite
- Predictable Resources: Bounded memory and time
For unbounded iteration, Host controls the loop:
const context = { now: 0, randomSeed: "seed" };
while (needsMoreWork(snapshot)) {
const result = await core.compute(schema, snapshot, intent, context);
snapshot = result.snapshot;
}Flow Re-Entry Safety
This is critical and often misunderstood.
The Problem
Because there's no resume() API, the same Flow will be evaluated multiple times for a single user action:
- First
compute(): Flow runs until effect, returnspending - Host executes effect, applies patches
- Second
compute(): Flow runs from the beginning
If the Flow is not re-entrant, step 3 will duplicate the work from step 1.
The Solution: State Guards
Every patch and effect MUST be guarded by state conditions.
// WRONG: No guard (runs every compute cycle)
flow.seq(
flow.patch(state.count).set(expr.add(state.count, 1)),
flow.effect('api.submit', {})
)
// RIGHT: State-guarded
flow.onceNull(state.submittedAt, ({ patch, effect }) => {
patch(state.submittedAt).set(expr.input('timestamp'));
patch(state.count).set(expr.add(state.count, 1));
effect('api.submit', {});
});The Pattern:
- Before executing step X, check if X's side effect already happened
- X's effect should change some state that guards X
- Creates a feedback loop that prevents re-execution
Complete Flow Example
{
"kind": "seq",
"steps": [
{
"kind": "if",
"cond": {
"kind": "lte",
"left": { "kind": "len", "arg": { "kind": "get", "path": "input.title" } },
"right": { "kind": "lit", "value": 0 }
},
"then": { "kind": "fail", "code": "EMPTY_TITLE" }
},
{
"kind": "patch",
"op": "set",
"path": "todos",
"value": {
"kind": "concat",
"args": [
{ "kind": "get", "path": "todos" },
[{
"id": { "kind": "get", "path": "input.localId" },
"title": { "kind": "get", "path": "input.title" },
"completed": false,
"syncStatus": "pending"
}]
]
}
},
{
"kind": "effect",
"type": "api:createTodo",
"params": {
"localId": { "kind": "get", "path": "input.localId" },
"title": { "kind": "get", "path": "input.title" }
}
}
]
}Flow in Builder DSL
The Builder package provides a type-safe DSL for defining Flows:
import { defineDomain } from "@manifesto-ai/builder";
const TodoDomain = defineDomain(
TodoStateSchema,
({ state, actions, expr, flow }) => {
const { addTodo } = actions.define({
addTodo: {
input: z.object({
title: z.string().min(1),
localId: z.string()
}),
flow: flow.seq(
// Validation
flow.when(
expr.lte(expr.len(expr.input('title')), 0),
flow.fail('EMPTY_TITLE')
),
// Optimistic update
flow.patch(state.todos).set(
expr.append(
state.todos,
expr.object({
id: expr.input('localId'),
title: expr.input('title'),
completed: expr.lit(false),
syncStatus: expr.lit('pending')
})
)
),
// API call
flow.effect('api:createTodo', {
localId: expr.input('localId'),
title: expr.input('title')
})
)
}
});
return { actions: { addTodo } };
}
);MEL equivalent:
domain TodoDomain {
type TodoItem = {
id: string,
title: string,
completed: boolean,
syncStatus: string
}
state {
todos: Array<TodoItem> = []
}
action addTodo(title: string, localId: string) {
when eq(trim(title), "") {
fail "EMPTY_TITLE"
}
when neq(trim(title), "") {
patch todos = append(todos, {
id: localId,
title: title,
completed: false,
syncStatus: "pending"
})
effect api:createTodo({
localId: localId,
title: title
})
}
}
}Flow Evaluation
Core evaluates Flows step by step:
┌─────────────────────────────────────────┐
│ compute(schema, snapshot, intent, context) │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Look up ActionSpec for intent.type │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Evaluate flow (FlowNode) │
│ - For each step: │
│ - Evaluate expressions │
│ - Generate patches │
│ - Or encounter effect → stop │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Return ComputeResult: │
│ - snapshot' (with patches applied) │
│ - requirements[] (if effect) │
│ - trace (what happened) │
└─────────────────────────────────────────┘Common Misconceptions
Misconception 1: "Flow has state"
Wrong: Flow remembers where it stopped.
Right: Flow is pure computation. It has no memory. State guards create the illusion of progress.
Misconception 2: "Effect returns to Flow"
Wrong: After effect executes, Flow continues with the result.
Right: After effect executes, Flow starts from the beginning and checks Snapshot to see effect result.
Misconception 3: "Flow is like async/await"
Wrong: Flows are like async functions with await for effects.
Right: Flows are declarative data structures. No execution context, no callbacks, no promises.
Requirements
From SPEC.md:
- Flows MUST NOT contain unbounded loops (
while,for, recursion) - All Flow execution MUST terminate in finite steps
callreferences MUST NOT form cycleseffectMUST terminate computation (no continuation in same compute cycle)
Related Concepts
- ActionSpec - Maps Intent to Flow
- ExprNode - Pure expressions used in Flows
- Effect - External operation declared in Flow
- Patch - State mutation produced by Flow
See Also
- Schema Specification - Normative specification including FlowSpec
- Core FDR - Design rationale including Flow termination guarantees
- Re-entry Safe Flows Guide - Practical patterns
- Effect - Understanding effects