# Execution Model (/docs/execution-model)



Flowlib takes a different approach to workflow execution than most automation tools. This page explains how data moves through a flow, how nodes talk to each other, and how iteration works.

## How a flow runs [#how-a-flow-runs]

When you trigger a flow, Flowlib follows a simple sequence:

1. **Load the flow** — fetch the flow definition (nodes + edges) from the database
2. **Sort the nodes** — use the edges to figure out which nodes depend on which, then line them up in order (topological sort)
3. **Execute each node in order** — one at a time, top to bottom
4. **Store the result** — save the full execution trace (every node's input, output, timing, and status)

Execution is **strictly sequential**. Node B won't start until Node A finishes. This keeps things predictable and easy to debug — you can always trace exactly what happened and in what order.

## How edges work [#how-edges-work]

Edges are the connections between nodes on the canvas. They serve two purposes:

1. **Execution order** — edges determine which nodes run before others
2. **Data availability** — a node can only reference data from nodes that are upstream of it (connected via edges)

Unlike some tools where data is automatically pushed forward, Flowlib uses a **pull-based** model. Each node explicitly chooses what upstream data to reference using template expressions. Just because Node A connects to Node B doesn't mean B automatically receives A's output — B's config params must reference A's data with a `{{ }}` expression.

### Direct parents vs. ancestors [#direct-parents-vs-ancestors]

When a node runs, it has access to two categories of upstream data:

* **Direct parents** — nodes with an edge directly into the current node. Their outputs appear as top-level keys.
* **Indirect ancestors** — nodes further back in the chain (grandparent nodes, etc.). Their outputs are grouped under a `previous_nodes` key.

For example, if your flow is `Fetch User → Transform → Send Email`, when "Send Email" runs:

* `transform` is a direct parent (top-level key)
* `fetch_user` is an indirect ancestor (available under `previous_nodes.fetch_user`)

## How data passes between nodes [#how-data-passes-between-nodes]

Every node produces an output when it runs. That output is stored and made available to downstream nodes as part of their **incoming data object** — a JSON object where each key is the upstream node's reference ID.

### Reference IDs [#reference-ids]

Each node has a reference ID derived from its label, normalized to `snake_case`:

* "Fetch User" → `fetch_user`
* "Send Email" → `send_email`
* "API Response" → `api_response`

These reference IDs become the keys in the incoming data object. If two upstream nodes would produce the same key, Flowlib appends numbers to avoid collisions (`some_node`, `some_node1`).

### Example [#example]

Imagine a node called "Send Email" with two upstream nodes:

```json
{
  "fetch_user": { "id": 123, "name": "Alice", "email": "alice@example.com" },
  "generate_subject": "Welcome aboard, Alice!"
}
```

The "Send Email" node can now reference `{{ fetch_user.email }}` or `{{ generate_subject }}` in its config.

## Template expressions [#template-expressions]

Node config fields support JavaScript expressions wrapped in double curly braces: `{{ expression }}`. These are evaluated against the incoming data object before the node runs.

**Simple references:**

* `{{ fetch_user.name }}` → `"Alice"`
* `{{ fetch_user.id }}` → `123`

**Full JavaScript:**

* `{{ users.filter(u => u.active).length }}` → `3`
* `{{ fetch_user.name.toUpperCase() }}` → `"ALICE"`

**Two evaluation modes:**

| Mode            | Example                                   | Returns                                |
| --------------- | ----------------------------------------- | -------------------------------------- |
| Pure expression | A field set to `{{ fetch_user }}`         | The raw value (object, array, number)  |
| Mixed template  | `"Hello {{ fetch_user.name }}, welcome!"` | A string with expressions interpolated |

If the entire field is a single `{{ }}` block, you get back the raw JavaScript value (an object, array, or number). If the field mixes text with expressions, the result is always a string.

Expressions run in a sandboxed JavaScript environment (QuickJS via WebAssembly), so they're safe and isolated. Built-in helpers like `json()`, `first()`, `last()`, `keys()`, `values()`, and `exists()` are available.

## Iteration and the data mapper [#iteration-and-the-data-mapper]

Most automation tools (like n8n) pass arrays of "items" through the entire workflow, processing each item individually at every node. Flowlib works differently.

In Flowlib, **nodes process single values by default**. If a node returns an array, the downstream node receives that array as one value. There's no automatic item-by-item processing.

When you **do** need iteration, you enable the **data mapper** on a specific node. The mapper is a per-node configuration that says: "take this array, and run this node once for each item."

### How the mapper works [#how-the-mapper-works]

Any node can have a mapper configuration with:

* **Expression** — a JavaScript expression that resolves to the array you want to iterate over (e.g., `{{ fetch_users }}`)
* **Mode** — how to handle the result:
  * `auto` (default) — if the expression returns an array, iterate; otherwise run once
  * `iterate` — always iterate (fails if expression doesn't return an array)
  * `reshape` — always run once, even if the expression returns an array
* **Concurrency** — how many iterations to run in parallel (default: 1, sequential)
* **Output mode** — how to package the results (`array`, `object`, `flatten`, `concat`)

### Iteration context [#iteration-context]

During each iteration, the node receives:

* All upstream data (same as usual)
* The current item's properties (spread into the context)
* An `_item` metadata object with `index`, `total`, `first`, `last`, and the raw `value`

So if you're iterating over a list of users, each iteration can reference `{{ name }}` and `{{ email }}` directly, plus `{{ _item.index }}` for the position.

### Concurrency [#concurrency]

With `concurrency: 1` (default), items process one at a time. Set it higher to process items in parallel batches — `concurrency: 5` runs 5 items simultaneously before moving to the next batch.

## How Flowlib differs from n8n [#how-flowlib-differs-from-n8n]

If you're coming from n8n, the biggest conceptual difference is how data flows between nodes.

### n8n: items flow through the whole pipeline [#n8n-items-flow-through-the-whole-pipeline]

In n8n, every node receives an array of **items**. Each item is a JSON object, and most nodes automatically process every item in the array. The items array is the central data structure that flows through the entire workflow.

* If a node produces 10 items, the next node runs 10 times automatically
* To stop automatic iteration, you need a specific "loop" or "batch" node
* All nodes in the chain see and operate on the same items array

### Flowlib: nodes produce values, downstream nodes reference them [#flowlib-nodes-produce-values-downstream-nodes-reference-them]

In Flowlib, each node produces a **single output value** (which can be an object, array, string, or number). Downstream nodes explicitly choose what to reference using template expressions.

* A node that returns an array of 10 users passes that array as **one value**
* The next node decides what to do with it — use the whole array, pick one field, or enable the mapper to iterate
* Iteration is opt-in, per-node, not a workflow-wide behavior

### Key differences at a glance [#key-differences-at-a-glance]

|                        | n8n                                         | Flowlib                                              |
| ---------------------- | ------------------------------------------- | ---------------------------------------------------- |
| **Core data model**    | Array of items flows through every node     | Each node produces a single output value             |
| **Iteration**          | Automatic — every node processes every item | Opt-in — enable the mapper on specific nodes         |
| **Data referencing**   | Implicit — items array is always available  | Explicit — use `{{ node_name.field }}` templates     |
| **Loop control**       | Dedicated loop node wraps a sub-workflow    | Mapper config on any node (expression + concurrency) |
| **Parallelism**        | Mainly at workflow level                    | Per-node — mapper supports concurrent iteration      |
| **Scope of iteration** | Affects the entire downstream chain         | Affects only the node with the mapper enabled        |

The Flowlib approach gives you more control. You decide exactly which node iterates, how many items run in parallel, and how results are packaged. The tradeoff is that you need to be more explicit about data references — but this also makes flows easier to reason about since there's no hidden implicit data passing.

## Branching [#branching]

### If/Else [#ifelse]

The If/Else node evaluates a condition and routes execution to one of two branches (true or false). Nodes on the inactive branch are automatically marked as **skipped**.

The If/Else node is a **passthrough** — its output equals its input. It doesn't transform data, it just decides which path to take.

### Switch [#switch]

The Switch node extends this to multiple branches. You define cases with JavaScript expressions, and execution routes to the first matching case (or all matching cases, depending on the match mode). A default branch catches anything that doesn't match.

### How skipping works [#how-skipping-works]

When a branching node runs, Flowlib looks at which output branches are active and which are inactive. Nodes connected to inactive branches are marked as skipped — but only if **all** of their incoming edges come from skipped or inactive branches. If a node has another active path leading into it, it still runs.

## Agent nodes [#agent-nodes]

Agent nodes work differently from standard nodes. Instead of running once and producing an output, they run an iterative loop:

1. Send a prompt to an LLM (OpenAI, Anthropic, etc.) along with available tool definitions
2. If the LLM requests tool calls, execute those tools and feed the results back
3. Repeat until the LLM responds with text, calls a stop tool, or hits the iteration limit
4. The agent's final response becomes the node's output

## State and execution traces [#state-and-execution-traces]

Every flow run produces a complete execution trace. Each node's record includes its input data, resolved config params, output, status (completed/failed/skipped), any error messages, and timing. This trace is persisted to the database and visible in the UI, making it straightforward to debug issues or understand what happened during a run.
