Skip to content

Get started

Last updated View as MarkdownAgent setup

Add Agent Memory to an agent so it can recall durable context across conversations.

This guide uses the Agents SDK to expose memory recall as a model-callable tool. The same pattern applies if you use another agent framework: store memories with ingest() or remember(), expose recall() through one of your agent's tools, and use the system prompt to tell the model when to search memory.

Prerequisites

  1. Sign up for a Cloudflare account ↗︎.
  2. Install Node.js ↗︎.

Node.js version manager

Use Vite+ (vp env) ↗︎ to install and manage Node.js versions.

For supported Node.js versions, refer to the Wrangler system requirements.

You also need access to Agent Memory.

How agent memory works

Use recall() when the model needs relevant memory to answer or act. Use ingest() when you have conversation messages and want Agent Memory to extract durable memories automatically. Use remember() when your agent already knows the exact memory to store.

Do not call ingest() after every model turn. Instead, batch ingestion after the user goes idle, when a conversation is compacted, or at another natural checkpoint.

1. Create a project

Create a Worker project:

npm create cloudflare@latest -- memory-agent

For setup, select the following options:

  • For What would you like to start with?, choose Hello World example.
  • For Which template would you like to use?, choose Worker only.
  • For Which language do you want to use?, choose TypeScript.
  • For Do you want to use git for version control?, choose Yes.
  • For Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?, choose Yes.
  • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).

Move into the project directory:

cd memory-agent

Install the dependencies used by this guide:

npm i agents ai @ai-sdk/provider zod

2. Create a namespace

A namespace scopes the memory profiles for your application. Create one with Wrangler:

npx wrangler agent-memory namespace create my-agent

You will use the namespace name, my-agent, in your Worker binding.

3. Configure bindings

Add an agent_memory binding to your Wrangler configuration, and register your agent's Durable Object.

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "memory-agent",
  "main": "src/server.ts",
  "upload_source_maps": true,
  // Set this to today's date
  "compatibility_date": "2026-11-06",
  "compatibility_flags": [
    "nodejs_compat"
  ],
  "ai": {
    "binding": "AI"
  },
  "agent_memory": [
    {
      "binding": "MEMORY",
      "namespace": "my-agent"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "ChatAgent",
        "class_name": "ChatAgent"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "ChatAgent"
      ]
    }
  ]
}
name = "memory-agent"
main = "src/server.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-11-06"
compatibility_flags = ["nodejs_compat"]

[ai]
binding = "AI"

[[agent_memory]]
binding = "MEMORY"
namespace = "my-agent"

[[durable_objects.bindings]]
name = "ChatAgent"
class_name = "ChatAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatAgent"]

Generate local TypeScript types for your bindings:

npx wrangler types

4. Add memory recall as a tool

The model cannot use memory just because your application has a memory binding. You need to expose recall through a tool and instruct the model when to call it.

This guide builds the agent on a Durable Object with the Agents SDK Lifecycle. The Sessions capability stores the conversation. agents/sessions is experimental, so its API may change between releases.

Create src/server.ts. It defines the system prompt and a search_memory tool that calls recall():

src/server.jsjs
import { DurableObject } from "cloudflare:workers";
import { tool } from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";
import { Sessions } from "agents/sessions";

const SYSTEM_PROMPT = `You are a helpful assistant.

Long-term memory is available through the search_memory tool.

MEMORY POLICY
- Search memory with search_memory when the user asks what you know or remember about them.
- Search memory when the request depends on prior sessions, preferences, project state, conventions, decisions, or long-running tasks.
- Phrase memory searches as concise topics, not questions.
- Do not search memory to repeat something the user just said in the current conversation.
- When search_memory returns results, always incorporate them into your response. The results are real memories from previous conversations.
- Treat recalled memories as helpful context, not guaranteed truth. If a memory is important for an irreversible action, confirm with the user.`;

const MEMORY_PROFILE_NAME = "demo-user";

export class ChatAgent extends DurableObject {
	sessions = new Sessions();
	lifecycle = Lifecycle.install(this).use(this.sessions);
	session = this.sessions.session();

	memoryTools() {
		return {
			search_memory: tool({
				description:
					"Search durable memory: facts, events, instructions, and tasks from prior conversations.",
				inputSchema: z.object({
					query: z.string().describe("A concise topic to search for"),
				}),
				execute: async ({ query }) => {
					const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
					const { answer } = await profile.recall(query, {
						responseLength: "short",
					});
					return answer || "No relevant memories found.";
				},
			}),
		};
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
src/server.tsts
import { DurableObject } from "cloudflare:workers";
import { tool } from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";
import { Sessions } from "agents/sessions";

const SYSTEM_PROMPT = `You are a helpful assistant.

Long-term memory is available through the search_memory tool.

MEMORY POLICY
- Search memory with search_memory when the user asks what you know or remember about them.
- Search memory when the request depends on prior sessions, preferences, project state, conventions, decisions, or long-running tasks.
- Phrase memory searches as concise topics, not questions.
- Do not search memory to repeat something the user just said in the current conversation.
- When search_memory returns results, always incorporate them into your response. The results are real memories from previous conversations.
- Treat recalled memories as helpful context, not guaranteed truth. If a memory is important for an irreversible action, confirm with the user.`;

const MEMORY_PROFILE_NAME = "demo-user";

export class ChatAgent extends DurableObject<Env> {
	readonly sessions = new Sessions();
	readonly lifecycle = Lifecycle.install(this).use(this.sessions);
	readonly session = this.sessions.session();

	memoryTools() {
		return {
			search_memory: tool({
				description:
					"Search durable memory: facts, events, instructions, and tasks from prior conversations.",
				inputSchema: z.object({
					query: z.string().describe("A concise topic to search for"),
				}),
				execute: async ({ query }) => {
					const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
					const { answer } = await profile.recall(query, {
						responseLength: "short",
					});
					return answer || "No relevant memories found.";
				},
			}),
		};
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

The system prompt is as important as the tool. It tells the model when to call search_memory, when not to call it, and how to treat recalled memory. The prompt is the same on every turn, so the model provider can keep it in its prompt cache.

5. Extract memories from conversation

Next, give your agent a way to add durable memories. In a chat agent, the usual path is to store the conversation in a session, then call ingest() after the user goes idle. The Scheduler capability runs the ingest later, even if the Durable Object is evicted in the meantime.

Replace the imports at the top of src/server.ts:

src/server.jsjs
import { DurableObject } from "cloudflare:workers";
import { convertToModelMessages, generateText, stepCountIs, tool } from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/ai-sdk";
import { routeAgentRequest } from "agents/routing";
import { Scheduler } from "agents/schedules";
import { Sessions } from "agents/sessions";
src/server.tsts
import { DurableObject } from "cloudflare:workers";
import {
	convertToModelMessages,
	generateText,
	stepCountIs,
	tool,
	type UIMessage,
} from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/ai-sdk";
import { routeAgentRequest } from "agents/routing";
import { Scheduler } from "agents/schedules";
import { Sessions } from "agents/sessions";

Add the ingestion delay near the top of the file, after the imports:

src/server.jsjs
const MEMORY_INGEST_DELAY_SECONDS = 10;
src/server.tsts
const MEMORY_INGEST_DELAY_SECONDS = 10;

Then update ChatAgent with the following shape. The comment marks where to keep memoryTools() from step 4.

src/server.jsjs
export class ChatAgent extends DurableObject {
	sessions = new Sessions();
	scheduler = new Scheduler({
		callbacks: {
			ingestMemory: () => this.runIngest(),
		},
	});
	lifecycle = Lifecycle.install(this).use(this.sessions).use(this.scheduler);
	session = this.sessions.session();

	// Keep `memoryTools()` from step 4 here.

	async chat(message) {
		// RPC calls do not start the Lifecycle on their own.
		await this.lifecycle.start();

		await this.session.appendMessage({
			id: `user-${crypto.randomUUID()}`,
			role: "user",
			parts: [{ type: "text", text: message }],
		});

		await this.scheduleIngest();

		const ai = createAI({ binding: this.env.AI });
		const result = await generateText({
			model: ai("@cf/zai-org/glm-5.3-flash"),
			system: SYSTEM_PROMPT,
			messages: await convertToModelMessages(await this.session.getHistory()),
			tools: this.memoryTools(),
			stopWhen: stepCountIs(5),
		});

		await this.session.appendMessage({
			id: `assistant-${crypto.randomUUID()}`,
			role: "assistant",
			parts: [{ type: "text", text: result.text }],
		});

		return result.text;
	}

	async scheduleIngest() {
		// Replace any pending ingest, so it runs once the user goes idle.
		for (const schedule of await this.scheduler.list()) {
			if (schedule.callback === "ingestMemory") {
				await this.scheduler.cancel(schedule.id);
			}
		}
		await this.scheduler.set(MEMORY_INGEST_DELAY_SECONDS, "ingestMemory");
	}

	async runIngest() {
		const cursor = (await this.ctx.storage.get("ingestCursor")) ?? 0;
		const history = await this.session.getHistory();
		const messages = history
			.slice(cursor)
			.filter(
				(message) => message.role === "user" || message.role === "assistant",
			)
			.map((message) => ({
				role: message.role,
				content: message.parts
					.map((part) =>
						part.type === "text" && typeof part.text === "string"
							? part.text
							: "",
					)
					.filter(Boolean)
					.join("\n\n"),
			}))
			.filter((message) => message.content);

		if (messages.length > 0) {
			const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
			await profile.ingest(messages, { sessionId: this.lifecycle.name });
		}

		await this.ctx.storage.put("ingestCursor", history.length);
		return { ingested: messages.length };
	}
}
src/server.tsts
export class ChatAgent extends DurableObject<Env> {
	readonly sessions = new Sessions();
	readonly scheduler = new Scheduler({
		callbacks: {
			ingestMemory: () => this.runIngest(),
		},
	});
	readonly lifecycle = Lifecycle.install(this)
		.use(this.sessions)
		.use(this.scheduler);
	readonly session = this.sessions.session();

	// Keep `memoryTools()` from step 4 here.

	async chat(message: string): Promise<string> {
		// RPC calls do not start the Lifecycle on their own.
		await this.lifecycle.start();

		await this.session.appendMessage({
			id: `user-${crypto.randomUUID()}`,
			role: "user",
			parts: [{ type: "text", text: message }],
		});

		await this.scheduleIngest();

		const ai = createAI({ binding: this.env.AI });
		const result = await generateText({
			model: ai("@cf/zai-org/glm-5.3-flash"),
			system: SYSTEM_PROMPT,
			messages: await convertToModelMessages(
				(await this.session.getHistory()) as UIMessage[],
			),
			tools: this.memoryTools(),
			stopWhen: stepCountIs(5),
		});

		await this.session.appendMessage({
			id: `assistant-${crypto.randomUUID()}`,
			role: "assistant",
			parts: [{ type: "text", text: result.text }],
		});

		return result.text;
	}

	private async scheduleIngest() {
		// Replace any pending ingest, so it runs once the user goes idle.
		for (const schedule of await this.scheduler.list()) {
			if (schedule.callback === "ingestMemory") {
				await this.scheduler.cancel(schedule.id);
			}
		}
		await this.scheduler.set(MEMORY_INGEST_DELAY_SECONDS, "ingestMemory");
	}

	private async runIngest(): Promise<{ ingested: number }> {
		const cursor = (await this.ctx.storage.get<number>("ingestCursor")) ?? 0;
		const history = await this.session.getHistory();
		const messages = history
			.slice(cursor)
			.filter(
				(message) => message.role === "user" || message.role === "assistant",
			)
			.map((message) => ({
				role: message.role as "user" | "assistant",
				content: message.parts
					.map((part) =>
						part.type === "text" && typeof part.text === "string"
							? part.text
							: "",
					)
					.filter(Boolean)
					.join("\n\n"),
			}))
			.filter((message) => message.content);

		if (messages.length > 0) {
			const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
			await profile.ingest(messages, { sessionId: this.lifecycle.name });
		}

		await this.ctx.storage.put("ingestCursor", history.length);
		return { ingested: messages.length };
	}
}

Replace the default export with a small test endpoint. Each conversationId maps to a separate Durable Object with its own session history. The Worker calls chat() over RPC:

src/server.jsjs
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (request.method === "POST" && url.pathname === "/chat") {
			const { message, conversationId = "default" } = await request.json();

			if (!message) {
				return Response.json({ error: "Missing message" }, { status: 400 });
			}

			const agent = env.ChatAgent.getByName(conversationId);
			const response = await agent.chat(message);
			return Response.json({ response });
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
src/server.tsts
export default {
	async fetch(request: Request, env: Env) {
		const url = new URL(request.url);

		if (request.method === "POST" && url.pathname === "/chat") {
			const { message, conversationId = "default" } =
				(await request.json()) as {
					message?: string;
					conversationId?: string;
				};
			if (!message) {
				return Response.json({ error: "Missing message" }, { status: 400 });
			}

			const agent = env.ChatAgent.getByName(conversationId);
			const response = await agent.chat(message);
			return Response.json({ response });
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

The ingestion path is scheduleIngest() and runIngest(). Each user message cancels the previous pending ingest and schedules a new one, so Agent Memory processes the conversation after the user goes idle instead of after every agent turn.

runIngest() keeps a cursor in the Durable Object's storage, so each batch only includes messages that have not already been ingested. The sessionId groups memories by conversation inside the shared memory profile.

This demo uses one Agent Memory profile, demo-user, across multiple conversations. In production, choose profile names that match your application scope, such as users, teams, tenants, or organizations.

You can also run the ingestion logic when a session is compacted. The important constraint is to ingest in batches at natural checkpoints, not after every agent turn. In production, choose an ingest delay that matches your application's user experience.

6. (Optional) Store explicit memories when needed

Automatic ingestion is enough for most apps. If you want the model to store a specific memory immediately, add a server-side tool whose execute function calls remember().

Use this when the agent already knows the exact memory to store. For example, the model might call a rememberMemory tool after the user says: "Remember that I prefer concise answers."

If the model can call a memory-write tool, add system prompt instructions that define what is worth remembering and when to ask for confirmation. For many agents, automatic conversation ingestion is simpler and safer than giving the model a direct memory-write tool.

7. Test the app

Start local development:

npx wrangler dev

Ask the first conversation to remember a durable preference:

curl -X POST "http://localhost:8787/chat" \
  -H "Content-Type: application/json" \
  -d '{"conversationId":"first-chat","message":"I prefer TypeScript examples and concise answers."}'

Wait at least 30 seconds before sending the next request. The code schedules ingestion to run 10 seconds after the user goes idle, and Agent Memory then needs additional time to extract, classify, and index the memories before they are available for recall.

Ask a different conversation a question that depends on durable memory. This request uses a different session history but the same Agent Memory profile.

curl -X POST "http://localhost:8787/chat" \
  -H "Content-Type: application/json" \
  -d '{"conversationId":"second-chat","message":"What do you know or remember about me and my preferences?"}'

The model should call search_memory, receive recalled memory from Agent Memory, and use that context in its response. The second conversation has no shared session history with the first, so any knowledge of user preferences comes from Agent Memory.

Next steps

Workers API

Use `ingest()`, `remember()`, `recall()`, and `getSummary()` from Workers.

Was this helpful?