Templates and expressions
FML uses two expression systems, and mixing them up is the single most common source of bugs in a plan. The rule behind all of them is one sentence:
Go text templates always produce a string. Wrapped Expr
$(...)preserves the real type.
This guide explains the three positions where expressions appear, why the string-vs-type distinction matters, and how to always pick the right one.
Prerequisites
- The Data flow: params, vars & context guide — you should know the three namespaces.
Why there are two systems
A prompt is text, so interpolating a value into it should give you text — that's what Go templates do. But a tool argument might need a real array, object, or number, not a string that looks like one. Handing "[1, 2, 3]" (a string) to an argument that expects [1, 2, 3] (an array) is a type error waiting to happen. $(...) exists to pass the native typed value through untouched.
The three positions
| Position | Syntax | Type | Where |
|---|---|---|---|
| 1. Text | {{ ... }} | Always string | prePrompt/prompt text, context strings, call string args, set values, parameter defaults |
| 2. Typed tool args | $( ... ) | Any (native) | Only inside call argument values |
| 3. Session conditions | "..." (bare Expr) | Bool / value | expect="...", iterate="..." |
1. Go text templates — {{ ... }}
Used anywhere the destination is text:
Common patterns:
| Pattern | Meaning |
|---|---|
{{ .params.x }} | Parameter, as a string |
{{ .vars.x }} | Variable, as a string |
{{ .context.session }} | Full session output (Go struct notation) |
{{ .context.session | json }} | Session output serialized to a JSON string |
{{ .it }} | Current iterator element, as a string |
The | json filter is how you drop a whole structured object into prompt text — it serializes to a JSON string, which is exactly what you want inside text.
2. Wrapped Expr — $( ... )
Used exclusively inside call argument values, when the argument must receive a non-string value from scope:
| Pattern | Returns | Use case |
|---|---|---|
$(context.session.field) | Any | Pass a nested object/array from a session |
$(params.count) | int | Pass a number as a number |
$(vars.myList) | array | Pass a variable holding an array |
$(len(context.items)) | int | Pass a computed integer |
3. Bare Expr in session arguments
expect and iterate take a plain quoted Expr string — no $(...) wrapper:
| Pattern | Meaning |
|---|---|
len(context.first) > 0 | Array length check |
context.overview.keyPoints != null | Nil check |
params.max_results | Parameter value |
The decision in one question
When you're about to interpolate a value, ask: is the destination text, or a tool argument?
- Text (a prompt, a label, a
contextstring) →{{ ... }}. If it's a structure, add| json. - A
callargument that needs a real array/object/number →$( ... ). - An
expect/iteratecondition → bare Expr in quotes, no wrapper.
The classic bug
You want to pass a session's array of sources to a tool:
And the inverse — trying to use $(...) in prompt text, where it doesn't belong:
Common mistakes
| Mistake | Fix |
|---|---|
"{{ .context.items }}" for an array/object tool arg | Use $(context.items) — templates only make strings |
$( ... ) inside prompt or prePrompt text | Use {{ ... | json }} in text; $() is for call args only |
Wrapping expect/iterate in $(...) | They take bare Expr strings: expect="len(context.x) > 0" |
Passing a number as "{{ .params.n }}" to a numeric arg | Use $(params.n) to keep it an int |
Next steps
- Apply this in PreCalls deep-dive, where argument typing matters most.
- Revisit the namespaces in Data flow: params, vars & context.
Go templates always make strings; $(...) preserves real types. Learn the three expression positions and never hand a tool a stringified array again.