Skip to main content
The Memory system intelligently manages conversation history.

Memory Integrated in Agent

const agent = new Agent({
  name: 'Memory Agent',
  instructions: 'You remember everything.',
  model: openai('gpt-4o'),
  memory: {
    maxTurns: 20,           // Limit turns
    maxTokens: 4000,        // Limit tokens
    summarizeAfter: 50,     // Summarize after N turns
    summarizePrompt: 'Create a concise summary with key facts and action items',
    summarizeModel: openai('gpt-4o-mini'), // Cheaper model for summaries
  },
});

Standalone Memory Manager

import { Memory } from '@runflow-ai/sdk';

// Using static methods (most common - 99% of cases)
await Memory.append({
  role: 'user',
  content: 'Hello!',
  timestamp: new Date(),
});

await Memory.append({
  role: 'assistant',
  content: 'Hi! How can I help you?',
  timestamp: new Date(),
});

// Get formatted history
const history = await Memory.getFormatted();
console.log(history);

// Get recent messages
const recent = await Memory.getRecent(5); // Last 5 turns

// Search in memory
const results = await Memory.search('order');

// Check if memory exists
const exists = await Memory.exists();

// Get full memory data
const data = await Memory.get();

// Clear memory
await Memory.clear();

Memory with User Identification

import { Memory } from '@runflow-ai/sdk';
import { identify } from '@runflow-ai/sdk/observability';

// Identify user
identify('+5511999999999');

// Memory automatically uses the context
await Memory.append({
  role: 'user',
  content: 'My order number is 12345',
  timestamp: new Date(),
});

// Memory is automatically bound to the phone number

Custom Memory Key

// Create memory with custom key
const memory = new Memory({
  memoryKey: 'custom_key_123',
  maxTurns: 10,
});

// Now use instance methods
await memory.append({ role: 'user', content: 'Hello', timestamp: new Date() });
const history = await memory.getFormatted();

Cross-Session Access

// Access memory from different sessions (admin, analytics, etc)
const dataUser1 = await Memory.get('phone:+5511999999999');
const dataUser2 = await Memory.get('email:user@example.com');

// Search across multiple sessions
const results = await Promise.all([
  Memory.search('bug', 'user:123'),
  Memory.search('bug', 'user:456'),
  Memory.search('bug', 'user:789'),
]);

// Get recent from specific session
const recent = await Memory.getRecent(5, 'session:abc123');

// Clear specific session
await Memory.clear('phone:+5511999999999');

Listing Sessions

Memory.list() queries all sessions for the current agent with filtering. Each result includes the last message, so you can decide what to do without loading the full conversation.
import { Memory } from '@runflow-ai/sdk';

// List all sessions for this agent
const all = await Memory.list();

// List sessions inactive for 4+ hours
const inactive = await Memory.list({
  lastInteractionBefore: new Date(Date.now() - 4 * 60 * 60 * 1000),
});

// List only sessions with a specific status
const inProgress = await Memory.list({
  status: 'in_progress',
});

// Combine filters
const needsFollowUp = await Memory.list({
  lastInteractionBefore: new Date(Date.now() - 4 * 60 * 60 * 1000),
  limit: 50,
});
Each session in the result includes:
FieldTypeDescription
idstringSession ID
entityTypestring?Entity type from identify() (e.g., phone, email)
entityValuestring?Entity value (e.g., +5511999999999)
statusstring?Session status (null = in progress, or qualified, closed, etc.)
updatedAtstringTimestamp of last interaction
messageCountnumberTotal messages in session
summarystring?Auto-generated conversation summary
lastMessageobject?Last message: { role, content, timestamp }

Checking Who Spoke Last

Use lastMessage.role to know if the user or the agent was the last to speak — useful for deciding whether to follow up:
const inactive = await Memory.list({
  lastInteractionBefore: new Date(Date.now() - 4 * 60 * 60 * 1000),
});

for (const session of inactive) {
  // Skip sessions already closed/qualified
  if (session.status) continue;

  // Only follow up if the agent was the last to speak (user didn't respond)
  if (session.lastMessage?.role !== 'assistant') continue;

  await agent.process({
    message: 'Follow up with this lead.',
    entityType: session.entityType,
    entityValue: session.entityValue,
    channel: 'whatsapp',
  });
}
Don’t use identify() in a loop — it sets a global singleton that would get overwritten on each iteration. Pass entityType/entityValue directly in the agent.process() input instead.

Session Status

Mark sessions with a status to track their lifecycle. Status is stored in session metadata — no migration needed.
import { Memory } from '@runflow-ai/sdk';

// Set status on the current session (requires identify() first)
await Memory.setStatus('qualified');
await Memory.setStatus('closed');
await Memory.setStatus('nurturing');
The best pattern is to let tools set the status — the LLM decides when to call them based on the conversation:
import { createTool } from '@runflow-ai/sdk';
import { Memory } from '@runflow-ai/sdk';

const qualifyLeadTool = createTool({
  id: 'qualify-lead',
  description: 'Score the lead based on qualification criteria. This marks the session status.',
  inputSchema: z.object({
    score: z.number().min(0).max(10),
  }),
  execute: async ({ context }) => {
    const status = context.score >= 7 ? 'qualified' : 'nurturing';
    await Memory.setStatus(status);
    return { score: context.score, status };
  },
});

const closeConversationTool = createTool({
  id: 'close-conversation',
  description: 'Close this conversation. Use when lead is not interested or conversation is finished.',
  inputSchema: z.object({
    reason: z.enum(['not_interested', 'wrong_contact', 'completed', 'other']),
  }),
  execute: async ({ context }) => {
    await Memory.setStatus('closed');
    return { closed: true, reason: context.reason };
  },
});
The developer never calls Memory.setStatus() directly — the tools do it. The agent’s instructions guide the LLM to use the right tool at the right time.

Status Lifecycle

New conversation            → status: null (in progress)
Lead qualified (score >= 7) → tool sets "qualified"
Lead scored low (score < 7) → tool sets "nurturing"
Lead not interested         → tool sets "closed"
CRON checks inactive leads  → Memory.list() skips sessions with status
See the SDR Agent with Follow-ups use case for a complete working example using Memory.list(), Memory.setStatus(), and scheduled callbacks.

Custom Summarization

// Agent with custom summarization
const agent = new Agent({
  name: 'Smart Agent',
  model: openai('gpt-4o'),
  memory: {
    summarizeAfter: 30,
    summarizePrompt: `Summarize in 3 bullet points:
- Main issue discussed
- Solution provided
- Next steps`,
    summarizeModel: anthropic('claude-3-haiku'), // Fast & cheap
  },
});

// Manual summarization with custom prompt
const summary = await Memory.summarize({
  prompt: 'Extract only the key decisions from this conversation',
  model: openai('gpt-4o-mini'),
});

Memory Configuration Options

OptionTypeDescription
maxTurnsnumberMaximum conversation turns to keep
maxTokensnumberMaximum tokens to keep
summarizeAfternumberTrigger summary after N turns
summarizePromptstringCustom prompt for summarization
summarizeModelModelProviderCustom model for summarization
memoryKeystringCustom memory key

Next Steps

Context Management

Learn about context management

Tools

Create custom tools