# Control the flow of a blueprint

A blueprint's sessions don't just run top to bottom. You **describe** the dependencies and conditions between them, and Frags resolves the order from those declarations — running independent sessions concurrently, gating others on a condition, and fanning a session out across a list.

This guide covers the four session arguments that shape that flow: `after`, `target`, `expect`, and `iterate`.

> Remember the golden rule: FML is declarative. You don't write the control flow imperatively — you annotate each session with its dependencies, and the runtime runs them in the right order for you.

## Prerequisites

- The [hello-world blueprint](/content/docs/guides/hello-world-plan/index.html) and [Call MCP servers and databases](/content/docs/guides/call-tools-from-a-plan/index.html) guides — you should know sessions, schemas, and how a session reads another's output via `{{ .context.<session>.<field> }}`.

## Order with `after`

By default, sessions are independent, and the runtime may run them **concurrently**. Add `after="<session>"` to force one session to wait for another to finish:

```FMLCopy
session("fetch") {
    - Return the raw records to analyze.
    schema {
        records: string[]
    }
}

session("analyze", after="fetch") {
    context "Records:\n{{ json .context.fetch.records }}"
    - Summarize the records into a short report.
    schema {
        summary: string
    }
}
```

To depend on **several** sessions, repeat the argument — `after="a", after="b"`. This is how you fan several parallel sessions back into one:

```FMLCopy
session("report",
        after="analyze_sales",
        after="analyze_dev") {
    - Combine the sales and dev analyses into a single executive summary.
    schema {
        summary: string
    }
}
```

Here `analyze_sales` and `analyze_dev` run in parallel, and `report` waits for both.

### There is no `before` — invert with `after`

FML has no `before` argument, and it doesn't need one. "Run A **before** B" is the same statement as "run B **after** A" — you just declare it from the other side. Only ever annotate the session that has to **wait**.

So to guarantee `cleanup` finishes _before_`report` starts, you don't touch `cleanup` — you make `report` depend on it:

```FMLCopy
session("cleanup") {
    - Normalize and de-duplicate the raw records.
    schema {
        records: string[]
    }
}

session("report", after="cleanup") {
    context "Cleaned records:\n{{ json .context.cleanup.records }}"
    - Write the summary from the cleaned records.
    schema {
        summary: string
    }
}
```

The session that runs first stays dependency-free; the one that follows names it. Anything with no `after` is fair game to run concurrently.

## Rename the output key with `target`

Every session publishes its result under its own name — both in the blueprint's final output and in the `context` namespace. `target="key"` renames it, which lets you keep machine-friendly session names while exposing a clean key:

```FMLCopy
session("extract-sales-signals", target="salesSignals") {
    - Extract the weekly sales signals.
    schema {
        wins: string[]
    }
}
```

Other sessions now reference this output as `context.salesSignals` (not `context.extract-sales-signals`), and the final object uses `salesSignals` as the key.

## Skip a session with `expect`

`expect="<expression>"` is a Go [Expr](https://github.com/expr-lang/expr) condition evaluated at run time. If it's false, the session is **skipped entirely** — no LLM call, no output. It's the clean way to guard against empty or missing data.

```FMLCopy
session("summarize",
        after="fetch",
        expect="len(context.fetch.records) > 0") {
    - Summarize the records.
    schema {
        summary: string
    }
}
```

Common conditions are `len(context.x) > 0`, `context.x.field != null`, and `params.flag`. One rule: **if `expect` reads `context.*`, you must also set `after`** on the session that produced it, so the value exists when the condition is checked.

### Run only when the right data is present

Because `expect` is a full Expr expression, it branches on **what kind of data** showed up, not just how much. You can check that a field exists and is non-null, holds a particular value, or is a non-empty collection — and skip the session when it isn't:

```FMLCopy
# Only summarize if the fetch actually returned a transcript
session("summarize",
        after="fetch",
        expect="context.fetch.transcript != null") {
    - Summarize the transcript.
    schema {
        summary: string
    }
}

# Only escalate when the deal was scored as enterprise
session("escalate",
        after="score",
        expect="context.score.tier == 'enterprise'") {
    - Draft an escalation note for the account team.
    schema {
        note: string
    }
}
```

When the condition is false the session is skipped cleanly — no LLM call, and its key never appears in the output. A downstream session depending on it via `after` sees a null value, and can guard against the skip with its own `expect`.

### Gate on a var, not just a session output

`expect` sees all three scopes — `params`, `context`, and `vars` — so you can require that data exists before the session runs, no matter where it came from. `vars` is especially useful here: it's where a **PreCall** fetch lands. A `call(...) -> someVar` runs _before_ the prompts and saves its result into a var, so `expect` is the natural guard for "only run if that fetch actually returned something."

In this blueprint an upstream session's PreCall pulls open tickets into `vars.openTickets`, and the triage session only runs when that var exists:

```FMLCopy
session("load_tickets") {
    # PreCall fetches data and routes it into vars.openTickets
    call("list_tickets") -> openTickets {
        status = "open"
    }
    - Confirm the tickets were loaded.
    schema {
        loaded: bool
    }
}

# Only triage if the fetch returned tickets
session("triage",
        after="load_tickets",
        expect="vars.openTickets != null") {
    context "Open tickets:\n{{ json .vars.openTickets }}"
    - Group the open tickets by theme and flag the urgent ones.
    schema {
        themes: string[]
    }
}
```

A globally `set` var works the same way — `expect="vars.featureFlag"` gates a session on a blueprint-level flag with no upstream session at all.

The ordering rule mirrors the `context` one: **if a PreCall populates the var, pair it with `after`** on that session so the fetch runs before the condition is checked. A top-level `set` var needs no `after` — it exists from the start.

## Fan out with `iterate`

`iterate="<expression>"` points at an **array** and runs the session **once per element**. Inside the session, `{{ .it }}` (in templates) or `it` (in Expr) is the current element. Because it produces one result per item, the session's `schema` **must** be an array.

```FMLCopy
session("read_transcript",
        after="fetch_meetings",
        expect="len(context.fetch_meetings.meetings) > 0",
        iterate="context.fetch_meetings.meetings") {

- Extract the key points from the meeting titled "{{ .it.title }}".

schema {
        meeting: string
        key_points: string[]
    }[]
}
```

Frags runs the session for each meeting and collects the results into an array — focusing the model on one item at a time, which is far more accurate than asking it to process the whole list in a single call.

## Putting it together

A common shape is **fetch → skip if empty → fan out per item → fan in to a report**:

```FMLCopy
require mcp Avoma

session("fetch_meetings") {
    use mcp Avoma
    + List this week's meetings.
    - Return the meetings found.
    schema {
        meetings: {
            uuid: string
            title: string
        }[]
    }
}

# Runs once per meeting — but only if there are any
session("analyze",
        after="fetch_meetings",
        expect="len(context.fetch_meetings.meetings) > 0",
        iterate="context.fetch_meetings.meetings") {

- Summarize the meeting titled "{{ .it.title }}" in two sentences.

schema {
        title: string
        summary: string
    }[]
}

# Fans back in once every meeting has been analyzed
session("report", after="analyze") {
    context "Per-meeting summaries:\n{{ json .context.analyze }}"
    - Write a one-paragraph digest across all the meetings.
    schema {
        digest: string
    }
}
```

Frags reads these annotations and runs the sessions in dependency order — no orchestration code required.

## Next steps

- Read the Frags reference on [Flow Control](https://github.com/FragsHQ/frags/wiki/FlowControl) and [Sessions](https://github.com/FragsHQ/frags/wiki/Sessions).
- Study full multi-session blueprints in the [blueprint marketplace](/content/blueprints/index.html).
- Run a finished blueprint over HTTP with the [API reference](/content/docs/api/index.html), or call it from your app with the [Python or TypeScript SDK](/content/docs/sdk/index.html).

Order, gate, and fan out sessions with after, target, expect, and iterate — the declarative flow controls Frags runs for you.
