Global Variables
EDDI includes a built-in global variable store for managing configuration values like default LLM model names, API base URLs, temperature settings, and feature flags. Variables are scoped per tenant — single-tenant deployments use "default" implicitly, while multi-tenant deployments can maintain separate variable sets per tenant. Unlike secrets, global variables are not encrypted and are fully visible in the UI and logs.
Architecture
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Configuration │────>│ GlobalVariable │────>│ GlobalVariable │
│ (JSON configs) │ │ Resolver │ │ Store │
│ ${vars:..} │ │ (regex + cache) │ │ (MongoDB/PG) │
└─────────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ Template Layer │
│ {vars.<key>} │
│ (Qute) │
└─────────────────┘Core Components
GlobalVariable
configs.variables.model
Record: tenantId, key, value, description, exportable
IGlobalVariableStore
configs.variables
Persistence interface (non-versioned, tenant-scoped key-value)
GlobalVariableStore
configs.variables.mongo
MongoDB implementation (globalvariables collection, composite _id: tenantId/key)
PostgresGlobalVariableStore
datastore.postgres
PostgreSQL implementation (global_variables table, PK: (tenant_id, key))
GlobalVariableResolver
configs.variables
Resolves ${vars:...} references with Caffeine cache
IRestGlobalVariableStore
configs.variables.rest
JAX-RS REST interface
RestGlobalVariableStore
configs.variables.rest
REST implementation with key validation and cache invalidation
Two Access Syntaxes
Global variables are available through two complementary syntaxes:
1. Template Syntax: {vars.<key>}
Available in LLM task system prompts and other template contexts. Resolved by the Qute template engine at template processing time.
2. Late-Binding Syntax: ${vars:<key>} / ${vars:tenantId/<key>}
Available everywhere — in LLM task parameters, HTTP call configurations, MCP/A2A settings, embedding configs, Slack configs, and even the type field that selects the LLM provider. Resolved by GlobalVariableResolver at runtime, after template processing but before vault secret resolution.
Supports two forms, mirroring the vault's pattern:
Short form
${vars:key}
Uses the context tenant (defaults to "default")
Full form
${vars:tenantId/key}
Uses the explicit tenant
Multi-tenant example:
When to Use Which
{vars.<key>}
System prompts, template-processed strings
Dynamic prompt content that changes per-deployment
${vars:<key>}
Everywhere (params, URLs, headers, type)
Operational config that affects infrastructure
Resolution Order
EDDI resolves configuration values in a strict three-step order:
This ordering is important: vault secrets can contain global variable references, and global variables can be composed with template expressions.
⚠️ Nesting is not supported.
${vars:${vault:x}}and${vault:${vars:x}}will NOT work. Each resolution layer operates independently on the fully-resolved output of the previous layer.
Where References Work
${vars:...} references are resolved in these pipeline callsites:
LLM Task (langchain.json)
type (provider selection), all template parameters
Chat Model Registry
All model parameters before model creation
HTTP Calls (httpcalls.json)
URL, request body, headers, query parameters
MCP Tool Providers
API keys, server URLs
A2A Tool Providers
API keys, agent URLs
Embedding Model Factory
All embedding model parameters
Embedding Store Factory
All embedding store parameters
Channel integrations are not on this list.
ChannelTargetRouterresolves the platform config of a channel integration (Slack bot tokens, signing secrets, …) throughSecretResolver, which understands${vault:...}only. A${vars:...}reference placed there is passed through verbatim to the channel API.
REST API
All endpoints require the eddi-admin or eddi-editor role.
Endpoints
GET
/variablestore/variables/{tenantId}
List all variables for a tenant
GET
/variablestore/variables/{tenantId}/{key}
Get a single variable by tenant and key
PUT
/variablestore/variables/{tenantId}/{key}
Create or update a variable
DELETE
/variablestore/variables/{tenantId}/{key}
Delete a variable
Single-tenant shortcut: Use
defaultas the tenantId for single-tenant deployments.
ID Validation
Both tenantId and key must match the pattern [a-zA-Z0-9_.\-]+:
default
has space
tenant-a
key=value
api.base-url
key/path
feature_flag_v2
special!char
Response Examples
GET /variablestore/variables/default:
PUT /variablestore/variables/default/default-model — request body:
GET /variablestore/variables/default/missing-key — returns 404 Not Found.
Caching
Global variables are cached in a Caffeine cache with a 2-minute TTL (configurable). This means:
Variables load once from the database, then serve from cache
After creating/updating/deleting a variable via the REST API, changes take effect immediately (the REST endpoint invalidates the cache)
If the database is updated directly (outside the REST API), changes appear within 2 minutes
Configuration
Downstream Cache Invalidation
When global variables change, downstream caches that were built with old variable values need to be evicted. The GlobalVariableResolver supports an invalidation listener pattern:
ChatModelRegistry registers a listener that clears all cached model instances when variables change
This ensures that if you change
${vars:default-model}fromgpt-4.1togpt-4.1-mini, all agents pick up the new model on their next request
Use Cases
Fleet-Wide Model Switching
Set a default model for all agents:
Reference it in all agents' langchain.json:
Switch all agents at once:
Multi-Tenant Model Switching
Set a different default model per tenant:
Reference in agent config:
Environment-Specific API Endpoints
Feature Flags
System Prompt Injection
Comparison: Global Variables vs Secrets vs Properties vs Snippets
Purpose
Operational config
Sensitive credentials
Per-user/conversation state
Reusable prompt text
Scope
Per-tenant (all agents)
Per-tenant
Per-user or per-conversation
Deployment-wide
Encryption
None
AES-256-GCM
None
None
Visibility
Fully visible
Write-only
Fully visible
Fully visible
Template syntax
{vars.<key>}
—
{properties.<key>}
{snippets.<name>}
Late-binding
${vars:<key>} or ${vars:tenantId/<key>}
${vault:<key>} or ${vault:tenantId/<key>}
—
—
Versioned
No
No
No
Yes
REST path
/variablestore/variables/{tenantId}
/secretstore/secrets/{tenantId}
via PropertySetter
/snippetstore/snippets
Caching
2 min (per-tenant)
5 min
No cache
5 min
Export
Configurable (exportable)
Always scrubbed
Per-user
Included
Decision Guide
Need to store an API key? → Use the Secrets Vault (
${vault:...})Need to change the LLM model for all agents? → Use a Global Variable (
${vars:...})Need to remember a user's name across conversations? → Use Properties with
scope: longTermNeed reusable system prompt instructions? → Use Prompt Snippets (
{snippets.<name>})
Testing
Backend Tests
GlobalVariableTest
9
Model record, tenant defaults, JSON serialization, equality
GlobalVariableResolverTest
16
Short/full form resolution, tenant caching, invalidation, edge cases
RestGlobalVariableStoreTest
11
CRUD, tenant+key validation, cache invalidation, patterns
GlobalVariableStoreTest
9
MongoDB adapter CRUD with mocked MongoCollection, tenant scoping
PostgresGlobalVariableStoreTest
14
PostgreSQL adapter CRUD with mocked JDBC, tenant isolation, error paths
GlobalVariableCrudIT
8
Full CRUD lifecycle against MongoDB (Testcontainers)
PostgresGlobalVariableCrudIT
8
Full CRUD lifecycle against PostgreSQL (Testcontainers)
Integration Tests
The integration tests verify the complete REST API lifecycle:
List variables (initially empty)
Create a variable via PUT
Read the variable by key
Update the variable value
Verify the variable appears in the list
Verify 404 for non-existent keys
Verify validation rejects invalid key patterns
Delete the variable and verify removal
Tests run against both MongoDB and PostgreSQL via Quarkus DevServices (Testcontainers).
Last updated
Was this helpful?