Properties are EDDI's primary mechanism for storing and retrieving state within and across conversations. They are key-value pairs that can be set by agent configuration, extracted from API responses, or written by LLM tools — and they're accessible in every template (system prompts, HTTP call bodies, output templates, property instructions).
Properties are the glue that connects the pipeline's processing steps with persistent user state. Understanding how they work is essential for building stateful agents.
Key Concepts
What Properties Are
A property has:
Name (key): The identifier used to access the property (e.g., preferred_language, company_name)
Value: Can be a String, Integer, Float, Map, List, or Boolean
Scope: How long the property lives
Visibility (v6): Who can see the property
Property vs Context vs Memory
Mechanism
Source
Lifetime
Access Pattern
Properties
Agent-set (via PropertySetter, LLM tools)
Configurable (step → longTerm)
{properties.key.valueString}
Context
Your application (passed per request)
Per request
{context.key}
Memory
Pipeline (each task writes data)
Per step (current turn's data)
{memory.current.key}
Use properties when the agent needs to remember something. Use context when your application injects something. Use memory when you need data from the current or previous pipeline step.
Scopes
Properties support four scopes that control their lifetime:
Scope
Lifetime
Persistence
Use Case
step
Current conversation turn only
Not persisted
Temporary data needed only for this response
conversation
Entire conversation session
Persisted in conversation memory
User preferences within a session, extracted entities
longTerm
Across conversations
Persisted in user property store
User profile data, preferences that should survive between sessions
secret
Across conversations (encrypted)
Persisted via SecretsVault
API keys, tokens, sensitive credentials
Choosing the Right Scope
Visibility (v6)
Properties also have a visibility dimension that controls which agents can see them:
Visibility
Who sees it
Use Case
self (default)
Only the owning agent
Agent-specific preferences, internal state
group
All agents in the same group conversation
Shared context in multi-agent orchestration
global
All agents for this user
Cross-agent user preferences (e.g., language, timezone)
Visibility is orthogonal to scope — a property can be longTerm + self (persists across sessions, visible only to the owning agent) or longTerm + global (persists and visible to all agents).
Setting Properties
Via PropertySetter Configuration (JSON)
The PropertySetter task (ai.labs.property) sets properties based on triggered actions:
When the greet_user action fires:
greeted is set to "true" for this conversation session
preferred_language is set from the input context and persisted across all conversations and agents
Via Pre/Post Request Instructions
Properties can be set before or after any lifecycle task (HTTP calls, LLM calls):
Via LLM Tools (Agent-Driven)
When enableMemoryTools is enabled in the agent configuration, the LLM can set properties using built-in memory tools:
See Persistent User Memory for full details on the LLM memory tools, visibility scoping, and Dream consolidation.
Accessing Properties in Templates
Properties are available in all templates via the properties namespace:
In Output Templates
In System Prompts (LLM)
In HTTP Call Bodies
Property Value Accessors
Accessor
Type
Example
.valueString
String value
{properties.name.valueString}
.valueInt
Integer value
{properties.age.valueInt}
.valueFloat
Float value
{properties.score.valueFloat}
.valueObject
Object/Map
{properties.profile.valueObject.email}
.valueList
List
{#for item in properties.tags.valueList}...{/for}
.valueBoolean
Boolean
{#if properties.isPremium.valueBoolean}...{/if}
Property Lifecycle
Properties are managed by Conversation.java at session boundaries — NOT by pipeline tasks.
1. Conversation Init
Recall order (most_recent or most_accessed) and max entries come from the agent's UserMemoryConfig if configured, otherwise sensible defaults (1000 entries, most recent).
2. Pipeline Execution
3. Conversation Teardown
Key insight: Persistent state is a session concern handled at init/teardown — NOT in the pipeline. This means properties "just work" without any task ordering dependencies.
Storage: In v6, all persistent properties are stored in the unified usermemories collection (MongoDB) or usermemories table (PostgreSQL). The legacy properties collection has been removed. See Persistent User Memory for the full unified memory model.
Secret Properties
Properties with scope=secret are automatically handled by the SecretsVault:
During pipeline execution, the secret value is available in memory normally
At teardown, the value is encrypted and stored in SecretsVault
The in-memory property value is scrubbed (replaced with a vault reference)
On next conversation init, the value is loaded from the vault and decrypted
Is this data only needed for the current response?
→ step
Will the user need this data later in the same conversation?
→ conversation
Should this data persist when the user starts a new conversation?
→ longTerm
Is this sensitive data (API keys, tokens)?
→ secret
Agent: "I'll remember that you prefer dark mode."
→ Tool call: rememberFact(key="ui_preference", value="dark_mode", category="preference", visibility="self")
Hello {properties.userName.valueString}! Your preferred language is {properties.preferred_language.valueString}.
You are a helpful assistant. The user's name is {properties.userName.valueString}.
They prefer {properties.preferred_language.valueString} responses.
Conversation.init()
└─→ loadUserProperties()
└─→ IPropertiesHandler.getUserMemoryStore()
└─→ IUserMemoryStore.getVisibleEntries(userId, agentId, groupIds, recallOrder, maxEntries)
└─→ Entries converted to Property objects with scope=longTerm
└─→ Loaded into conversationProperties
└─→ Available as {properties.key} in all templates
LifecycleManager runs pipeline
└─→ PropertySetterTask sets properties based on actions
└─→ scope=step (cleared after this turn)
└─→ scope=conversation (lives for the session)
└─→ scope=longTerm (persisted across conversations)
└─→ scope=secret (auto-vaulted via SecretsVault)
Conversation.postConversationLifecycleTasks()
└─→ storePropertiesPermanently()
└─→ All longTerm properties saved via IUserMemoryStore.upsert()
└─→ Visibility applied at persistence boundary:
- Explicit visibility on property → used as-is
- No visibility set → defaults to agent's defaultVisibility (or `global`)