Data flow: params, vars & context | Diaphora Docs | Diaphora

Data flow: params, vars & context

Because each session runs in its own isolated LLM context, the interesting part of writing a blueprint 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 blueprint's inputs to its final output.

Prerequisites

The three namespaces

Namespace Holds Written by Read as (template / Expr)
params User-supplied inputs parameter(...) declarations {{ .params.x }} / params.x
vars Globals and PreCall results set and call(...) -> var {{ .vars.x }} / vars.x
context Completed session outputs sessions 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 blueprint runs, and context accumulates the finished output of each session.

params — inputs from the caller

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

parameter("topic",       type=string)
parameter("max_results", type=int, default=5)   # default makes it optional
Argument Required Value Notes
type yes string / int / bool Bare keyword, no quotes
title no quoted string Human-readable label
default no literal value Makes the parameter optional at runtime

Once declared, a parameter is readable in every session:

+ 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 blueprint is written against.

vars — globals and fetched data

vars is your working memory. Two things land here:

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

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:

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:

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:

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 blueprint.

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:

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 }
}

Common mistakes

Mistake Fix
Reading context.other without after="other" Always pair a context read with after.
Expecting a session to "just know" earlier output Nothing 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 param Parameters are read-only inputs; use vars for working state.

Next steps

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