Docs/Guides/Data flow: params, vars & context
Plans7 min read

Data flow: params, vars & context

Because each session runs in its own isolated LLM context, the interesting part of writing a plan is moving data around — getting a user's input into a session, carrying a fetch result forward, and handing one session's output to the next.

FML gives you exactly three namespaces to do this, available everywhere in templates and expressions: params, vars, and context. This guide explains what each holds, when to reach for it, and how data flows from the plan's inputs to its final output.

Prerequisites

The three namespaces

NamespaceHoldsWritten byRead as (template / Expr)
paramsUser-supplied inputsparameter(...) declarations{{ .params.x }} / params.x
varsGlobals and PreCall resultsset and call(...) -> var{{ .vars.x }} / vars.x
contextCompleted session outputssessions finishing (keyed by name or target){{ .context.s }} / context.s

Think of it as inputs → working memory → results. params come in at the start, vars are the scratch space you populate as the plan runs, and context accumulates the finished output of each session.

params — inputs from the caller

Parameters are declared at the root of the plan and supplied by whoever runs it (via the API, SDK, or CLI):

FML
parameter("topic",       type=string)
parameter("max_results", type=int, default=5)   # default makes it optional

Once declared, a parameter is readable in every session:

FML
+ Search for recent information about {{ .params.topic }}.
- Summarize the top {{ .params.max_results }} findings.

Parameters are read-only. They're the stable inputs the whole plan is written against.

vars — globals and fetched data

vars is your working memory. Two things land here:

Globals set with set — plan-wide or session-scoped constants:

FML
set defaultRegion = "eu-west"     # root level: available everywhere

session("lookup") {
    set threshold = 10            # session level: merged with globals
    - Return records above the {{ .vars.threshold }} threshold in {{ .vars.defaultRegion }}.
    schema { records: string[] }
}

PreCall results routed to a var — a deterministic fetch, saved for later:

FML
call("list_tickets") -> openTickets {
    status = "open"
}

After that call, vars.openTickets holds the fetched data. You can inject it into a prompt ({{ json .vars.openTickets }}), pass it to another tool as a typed value ($(vars.openTickets)), or gate a session on it (expect="vars.openTickets != null"). See PreCalls deep-dive for routing details.

context — outputs from other sessions

When a session finishes, its typed output is stored under its name in context. A session named overview with this schema:

FML
session("overview") {
    - Summarize the topic.
    schema {
        summary:   string
        keyPoints: string[]
    }
}

...publishes context.overview.summary and context.overview.keyPoints. Any later session can read them.

But reading context does not happen automatically — a fresh session doesn't see prior output until you either transfer it in or reference it in a directive. There are two ways to bring it into a session:

1. The context directive injects prior output into the session's LLM context so the model can read it:

FML
session("report", after="overview") {
    context "Overview so far:\n{{ .context.overview | json }}"
    - Write a one-paragraph executive summary from the overview.
    schema { summary: string }
}

Use context true to dump the entire accumulated context as JSON, or a template string (as above) to include just what's relevant. Either form requires after so the source session has already run.

2. Expressions in session arguments reference context to sequence or gate work — expect="len(context.overview.keyPoints) > 0", iterate="context.overview.keyPoints". These also require after. See Control the flow of a plan.

The golden rule for context: if you read context.* anywhere in a session — in a context directive, expect, or iterate — that session must declare after on the session that produced it. Otherwise the value may not exist yet.

How it flows end to end

Here's the full loop — a parameter comes in, a PreCall stores data in a var, one session produces output into context, and a second session reads all three:

FML
parameter("topic", type=string)
set tone = "concise"

session("gather") {
    use search

    call("searchDocuments") -> rawDocs {
        query = "{{ .params.topic }}"
    }

    + Search for {{ .params.topic }} using the results already fetched.
    - Extract the key findings.
    schema { findings: string[] }
}

session("write", after="gather") {
    context "Findings:\n{{ json .context.gather.findings }}"
    - Write a {{ .vars.tone }} brief on {{ .params.topic }} from the findings above.
    schema { brief: string }
}
  • params.topic — the caller's input, read in both sessions.
  • vars.rawDocs / vars.tone — fetched data and a global constant.
  • context.gather.findings — the first session's output, transferred into the second (with after set).

Common mistakes

MistakeFix
Reading context.other without after="other"Always pair a context read with after.
Expecting a session to "just know" earlier outputNothing crosses sessions implicitly — transfer it with a context directive.
Passing an array to a tool as "{{ .vars.list }}"Templates render to strings; use $(vars.list) to preserve the type — see Templates and expressions.
Trying to reassign a paramParameters are read-only inputs; use vars for working state.

Next steps

← All guides

How data moves through a plan across the three namespaces — caller inputs, working memory, and session outputs — and the rule for reading context safely.