Concepts

Persistent Memory

Reliant stores validated conversation turns in your own database and automatically injects the recent history as context on every subsequent call — giving your agents stateful, per-user memory without any external service.


How it works

Each call that has memory active produces a conversation_id. After the LLM output is validated, the user input and the validated JSON are stored as a turn. On the next call for the same user, the last N turns are prepended to the prompt as read-only context — the system prompt (and all its validation rules) remains untouched, so retries, fallbacks, and quality checks work exactly the same as without memory.

Only validated outputs are stored. If an execution returns FALLBACK or FAILED, that turn is discarded — bad data never pollutes the memory of future calls.

Enabling memory

Memory activates when both conditions are met in the same request:

1
Pass end_user_id
Any string that identifies your end user — a user ID, a phone number, a session token. This is the primary scope key for memory isolation.
2
Enable memory
Either toggle Memory enabled on the schema (permanent), or pass options.memory: true in the request body (ad-hoc per call).

Option A — Enable on the schema (recommended)

In the dashboard under Schemas, edit your schema and toggle Memory enabled. Every execute call for that schema that includes end_user_id will automatically use memory. You can also configure the memory window (number of turns to inject) per schema.

Option B — Force-enable per request

Pass options.memory: true in the request body to enable memory for that single call, even if the schema does not have it on.

json — request with memory
{ "prompt": "My name is João Silva, email joao@empresa.com", "schema_id": "cmc...", "provider": "anthropic", "model": "claude-sonnet-4-6", "user_id": "uuid-do-seu-usuario", "end_user_id": "cliente-123", "options": { "memory": true } }

Response

When memory is active, the response includes a conversation_id both at the top level and inside metadata.

json — response
{ "success": true, "status": "SUCCESS", "output": { "name": "João Silva", "email": "joao@empresa.com", "phone": null }, "conversation_id": "cmr4x9k2e0001qk01abc12345", "metadata": { "execution_id": "cmr4x...", "attempts": 1, "latency_ms": 812, "tokens_used": 284, "provider": "anthropic", "model": "claude-sonnet-4-6", "conversation_id": "cmr4x9k2e0001qk01abc12345" } }

Multiple threads with external_id

If the same user needs independent memory threads — for example, separate support tickets or independent chat sessions — pass a different external_id for each thread. Memory is scoped by (project + schema + end_user_id + external_id), so threads never mix.

json
// Thread A — support ticket #42 { "end_user_id": "cliente-123", "external_id": "ticket-42", ... } // Thread B — support ticket #87 (completely separate memory) { "end_user_id": "cliente-123", "external_id": "ticket-87", ... }

Full example — stateful extraction

typescript
const BASE = 'https://reliant-production.up.railway.app' const HEADERS = { 'Content-Type': 'application/json', 'X-Reliant-Key': 'rel_...', } // Turn 1 — user sends name and email const r1 = await fetch(`${BASE}/execute`, { method: 'POST', headers: HEADERS, body: JSON.stringify({ prompt: 'My name is João Silva, email joao@empresa.com', schema_id: 'your-schema-id', provider: 'anthropic', model: 'claude-sonnet-4-6', user_id: 'your-user-uuid', end_user_id: 'cliente-123', options: { memory: true }, }), }) const d1 = await r1.json() // d1.output → { name: 'João Silva', email: 'joao@empresa.com', phone: null } // d1.conversation_id → 'cmr4x...' // Turn 2 — user sends phone, name/email remembered automatically const r2 = await fetch(`${BASE}/execute`, { method: 'POST', headers: HEADERS, body: JSON.stringify({ prompt: 'My phone is (11) 99999-8888', schema_id: 'your-schema-id', provider: 'anthropic', model: 'claude-sonnet-4-6', user_id: 'your-user-uuid', end_user_id: 'cliente-123', options: { memory: true }, }), }) const d2 = await r2.json() // d2.output → { name: 'João Silva', email: 'joao@empresa.com', phone: '(11) 99999-8888' } // ↑ name and email filled from memory — no need to repeat them

Limits

ParameterDefaultDescription
Memory window10 turnsNumber of recent turns injected as context. Configurable per schema.
Max per stored turn4,000 charsContent exceeding this is truncated before storage.
Max injected context24,000 charsTotal history block injected per call. Older turns are omitted if exceeded.

Security and isolation

Memory is scoped by (project + schema + end_user_id + external_id). It is physically impossible for one user's history to appear in another user's context. Memory is stored in your own database (Railway PostgreSQL) — no data leaves your infrastructure.

The injected history is marked as read-only context and the LLM is explicitly instructed to never follow instructions found inside it. Only the system prompt has authority. This prevents memory-poisoning attacks where a malicious user turn tries to override the schema rules.


Managing memory

In the dashboard, go to Memory to see all active conversation threads, inspect individual turns, and erase a user's memory when needed (e.g. GDPR right-to-erasure requests).