Docs

Data & Placeholders

The two data stores a flow works with — the dictionary and the session — and the ${…} placeholder model that reads them into a node's fields.

The Two Stores a Run Remembers

Almost everything a flow 'remembers' while it runs lives in one of two places.

A single run keeps its working data in two stores. Get these two straight and most flow surprises disappear: the dictionary is the per-flow scratchpad, and the session is the run-wide shared store. Both are working memory for one run — they are not a database, and they are released when the conversation ends.

Dictionary${dictionary.…}
The per-flow working scratchpad — a nested object (objects, arrays, and plain values) that is private to one flow. It starts empty at a flow's start and is filled as the flow runs: an Assignment node sets properties on it, a Loop writes each element into it, an Agent node can drop its answer into it, and a Script node reads and writes it directly. Read a value in a field with a nested dot-path, e.g. ${dictionary.user.name}.
Session${session.…}
The whole-run shared store — a flat map of simple values only (text, a number, true/false, or empty). Every flow in the run — including flows it runs or jumps into, and each parallel branch — sees the same session, so use it for a value that must be visible everywhere in the run (a flag, an id). It starts empty: it is not pre-filled with the user's email or any run id, so ${session.userEmail} is empty unless some node wrote a userEmail key first.
The conversation
There is a third thing — the running conversation (the prompts and the agent's answers) that Agent nodes read and write. It is not a ${…} store: you never reach it with a placeholder. A Script node can read and drive it through the system/agent resource — see the Script Nodes page.

The ${…} Placeholder Model

Wherever a field is interpolated, the same rules apply — and they are strict.

Many node fields are interpolated: before the node runs, the platform scans the field for ${…} markers and substitutes each one with the value it resolves to. The rules are the same in every interpolated field, and they are deliberately narrow.

  • Only two roots resolve: ${dictionary.…} and ${session.…}. There is no ${agent.…}, ${input.…}, ${env.…}, ${settings.…}, or any other root — any other root resolves to empty.
  • Dictionary is nested; session is flat. ${dictionary.a.b.c} walks a path through plain objects. ${session.a.b} looks up the single key named a.b — it does not nest.
  • No array indexing, no bare root. ${dictionary.list[0]} and ${dictionary.list.0} both resolve to nothing, and a bare ${dictionary} (no dot) is not recognized either.
  • A missing value resolves to empty. There is no default or fallback syntax and no escaping — if ${dictionary.x} is not there, it simply vanishes from the result.

The type you get back depends on how the field is written. A field that is exactly one placeholder keeps the resolved value's type; a field that mixes text and a placeholder is always a string; and a plain value with no placeholder is stored as text — there is no automatic number or boolean conversion.

how a value resolves

${dictionary.count}300                     (one whole-string placeholder keeps its type — a number)
"Total is ${dictionary.count}""Total is 300"          (text mixed with a placeholder is always a string)
"1""1"                     (a plain value is stored as text — never the number 1)
${Array([red, green, blue])}["red","green","blue"]  (the Array(...) form builds a real array)
${dictionary.missing}           →  (empty)                 (a value that isn't there resolves to nothing)
Numbers arrive as text unless a placeholder carries them
Typing 1 into a value field stores the string "1", not the number. To keep a real number, point at a placeholder that already holds one — ${dictionary.count} — or convert it inside a Script node.

Where Placeholders Resolve

The exact fields that are interpolated — and the ones that are always literal.

Interpolation is not everywhere. This is the map of which fields read ${…} and which take a literal value exactly as typed.

Agent · prompt content
Interpolated. Each prompt turn's content resolves ${dictionary.*} and ${session.*} before the model sees it — e.g. a System turn that reads Answer for ${dictionary.user.name}.. The turn's role is literal.
Agent · Session ID / Request ID
Interpolated overrides. Both accept ${dictionary.*} / ${session.*}. Leave them blank in the normal case — the agent then uses the run's natural ids. Set one only to deliberately point the turn at a specific chat slot; do not force it to an empty value.
Agent · model & settings
Literal. The provider, model, sampling, token cap, effort, and the behaviour rules are taken exactly as set — placeholders do not apply.
Assignment & Script · pair value
Value interpolated, key literal. Each pair's value resolves through the model above; the pair's key is a plain, flat property name (no dots, no ${…}). A Script node's pairs arrive to the script as its inputs; a Script node's file name is a literal.
Condition · expression
Interpolated, then evaluated. The expression must wrap each data reference in ${…} — a bare dictionary.x throws. See the next section.
Loop · Read from / Set to
References, not interpolation. Each must name a store slot with a dictionary. or session. prefix — a bare name fails the node. See "References vs Placeholders" below.
Parallel Context · Context Collection
A dictionary reference. It must start with dictionary. and point at an array — not a session key, not a bare name.
Start label · End terminate
Literal. A Start node's label is a fixed entry name (the form even forbids $ { } .), and an End node's terminate option is a plain on/off — neither is interpolated.

Conditions Must Be Wrapped

The single most common mistake in a Condition expression.

A Condition node reads one boolean expression and routes down its True or False branch. The expression is not free-form code: each data reference must be wrapped in ${…} so it is substituted with a value before the comparison is evaluated.

condition — wrapped vs bare

${dictionary.score} > 25          →  runs: (30 > 25) → True branch          ✅
${dictionary.answer} === true     →  runs: (true === true) → True branch    ✅

dictionary.score > 25             →  nothing is substituted; evaluation fails ❌
                                     error: "Member access is not allowed"
A bare reference in a condition fails the node
Writing dictionary.score > 25 without the wrapper leaves the reference unsubstituted, and the evaluator rejects it with Member access is not allowed. The runnable form is always ${dictionary.score} > 25. Keep expressions simple — comparisons and logic over values and placeholders; there are no function calls, no member access, and no array literals.

References vs Placeholders

A few fields name a store slot instead of interpolating a value — and they need a prefix.

Most fields interpolate a value. A handful instead take a reference — the name of a store slot to read from or write into. A reference must carry a dictionary. or session. prefix (an optional ${…} wrapper is allowed), and a bare name fails.

loop & parallel context — references need a prefix

Loop · Read from :  dictionary.items        ✅   (the array to walk)
Loop · Set to    :  dictionary.current      ✅   (where each element lands — Read from ≠ Set to)
Loop · Read from :  items                   ❌   error: expected dictionary.<path> or session.<key>

Parallel Context · Context Collection :  dictionary.orders   ✅   (must be a dictionary array)
Parallel Context · Context Collection :  session.orders      ❌   (session is not allowed here)

A Loop delivers the current element into its Set to slot on every pass, so the loop body reads it back with a normal placeholder — e.g. ${dictionary.current}. A Parallel Context runs its template flow once per element of the collection, each on its own copy of that element, and merges each result back into the collection when it finishes.

Good to Know

  • The dictionary is per-flow; the session is per-run. A value one flow puts in its dictionary is not visible to a separate parallel flow — put anything that must be seen run-wide into the session (and keep it a simple value).
  • Identity is not in the session. The user's email, the chat id, and the run ids are not placeholder-readable — if a flow needs one in a field, a Script node must copy it into the dictionary or session first, then a field can read it.
  • Store real numbers and arrays through placeholders. Because a plain typed value is text, a later Condition comparing ${dictionary.x} > 25 against a value you typed as 1 compares a string — set numbers via a placeholder that already carries the number.
  • Assignment keys are flat. A key of a.b creates a literal property named a.b, not a nested ab. Use simple top-level names.
two roots resolvedictionary nested · session flatplain values are textwrap conditions in ${…}

Script Nodes & Scripts

Read and write these stores from your own code.

Node Reference

Every node's fields, dot by dot.

Flows & Runtime

How a flow of nodes is executed.

Previous

Node Reference

Next

Script Nodes & Scripts

Keen Agents 2026

Documentation

Release 15