Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9d5a27e
Add variable composition: Handlebars template rendering, <<variable_n…
dmontagu Mar 7, 2026
99bb3c2
Merge remote-tracking branch 'origin/main' into feature/variable-comp…
petyosi May 8, 2026
a335ced
Add agents.md
petyosi May 8, 2026
91066f9
Use prompt reference syntax for variable composition
petyosi May 8, 2026
24f70c5
Make Handlebars support lazy optional
petyosi May 8, 2026
ae02b79
Address variable composition review findings
petyosi May 8, 2026
4ffdaff
Skip Handlebars tests without optional dependency
petyosi May 8, 2026
fcbff77
Cover variable composition edge cases
petyosi May 8, 2026
10d2d52
Cover pytest plugin exception fallback
petyosi May 8, 2026
8589f54
Address variable composition review follow-ups
petyosi May 8, 2026
402d995
Address Cubic variable review comments
petyosi May 8, 2026
5fbadd2
Narrow pytest exception recording fallback
petyosi May 8, 2026
96fb368
Merge branch 'main' into feature/variable-composition
petyosi May 8, 2026
0b2d469
Send template input schemas for remote variables
petyosi May 8, 2026
4a37df0
Merge branch 'main' into feature/variable-composition
petyosi May 8, 2026
052fe6c
Address remaining variable review comments
petyosi May 8, 2026
29592ad
Merge remote-tracking branch 'origin/feature/variable-composition' in…
petyosi May 8, 2026
2e8c2b2
Document template variable dependency
petyosi May 11, 2026
94d3c40
Fail early without template variable dependency
petyosi May 11, 2026
67e785e
Address variable composition review cleanup
petyosi May 11, 2026
85331ca
Fix runnable managed variables docs example
petyosi May 11, 2026
15bdf98
Fix variable composition default handling
petyosi May 11, 2026
543e822
Clarify provider no-value composition test
petyosi May 11, 2026
c068526
Drop dict overload for template_var
dmontagu May 20, 2026
8a3a1a7
Drop render() and template_inputs= on var()
dmontagu May 20, 2026
3b2cb6b
Promote _reason to public, expose code_default at top level
dmontagu May 20, 2026
551b74f
Rename reference_warnings to reference_errors
dmontagu May 20, 2026
db1f8e1
Honor variable overrides through composition
dmontagu May 20, 2026
fd0c69f
Warn on composition failure; treat unresolved refs as errors
dmontagu May 20, 2026
aeb1ac7
Move template_inputs state to TemplateVariable
dmontagu May 20, 2026
3a12385
Replace double-backticks with single backticks in docstrings
dmontagu May 20, 2026
947fba2
Skip template_var doc examples without pydantic-handlebars
dmontagu May 20, 2026
c80b05d
Merge remote-tracking branch 'origin/main' into feature/variable-comp…
dmontagu May 20, 2026
0ef5fe4
Cover the uncovered composition and render branches
dmontagu May 20, 2026
51ae033
Skip handlebars-dependent coverage test on Python 3.9
dmontagu May 21, 2026
b2a2f90
Export composition types from logfire.variables
dmontagu May 21, 2026
362a227
revert pyi file changes for simpler PR diff
alexmojaki May 21, 2026
bf9a3bc
Merge branch 'main' into feature/variable-composition
alexmojaki May 21, 2026
b8b2fa5
Use logfire module shortcuts in type checking test
alexmojaki May 21, 2026
c7ae5b9
Merge branch 'main' into feature/variable-composition
alexmojaki May 21, 2026
17fd010
Move ResolutionReason to abstract variables module
alexmojaki May 21, 2026
9474807
Merge branch 'codex/variable-structure-refactor' of github.com:pydant…
alexmojaki May 21, 2026
b2f4c76
Merge remote-tracking branch 'origin/codex/variable-structure-refacto…
alexmojaki May 21, 2026
d2bfa54
Remove _record_exception, we have a better solution from main now
alexmojaki May 21, 2026
8b04bfd
Fix variable helper review findings
alexmojaki May 21, 2026
e7ab01d
Address managed variable doc review threads
dmontagu May 21, 2026
b5e54c2
Move template-inputs schema off the shared variable base
dmontagu May 21, 2026
85dcbad
Share the lookup priority chain between resolution and composition
dmontagu May 21, 2026
2c76bc3
Skip composition doc examples on Python 3.9
dmontagu May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
| `json_schema` | JSON Schema for validation (optional) |
| `description` | Human-readable description (optional) |
| `aliases` | Alternative names that resolve to this variable (optional, for migrations) |
| `example` | JSON-serialized example value, used as template in UI (optional) |
| `example` | JSON-serialized example value, used as starting point when creating versions in the UI (optional) |
| `template_inputs_schema` | JSON Schema for template `{{placeholder}}` inputs (optional, set automatically when template inputs are declared via `logfire.template_var()`) |

**LabeledValue** — A label with an inline serialized value:

Expand Down
51 changes: 47 additions & 4 deletions docs/reference/advanced/managed-variables/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Managed variables are a way to externalize runtime configuration from your code.
- **Observability-integrated**: Every variable resolution creates a span, and using the context manager automatically sets baggage so downstream operations are tagged with which label and version was used
- **Versions and labels**: Create immutable version snapshots of your variable's value, and assign labels (like `production`, `staging`, `canary`) that point to specific versions
- **Rollouts and targeting**: Control what percentage of requests receive each labeled version, and route specific users or segments based on attributes
- **Templates and composition**: Use `{{placeholder}}` Handlebars syntax in values that get rendered with runtime inputs, and compose variables from reusable fragments via `@{other_variable}@` references (see [Templates and Composition](templates-and-composition.md))

### Versions and Labels

Expand Down Expand Up @@ -112,6 +113,46 @@ With managed variables, you can iterate safely in production:
- **Instant rollback**: If a version is causing problems, move the label back to the previous version in seconds, with no deploy required
- **Full history**: Every version is immutable and preserved, so you can always see exactly what was served and when

## Template Variables

For AI applications, variables often contain prompt templates with placeholders that get filled in at runtime. **Template variables** support this natively with Handlebars `{{placeholder}}` syntax:

!!! note "Install the variables extra for templates"
Template rendering requires the `pydantic-handlebars` package, which is installed by the `logfire[variables]` extra on Python 3.10 and later.

```bash
pip install 'logfire[variables]'
```

```python
from pydantic import BaseModel

import logfire

logfire.configure()


class PromptInputs(BaseModel):
user_name: str
is_premium: bool = False


prompt = logfire.template_var(
'system_prompt',
type=str,
default='Hello {{user_name}}!{{#if is_premium}} Welcome back, valued member.{{/if}}',
inputs_type=PromptInputs,
)

with prompt.get(PromptInputs(user_name='Alice', is_premium=True)) as resolved:
Comment thread
dmontagu marked this conversation as resolved.
print(resolved.value)
#> Hello Alice! Welcome back, valued member.
```

Variables can also reference other variables using `@{variable_name}@` syntax, allowing you to compose values from reusable fragments that can be independently updated in the UI.

For full details, see [Templates and Composition](templates-and-composition.md).

## How It Works

Here's the typical workflow using the `AgentConfig` example from above:
Expand Down Expand Up @@ -231,8 +272,10 @@ This bypasses the rollout weights and directly resolves the value from the speci

### Variable Parameters

| Parameter | Description |
|-----------|-------------------------------------------------------------------------|
| `name` | Unique identifier for the variable |
| Parameter | Description |
|-----------|-------------|
| `name` | Unique identifier for the variable |
| `type` | Expected type for validation; can be a primitive type or Pydantic model |
| `default` | Default value when no configuration is found (can also be a function) |
| `default` | Default value when no configuration is found (can also be a function) |

For variables with Handlebars template rendering, use `logfire.template_var()` instead, which adds an `inputs_type` parameter. See [Templates and Composition](templates-and-composition.md).
220 changes: 220 additions & 0 deletions docs/reference/advanced/managed-variables/templates-and-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Template Variables and Composition

Managed variables can contain **Handlebars templates** (`{{placeholder}}`) and **composition references** (`@{other_variable}@`), enabling dynamic values that are assembled from multiple sources and rendered with runtime inputs.

This is especially useful for AI applications where prompts are built from reusable fragments and personalized with request-specific data.

!!! note "Install the variables extra"
Template rendering requires the `pydantic-handlebars` package, which is installed by the `logfire[variables]` extra on Python 3.10 and later:

```bash
pip install 'logfire[variables]'
```

Without this extra, `logfire.template_var()` raises an error immediately so your application does not silently use an unrendered template.

## Template Variables

A **template variable** is a variable whose value contains `{{placeholder}}` expressions that are rendered with typed inputs at resolution time. Define one with `logfire.template_var()` and call `.get(inputs)` to resolve and render in one step:

```python
from pydantic import BaseModel

import logfire

logfire.configure()


class PromptInputs(BaseModel):
user_name: str
is_premium: bool = False


prompt = logfire.template_var(
'system_prompt',
type=str,
default='Hello {{user_name}}!{{#if is_premium}} Thank you for being a premium member.{{/if}}',
inputs_type=PromptInputs,
)

with prompt.get(PromptInputs(user_name='Alice', is_premium=True)) as resolved:
print(resolved.value)
#> Hello Alice! Thank you for being a premium member.

with prompt.get(PromptInputs(user_name='Bob')) as resolved:
print(resolved.value)
#> Hello Bob!
```

The full resolution pipeline is:

1. **Resolve** — fetch the serialized value from the provider (or use the code default)
2. **Compose** — expand any `@{variable_name}@` references (see [Composition](#variable-composition) below)
3. **Render** — render `{{placeholder}}` Handlebars templates using the provided inputs
4. **Deserialize** — validate and deserialize to the variable's type

`logfire.template_var()` accepts the same parameters as `logfire.var()` plus an `inputs_type` parameter — a Pydantic `BaseModel` (or any type supported by `TypeAdapter`) describing the expected template inputs. It is used for type-safe `.get(inputs)` calls and generates a `template_inputs_schema` for validation.

### Handlebars Syntax

Template variables use [Handlebars](https://handlebarsjs.com/) syntax, powered by the [`pydantic-handlebars`](https://github.com/pydantic/pydantic-handlebars) library. The most common patterns:

| Syntax | Description |
|--------|-------------|
| `{{field}}` | Insert a value |
| `{{obj.nested}}` | Dot-notation access |
| `{{#if field}}...{{/if}}` | Conditional block |
| `{{#unless field}}...{{/unless}}` | Inverse conditional |
| `{{#each items}}...{{/each}}` | Iterate over a list |
| `{{#with obj}}...{{/with}}` | Change context |
| `{{! comment }}` | Comment (not rendered) |

### Structured Template Variables

Template variables work with structured types too. Only string fields containing `{{placeholders}}` are rendered — other fields pass through unchanged:

```python
from pydantic import BaseModel

import logfire

logfire.configure()


class UserContext(BaseModel):
user_name: str
tier: str


class AgentConfig(BaseModel):
instructions: str
model: str
temperature: float


agent_config = logfire.template_var(
'agent_config',
type=AgentConfig,
default=AgentConfig(
instructions='You are helping {{user_name}}, a {{tier}} customer.',
model='openai:gpt-4o-mini',
temperature=0.7,
),
inputs_type=UserContext,
)

with agent_config.get(UserContext(user_name='Alice', tier='premium')) as resolved:
print(resolved.value.instructions)
#> You are helping Alice, a premium customer.
print(resolved.value.model)
#> openai:gpt-4o-mini
```

### Template Validation

When a template variable is pushed to Logfire (via `logfire.variables_push()`), the `template_inputs_schema` is synced alongside the variable's JSON schema. The system validates that all `{{field}}` references in variable values (including values reachable through composition) are compatible with the declared schema.

For example, if your `inputs_type` declares `user_name: str` and `is_premium: bool`, but a version value references `{{unknown_field}}`, the validation will flag this as an error.

## Variable Composition {#variable-composition}

**Composition** lets a variable's value reference other variables using `@{variable_name}@` syntax. When the variable is resolved, `@{ref}@` references are expanded by looking up the referenced variable and substituting its value.

This is useful for building values from reusable fragments:

```python
import logfire

logfire.configure()

# A reusable instruction fragment
safety_rules = logfire.var(
'safety_rules',
type=str,
default='Never share personal data. Always be respectful.',
)

# A prompt that includes the safety rules via composition
agent_prompt = logfire.var(
'agent_prompt',
type=str,
default='You are a helpful assistant. @{safety_rules}@',
)

with agent_prompt.get() as resolved:
print(resolved.value)
#> You are a helpful assistant. Never share personal data. Always be respectful.
```

When `safety_rules` is updated in the Logfire UI, all variables that reference `@{safety_rules}@` automatically pick up the new value — no code changes or redeployment required.

### Composition Control Flow

The `@{}@` syntax supports a small Handlebars-compatible subset for composing variables. It supports simple references, dotted field reads, and block helpers that branch or iterate over a top-level referenced variable:

| Syntax | Description |
|--------|-------------|
| `@{variable_name}@` | Insert a variable's value |
| `@{variable.field}@` | Access a nested field |
| `@{#if variable}@...@{else}@...@{/if}@` | Conditional on whether a variable is set |
| `@{#each items}@...@{/each}@` | Iterate over a list variable |

Block helper conditions and iterables must be top-level variable names. Use `@{#if user}@...@{user.active}@...@{/if}@` rather than `@{#if user.active}@`.
Comment thread
dmontagu marked this conversation as resolved.

### Composition Tracking

Every `@{ref}@` expansion is recorded in the resolution result. You can inspect which variables were composed and their values:

```python skip="true"
with agent_prompt.get() as resolved:
for ref in resolved.composed_from:
print(f" {ref.name}: version={ref.version}, label={ref.label}")
```

These composition details are also recorded as span attributes, so you can see the full composition chain in your Logfire traces.

### Combining Templates and Composition

Template variables and composition work together. A common pattern is to compose reusable fragments via `@{ref}@` and render runtime inputs via `{{}}`:

```python skip="true"
from pydantic import BaseModel

import logfire

logfire.configure()


class ChatInputs(BaseModel):
user_name: str
language: str


# Reusable fragment (no template inputs)
tone_instructions = logfire.var(
'tone_instructions',
type=str,
default='Be friendly and concise.',
)

# Template variable that composes the fragment and renders inputs
chat_prompt = logfire.template_var(
'chat_prompt',
type=str,
default='You are helping {{user_name}}. Respond in {{language}}. @{tone_instructions}@',
inputs_type=ChatInputs,
)

# Resolution: compose @{tone_instructions}@ first, then render {{user_name}} and {{language}}
with chat_prompt.get(ChatInputs(user_name='Alice', language='French')) as resolved:
print(resolved.value)
# "You are helping Alice. Respond in French. Be friendly and concise."
```

### Cycle Detection

The system detects circular references during validation. If variable A references `@{B}@` and variable B references `@{A}@`, `logfire.variables_validate()` reports the cycle, and `logfire.variables_push(strict=True)` fails instead of applying the invalid configuration. This prevents infinite loops during resolution.

## Requirements

`pydantic-handlebars` requires Python 3.10 or later. On Python 3.9, basic variable features (`logfire.var()` without templates or composition) still work, but template rendering is not available.
Loading
Loading