> This page is for version v4 (default).
> For other versions, use one of these documentation indexes:
> - v4 (default): https://docs-beta.getzep.com/v4/llms.txt
> - v3: https://docs-beta.getzep.com/v3/llms.txt
> - v2: https://docs-beta.getzep.com/v2/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs-beta.getzep.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-beta.getzep.com/_mcp/server.

# Mastra integration

> **Note**
>
> The v4 version of `@getzep/zep-mastra` is not released yet. The current `@getzep/zep-mastra` release uses the v3 API. To use this integration now, follow the [v3 version of this page](/v3/mastra-memory).

> **Keep retrieved context out of privileged instructions**
>
> Zep context can include content that your users, documents, or tools supplied. A system or developer message gives that content higher instruction priority than ordinary input. Some convenience integrations use system-message injection. Use direct SDK retrieval or an actual retrieval tool call unless all stored content is application-authored and trusted. Follow [Memory security best practices](/memory-security) for provider-specific placement.

> **Build an agent with Zep tools**
>
> To build an agent that plans its retrieval and uses several Zep tools, read [Build an Agent with Zep](/build-an-agent-with-zep). The guide shows how to add domain knowledge, design tools, and evaluate the agent.

[Mastra](https://mastra.ai) agents using Zep gain long-term memory backed by a temporal knowledge graph. The `@getzep/zep-mastra` package provides two complementary surfaces:

* **Automatic memory for trusted content** — `createZepProcessors` builds a `ZepInputProcessor`/`ZepOutputProcessor` pair that plugs into Mastra's native `inputProcessors`/`outputProcessors` pipeline. The input processor inserts context into a system message.
* **Tools** — `createZepToolset` builds `zepRemember`/`zepSearch`/`zepContext` tools that let the model decide when to persist or recall. Use retrieval tools for end-user or third-party context.

## Core benefits

* **Automatic memory loop for trusted content**: Processors can inject application-authored context and persist each completed turn
* **Model-in-the-loop tools**: `zepRemember`, `zepSearch`, and `zepContext` drop straight into an `Agent`'s `tools` record
* **Per-call identity**: Resolve `graphUuid`/`threadUuid` from Mastra's `requestContext` so one processor or tool instance serves many end users
* **User and shared Context Graphs**: Bind to a user's personal graph or a shared knowledge base
* **Graceful degradation**: A Zep outage is logged and surfaced as a non-fatal result — it never crashes the host agent

## How it works

The processors sit on opposite sides of the model call:

1. **`ZepInputProcessor`** runs before the model is called. It extracts the latest user message, retrieves a Zep context block (`thread.getContext`, or a custom `contextBuilder`), wraps it with `contextTemplate`/`formatContext`, and injects it as a system message.
2. **`ZepOutputProcessor`** runs after the model responds. It persists the completed turn — the latest user message plus the assistant's response — to the bound thread via a single `thread.addMessages` call. The assistant text persisted is the final step's text; when generation ends mid-tool-loop (`finishReason === "tool-calls"`), the user message is still persisted.

Because injection and persistence sit on opposite sides of the model call, it's safe to enable both processors together — they don't interfere with each other. Every Zep call is wrapped: a missing `threadUuid` or any Zep failure degrades gracefully — messages pass through unchanged, a warning is logged — and the input processor never calls `abort()` or throws into the agent loop.

Zep is a temporal knowledge graph, not a row-oriented message store, so the package exposes Zep's two real operations — persist and retrieve — through processors and tools rather than a `MastraStorage` adapter, which would require CRUD operations a temporal knowledge graph can't honor faithfully.

## Installation

```bash
npm install @getzep/zep-mastra @getzep/zep-cloud@preview @mastra/core
```

> **Info**
>
> Requires Node.js 20+, `@mastra/core>=1.42.0` (peer), `@getzep/zep-cloud`, and a Zep Cloud API key. Get your API key from [app.getzep.com](https://app.getzep.com).

Set up your environment variables:

```bash
export ZEP_API_KEY="your-zep-api-key"
export OPENAI_API_KEY="your-openai-api-key"
```

## Identifiers

Zep v4 addresses every user, thread, and graph by a server-generated UUID. A create call accepts no client-chosen identifier, so a new user, thread, or graph has no name.

* `createZepUserAndThread` returns `userUuid`, `graphUuid`, and `threadUuid`. Store all three in your own database.
* The processors take `graphUuid` and `threadUuid`. The tools take them on a `ZepBinding`.
* The integration never calls `lookup` at run time. Resolve a v3 name to its UUID one time, then reuse the UUID.

#### Upgrading from @getzep/zep-mastra 0.2.0

The package now targets the Zep v4 SDK, and the public API takes UUIDs:

* `ensureZepUserAndThread` is replaced by `createZepUserAndThread`, which returns `{ userUuid, graphUuid, threadUuid }` or `null`. Zep v4 has no name-addressed create, so the function is not idempotent on a name.
* `ZepBinding` takes `graphUuid` in place of `userId`/`graphId`, and `ZepThreadBinding` takes `threadUuid` in place of `threadId`.
* The processors take `graphUuid` and `threadUuid`, and `ResolvedZepIdentity` returns the same two fields.
* `templateId` becomes `templateUuid`, and `searchFilters` becomes `filters`.

See the [CHANGELOG](https://github.com/getzep/zep/blob/main/integrations/mastra/typescript/CHANGELOG.md) for the full migration notes.

## Automatic memory for trusted deployments

`createZepProcessors` inserts retrieved context into a system message. Use this processor pair only when all stored content is fully trusted and application-authored:

```typescript
import { ZepClient } from "@getzep/zep-cloud";
import { Agent } from "@mastra/core/agent";
import { createZepProcessors, createZepUserAndThread } from "@getzep/zep-mastra";

const client = new ZepClient({ apiKey: process.env.ZEP_API_KEY! });

// 1. Create the Zep user + thread before the first turn. Zep generates the UUIDs.
const identity = await createZepUserAndThread({
  client,
  firstName: "Jane",
  lastName: "Smith",
});
if (!identity) throw new Error("Could not create the Zep user and thread.");

// 2. Trusted-only path: the input processor inserts context into a system message.
const { inputProcessor, outputProcessor } = createZepProcessors({
  client,
  graphUuid: identity.graphUuid,
  threadUuid: identity.threadUuid,
});

// 3. Attach to a Mastra agent (id and name are both required by Mastra).
const agent = new Agent({
  id: "memory-agent",
  name: "Memory Agent",
  instructions: "You have long-term memory about the user. Use it to personalize replies.",
  model: "openai/gpt-5.6-terra",
  inputProcessors: [inputProcessor],
  outputProcessors: [outputProcessor],
});
```

### Customizing context injection

By default the input processor retrieves the context block with `thread.getContext` and wraps it in `DEFAULT_CONTEXT_TEMPLATE` — the canonical `<ZEP_CONTEXT>` wrapper shared by Zep's framework integrations. Three options change that, in increasing order of control:

```typescript
const { inputProcessor, outputProcessor } = createZepProcessors({
  client,
  graphUuid,
  threadUuid,
  // Replace thread.getContext with your own retrieval:
  contextBuilder: async ({ client, graphUuid, userMessage }) => {
    if (!graphUuid) return undefined;
    const result = await client.graph.searchEdges(graphUuid, {
      body: { query: userMessage },
    });
    return result.data.map((e) => e.fact).join("\n");
  },
  // Or just customize the wrapping template (must contain a literal `{context}`):
  contextTemplate: "Known facts about the user:\n{context}",
  // Or fully take over formatting (wins over contextTemplate):
  formatContext: (context) => `<memory>${context}</memory>`,
});
```

* **`contextBuilder`** replaces the default retrieval with your own async function. It receives a `ZepContextBuilderInput` — the client, the resolved `graphUuid`/`threadUuid`, and the latest user message — and returns the context string (or `undefined` to inject nothing for that turn). The result still passes through the template or `formatContext`.
* **`contextTemplate`** customizes the wrapping text. It must contain a literal `{context}` placeholder, replaced via literal string replacement (not a format string), so braces, `%`, and `$` in the retrieved context are safe.
* **`formatContext`** takes over formatting entirely and wins over `contextTemplate`.

### Per-call identity

Pass `resolveIdentity` (a `ZepIdentityResolver`, sync or async) to resolve `graphUuid`/`threadUuid` per call from Mastra's `requestContext` instead of binding a fixed identity at construction time — useful when a single processor instance serves many end users:

```typescript
const { inputProcessor, outputProcessor } = createZepProcessors({
  client,
  resolveIdentity: (requestContext) => ({
    graphUuid: (requestContext as { graphUuid?: string } | undefined)?.graphUuid,
    threadUuid: (requestContext as { threadUuid?: string } | undefined)?.threadUuid,
  }),
});
```

The same `resolveIdentity` option is accepted by `createZepSearchTool`, `createZepRememberTool`, and `createZepContextTool` (resolved from each tool call's `context.requestContext`), and `createZepToolset` forwards it to all three tools.

If both a fixed `graphUuid`/`threadUuid` and `resolveIdentity` are set, `resolveIdentity`'s result wins for whichever fields it returns; any field it omits or resolves to `undefined` falls back to the constructor-bound value.

## Provisioning with createZepUserAndThread

Zep requires the user and thread to exist before messages are added. Call `createZepUserAndThread` once, out-of-band, before the first turn, then store the returned UUIDs. Zep v4 addresses every later call by UUID, so this function is not idempotent on a name: a second call creates a second user. A failure (auth, network, 5xx) is logged at `warn` and reported as `null`, never thrown.

Pass `onUserCreated` (a `ZepUserCreatedHook`) to run one-time setup — per-user ontology, custom instructions, seeding — immediately after the user is created. The hook receives the `userUuid`:

```typescript
const identity = await createZepUserAndThread({
  client,
  firstName: "Jane",
  lastName: "Smith",
  email: "jane@example.com",
  // Runs exactly once, immediately after the user is created —
  // e.g. configure per-user summary instructions:
  onUserCreated: async (client, userUuid) => {
    await client.user.setSummaryInstructions(userUuid, {
      instructions: [{ name: "diet", text: "Track the user's dietary preferences." }],
    });
  },
});
// identity: { userUuid, graphUuid, threadUuid } | null
```

The function sends no client-chosen identifier. The v4 create endpoints reject a `userId` or a `threadId`, and the returned UUIDs are the only addresses.

## Tools

The toolset puts the model in the loop: the agent calls a tool when it decides to persist or recall. Use it standalone or alongside the processors.

```typescript
import { ZepClient } from "@getzep/zep-cloud";
import { Agent } from "@mastra/core/agent";
import { createZepToolset, createZepUserAndThread } from "@getzep/zep-mastra";

const client = new ZepClient({ apiKey: process.env.ZEP_API_KEY! });

// 1. Create the Zep user + thread before the first turn.
const identity = await createZepUserAndThread({ client, firstName: "Jane", lastName: "Smith" });
if (!identity) throw new Error("Could not create the Zep user and thread.");

// 2. Build the tool set bound to that graph + thread.
const binding = { graphUuid: identity.graphUuid, threadUuid: identity.threadUuid };
const { zepRemember, zepSearch, zepContext } = createZepToolset({ client, binding });

// 3. Attach the tools to an Agent (id and name are both required by Mastra).
const agent = new Agent({
  id: "memory-agent",
  name: "Memory Agent",
  instructions: "You have long-term memory. Store and recall user facts.",
  model: "openai/gpt-5.6-terra",
  tools: { zepRemember, zepSearch, zepContext },
});
```

The toolset provides three tools:

| Tool          | Zep operation                                   | What it does                                                                                                                                                            |
| ------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zepRemember` | `thread.addMessages` / `graph.episode.add`      | Persists a message via `thread.addMessages` only when a `role` and a `threadUuid` are both present; otherwise the content is ingested as a fact via `graph.episode.add` |
| `zepSearch`   | `graph.searchEdges` and the other scope methods | Model-callable search over the bound graph; each search parameter can be exposed to the model, pinned, or hidden                                                        |
| `zepContext`  | `thread.getContext`                             | Returns the prompt-ready context block assembled from the whole user graph                                                                                              |

Each tool is also exported as a standalone factory (`createZepRememberTool`, `createZepSearchTool`, `createZepContextTool`) for wiring a single tool with custom options.

Each tool has a typed input and output schema:

| Tool          | Input                                                                                                                       | Output                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `zepRemember` | `content` (string); optional `role`; optional `name`                                                                        | `{ stored: boolean, message: string }` |
| `zepSearch`   | `query` (string, 1–400 chars); optional `scope`, `reranker`, `limit`, `mmrLambda`, `centerNodeUuid` unless pinned or hidden | `{ facts: string[], found: boolean }`  |
| `zepContext`  | none                                                                                                                        | `{ context: string, found: boolean }`  |

`zepSearch` returns `facts` as extracted strings tailored to the search scope — edge facts, `"name: summary"` for entities, episode content, and so on — with `found` set to `true` when the result is non-empty.

### Pin-or-expose search parameters

`createZepSearchTool` exposes each search parameter to the model by default, alongside the always-required `query`, so the model can tune its own searches per call:

| Parameter        | Model-visible values                                                     | Default            |
| ---------------- | ------------------------------------------------------------------------ | ------------------ |
| `scope`          | `edges`, `nodes`, `episodes`, `observations`, `thread_summaries`, `auto` | `edges`            |
| `reranker`       | `rrf`, `mmr`, `node_distance`, `episode_mentions`, `cross_encoder`       | `rrf`              |
| `limit`          | number (values above 50 are clamped to 50)                               | `10`               |
| `mmrLambda`      | number                                                                   | Zep server default |
| `centerNodeUuid` | string                                                                   | Zep server default |

Each parameter is independently tri-state at construction time (`ZepSearchPinnableParams`):

* **`pinnedParams`** fixes a parameter to a constant value: hidden from the model's schema, always sent.
* **`hiddenParams`** removes a parameter from the schema *without* pinning it: omitted from the search call entirely, so Zep's own server default applies.
* **Omitted from both** — exposed to the model with the documented default.

```typescript
// Model only ever sees `query`; scope/reranker/limit are fixed.
createZepSearchTool({
  client,
  binding: { graphUuid },
  pinnedParams: { scope: "edges", reranker: "rrf", limit: 10 },
});

// Hide mmrLambda/centerNodeUuid from the schema without fixing a value.
createZepSearchTool({
  client,
  binding: { graphUuid },
  hiddenParams: new Set(["mmrLambda", "centerNodeUuid"]),
});
```

`filters` and `bfsOriginNodeUuids` are always constructor-only — never exposed to the model — and applied whenever set. The legacy `scope`/`reranker`/`limit` constructor arguments pin (and hide) their parameter, equivalent to the corresponding `pinnedParams` entry.

## Binding: user graph vs shared Context Graph

Zep v4 addresses every graph by its server-generated UUID, so a user graph and a standalone graph are the same kind of address. Tools and processors are bound with `graphUuid`, and the thread-scoped surfaces are bound with `threadUuid`:

* The `graphUuid` of a **user graph** is the `graphUuid` field of the `User` that `user.create` returns. A user graph is the home for personalized agent memory.
* The `graphUuid` of a **shared Context Graph** is the `uuid` field of the `Graph` that `graph.create` returns. A shared Context Graph holds shared or domain knowledge such as a product knowledge base or runbooks. It has no user node and no user summary.
* The `threadUuid` is the `uuid` field of the `Thread` that `thread.create` returns. Context retrieval and the `zepContext` tool need it. The thread scopes relevance; retrieval still spans the whole user graph.

If no `graphUuid` or no `threadUuid` can be resolved, tools and processors degrade gracefully instead of throwing.

## Roles

`zepRemember` accepts an arbitrary `role` string and maps it onto Zep's closed `RoleType` enum: `user`, `assistant`, `system`, `tool`, or `function`. Host-framework role names like `human` or `ai` are coerced safely; an unknown role is omitted. The mapper is exported as `toRoleType`.

## Best practices

* **Use tools for untrusted context.** Use processors only when all stored content is application-authored and trusted.
* **Call `createZepUserAndThread` once** before the first turn, store the returned UUIDs, then reuse a single `ZepClient`
* **Pass real names** so Zep can anchor the user's identity node in the graph
* **Don't read-after-write within a turn** — Zep builds the graph asynchronously, so a just-stored fact is not instantly retrievable
* **Pass a custom `logger`** to route Zep warnings into your logging stack

## Next steps

* Explore [customizing graph structure](/customizing-graph-structure) for advanced knowledge organization
* Learn about [searching the graph](/searching-the-graph) and how to tune search
* See [code examples](https://github.com/getzep/zep/tree/main/integrations/mastra/typescript/examples) for additional patterns