> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs-beta.getzep.com/v4/migrating-from-v3/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-beta.getzep.com/_mcp/server. # Migrating from v3 > A method-by-method map from the Zep v3 SDK to v4, plus the behavior changes that need more than a rename. Most v3 methods have a direct v4 replacement. This page maps all of them for Python, TypeScript, and Go, then covers the changes that need more than a rename. The tables match `zep-cloud` / `@getzep/zep-cloud` / `zep-go` **v3.28.0** and the v4 alpha packages **4.0.0a5** / **4.0.0-alpha.5** / **v4.0.0-alpha.5**. UUID addressing, pagination, and asynchronous deletes account for most of the work in a port. Read [Identifiers](#identifiers-need-to-be-adapted), [Pagination](#pagination), and [Deletes](#deletes) first. ## Identifiers need to be adapted Your existing `user_id`, `thread_id`, and `graph_id` values do not work the way they did in v3. You cannot pass them to `get`, `update`, `delete`, `add_messages`, or any other method that used to accept them. Those methods now take a UUID. If you pass the old identifier, the call fails. Zep generates the UUID of every user, thread, and graph. The client addresses each resource by that UUID. A v4 create call does not accept an identifier from the client: * `user.create` does not accept a `user_id`. * `thread.create` does not accept a `thread_id`. * `graph.create` does not accept a `graph_id`. * `graph.clone` does not accept a `target_graph_id`. A create request that contains one of these fields with a value fails with HTTP 400 and the error code `invalid_request`. A resource that v4 creates has no legacy name. The v4 response for that resource contains no `user_id`, `thread_id`, or `graph_id`. A resource that v3 created keeps the identifier that your application gave it in v3. In v4, that identifier is a legacy name: a read-only field that you cannot change. A legacy name is not an address. Only the transitional `lookup` operations accept a legacy name as input. Two reasons drive the design. Every identifier is an opaque UUID that holds no tenant, no resource type, no order, and no customer data. A customer-supplied value can hold personal data, and a value in a URL path or a query string goes into access logs, proxy logs, referrer headers, and error reports. For the same reason, each `lookup` operation is a `POST` with the identifier in the request body. ### Store the UUID in your own database Every v4 application uses the same pattern: 1. Call `user.create`, `thread.create`, or `graph.create` with no identifier. 2. Read the `uuid` from the response. The `user.create` response also contains the `graph_uuid` of the user graph. 3. Write the UUID into your own table. For example, use `zep_user_uuid` and `zep_graph_uuid` columns on your `users` table, and a `zep_thread_uuid` column on the table that holds conversations. 4. Pass the stored UUID on every later call. Your own identifiers stay in your database, in the same row as the Zep UUID. Zep does not store them, and no Zep operation accepts them. To find the Zep resource for one of your records, read the UUID from that record. If you need a label on a Zep user, put the label in the user `metadata`. A thread has no `metadata` field. A graph has a `name` and a `description`. The graph `name` is descriptive text. It is not an identifier, and no operation addresses a graph by its `name`. [Migrate a production application](#migrate-a-production-application) shows this pattern with code, including safe retries. ### Resolve legacy names with lookup The `lookup` operations resolve a legacy name to a UUID: * `user.lookup` resolves one `user_id`. * `thread.lookup` resolves one `thread_id`. * `graph.lookup` resolves one `graph_id`. * `lookup.batch` resolves up to 1000 identifiers of users, threads, and graphs in one request. Use `lookup` only to migrate identifiers that your application stored in v3. A resource that v4 creates has no legacy name, so `lookup` never finds it. > **Warning** > > Do not call `lookup` in the request path of your application. A lookup before > every read adds a round trip to each request. Resolve each identifier one > time, keep the UUID, and address the resource by UUID. > **Note** > > `lookup` is transitional. It becomes deprecated when the v3 surface retires, > and a later major version removes it. Code that reads a stored UUID needs no > further change. The example below resolves many identifiers in one request. The response has one item for each identifier in the request. An identifier that does not resolve returns `found: false` with no UUID. **`Python`** ```python Python lookup = client.lookup.batch( users=["user_1234", "user_5678"], threads=["thread_1234"], graphs=["support-emea"], ) uuids = { (item.resource_type, item.legacy_id): item.uuid_ for item in lookup.items or [] if item.found } ``` **`TypeScript`** ```typescript TypeScript const lookup = await client.lookup.batch({ users: ["user_1234", "user_5678"], threads: ["thread_1234"], graphs: ["support-emea"], }); const uuids = new Map(); for (const item of lookup.items ?? []) { if (item.found && item.uuid) { uuids.set(`${item.resourceType}:${item.legacyId}`, item.uuid); } } ``` **`Go`** ```go Go lookup, err := client.Lookup.Batch(ctx, &zep.BatchLookupRequest{ Users: []string{"user_1234", "user_5678"}, Threads: []string{"thread_1234"}, Graphs: []string{"support-emea"}, }) if err != nil { return err } uuids := map[string]string{} for _, item := range lookup.Items { if item.Found != nil && *item.Found && item.ResourceType != nil && item.LegacyID != nil && item.UUID != nil { uuids[*item.ResourceType+":"+*item.LegacyID] = *item.UUID } } ``` The map key contains the resource type, because a user and a thread can have the same legacy name. ### Migrate one call The example below resolves one thread and then adds messages to it. The lookup is a one-time migration step. In production code, read the UUID from your own database, as the [production procedure](#migrate-a-production-application) shows. #### Python **v3.** `thread.add_messages` takes your `thread_id`. ```python from zep_cloud.types import Message client.thread.add_messages( thread_id="thread_1234", messages=[ Message(role="user", name="Ada", content="I prefer window seats."), ], ) ``` **v4.** Look up the thread, then pass `uuid_`. Store `uuid_` with your own conversation record, and read it from there on every later call. Use `user.lookup(user_id=...)` and `graph.lookup(graph_id=...)` the same way. ```python from zep_cloud import AddMessage thread = client.thread.lookup(thread_id="thread_1234") client.thread.add_messages( thread.uuid_, messages=[ AddMessage(role="user", name="Ada", content="I prefer window seats."), ], ) ``` #### TypeScript **v3.** `thread.addMessages` takes your `threadId`. ```typescript await client.thread.addMessages("thread_1234", { messages: [ { role: "user", name: "Ada", content: "I prefer window seats." }, ], }); ``` **v4.** Look up the thread, then pass `uuid`. Store `uuid` with your own conversation record, and read it from there on every later call. Use `user.lookup({ userId })` and `graph.lookup({ graphId })` the same way. ```typescript const thread = await client.thread.lookup({ threadId: "thread_1234" }); await client.thread.addMessages(thread.uuid, { messages: [ { role: "user", name: "Ada", content: "I prefer window seats." }, ], }); ``` #### Go **v3.** `Thread.AddMessages` takes your thread ID. ```go _, err := client.Thread.AddMessages(ctx, "thread_1234", &zep.AddThreadMessagesRequest{ Messages: []*zep.Message{ { Role: zep.RoleTypeUser, Name: zep.String("Ada"), Content: "I prefer window seats.", }, }, }) ``` **v4.** Look up the thread, then pass the UUID. `Lookup` returns pointer fields, so dereference them after a nil check. Store the UUID with your own conversation record, and read it from there on every later call. Use `User.Lookup` with `UserID` and `Graph.Lookup` with `GraphID` the same way. ```go thread, err := client.Thread.Lookup(ctx, &zep.LookupRequest{ ThreadID: zep.String("thread_1234"), }) if err != nil { return err } if thread.UUID == nil { return fmt.Errorf("thread thread_1234 has no UUID") } _, err = client.Thread.AddMessages(ctx, *thread.UUID, &zep.AddMessagesRequest{ Messages: []*zep.AddMessage{ { Role: zep.RoleTypeUser.Ptr(), Name: zep.String("Ada"), Content: zep.String("I prefer window seats."), }, }, }) ``` ## Migrate a production application Use this procedure when your application runs on v3 in production and must continue to create users, threads, and graphs during the migration. At the end, your application uses only v4, and it addresses every resource by a UUID that it keeps in its own database. Two facts set the order of the steps: * v3 and v4 serve the same project data at the same time. A resource that v3 creates is available through v4, and v4 addresses it by UUID. * v3 cannot address a resource that v4 creates. v3 addresses users, threads, and graphs by the identifier that your application gave them. A resource that v4 creates has no such identifier. For these reasons, your application moves its reads and writes to v4 first. It moves resource creation to v4 only after no v3 read or write remains. > **Note** > > During steps 2 to 5, your application uses the v3 SDK and the v4 SDK. The Go > SDKs have different module paths (`github.com/getzep/zep-go/v3` and > `github.com/getzep/zep-go/v4`), so one Go module can import both. In > TypeScript, install the v3 SDK under an alias, for example > `npm install zep-cloud-v3@npm:@getzep/zep-cloud@^3.30.0`, and install the v4 > SDK as `@getzep/zep-cloud`. In Python, both SDKs are the `zep-cloud` package, > so one Python environment cannot contain both. Run the v3 calls and the v4 > calls in different processes, for example in two services. #### Add nullable UUID columns **Purpose:** Give each of your records a place for the Zep UUID. Add a nullable column for the Zep UUID to each table that refers to a Zep resource. For example: * Add `zep_user_uuid` and `zep_graph_uuid` to your `users` table. * Add `zep_thread_uuid` to the table that holds conversations. * Add `zep_graph_uuid` to each table that refers to a standalone graph. * Add `zep_lookup_status` to each table that the backfill reads. The backfill writes `'not_found'` into this column and skips those rows later. Keep your own identifier columns. They stay the key of your records. The columns are nullable because the existing rows have no UUID yet. Step 3 fills them. **Finish this step when** the columns exist in production and your application runs with the new schema. #### Capture the UUID of every new resource **Purpose:** Stop the number of rows without a UUID from growing. Keep the v3 create calls, and keep your v3 identifiers for now. Change each create call to write the UUID from the v3 response into the new column. The v3 response already contains the UUID that v4 uses, so no lookup is necessary. Use v3 SDK version 3.30.0 or later, which exposes the graph fields below. | v3 create call | Value to store | Column | | --------------- | ---------------------- | -------------------------------- | | `user.add` | `uuid` | `zep_user_uuid` | | `user.add` | `graph_uuid` | `zep_graph_uuid` on the user row | | `thread.create` | `uuid` | `zep_thread_uuid` | | `graph.create` | `canonical_graph_uuid` | `zep_graph_uuid` | > **Warning** > > Do not store the v3 `Graph.uuid`. That value identifies an internal group > record, and v4 does not accept it as a graph UUID. v4 addresses a graph > by the value that v3 returns as `canonical_graph_uuid`. User and thread > UUIDs have the same value in v3 and v4. A v3 `graph.clone` response contains the identifiers of the target, not a UUID. After a clone into a standalone graph, call `graph.get` on v3 with the target `graph_id`, and store its `canonical_graph_uuid`. After a clone into a user graph, call `user.get` on v3 with the target `user_id`, and store its `graph_uuid`. The graph fields are empty when the graph record does not exist yet. In that case, write `NULL`. The backfill in step 3 fills the value. **`Python`** ```python Python from zep_cloud.client import Zep # zep-cloud 3.30.0 or later (v3) zep_v3 = Zep(api_key=ZEP_API_KEY) def create_user(db, user_id: str, email: str) -> None: user = zep_v3.user.add(user_id=user_id, email=email) db.execute( "UPDATE users SET zep_user_uuid = %s, zep_graph_uuid = %s WHERE id = %s", (user.uuid_, user.graph_uuid, user_id), ) def create_conversation(db, conversation_id: str, user_id: str) -> None: thread = zep_v3.thread.create(thread_id=conversation_id, user_id=user_id) db.execute( "UPDATE conversations SET zep_thread_uuid = %s WHERE id = %s", (thread.uuid_, conversation_id), ) def create_team_graph(db, team_id: str) -> None: graph = zep_v3.graph.create(graph_id=team_id, name="Team knowledge") db.execute( "UPDATE teams SET zep_graph_uuid = %s WHERE id = %s", (graph.canonical_graph_uuid, team_id), ) ``` **`TypeScript`** ```typescript TypeScript import { ZepClient as ZepV3Client } from "zep-cloud-v3"; // v3.30.0 or later const zepV3 = new ZepV3Client({ apiKey: ZEP_API_KEY }); async function createUser(userId: string, email: string) { const user = await zepV3.user.add({ userId, email }); await db.query( "UPDATE users SET zep_user_uuid = $1, zep_graph_uuid = $2 WHERE id = $3", [user.uuid ?? null, user.graphUuid ?? null, userId], ); } async function createConversation(conversationId: string, userId: string) { const thread = await zepV3.thread.create({ threadId: conversationId, userId }); await db.query( "UPDATE conversations SET zep_thread_uuid = $1 WHERE id = $2", [thread.uuid ?? null, conversationId], ); } async function createTeamGraph(teamId: string) { const graph = await zepV3.graph.create({ graphId: teamId, name: "Team knowledge" }); await db.query( "UPDATE teams SET zep_graph_uuid = $1 WHERE id = $2", [graph.canonicalGraphUuid ?? null, teamId], ); } ``` **`Go`** ```go Go import ( zepv3 "github.com/getzep/zep-go/v3" // v3.30.0 or later zepv3client "github.com/getzep/zep-go/v3/client" ) func createUser(ctx context.Context, zepV3 *zepv3client.Client, db *sql.DB, userID, email string) error { user, err := zepV3.User.Add(ctx, &zepv3.CreateUserRequest{UserID: userID, Email: &email}) if err != nil { return err } _, err = db.ExecContext(ctx, "UPDATE users SET zep_user_uuid = $1, zep_graph_uuid = $2 WHERE id = $3", user.UUID, user.GraphUUID, userID) return err } func createConversation(ctx context.Context, zepV3 *zepv3client.Client, db *sql.DB, conversationID, userID string) error { thread, err := zepV3.Thread.Create(ctx, &zepv3.CreateThreadRequest{ThreadID: conversationID, UserID: userID}) if err != nil { return err } _, err = db.ExecContext(ctx, "UPDATE conversations SET zep_thread_uuid = $1 WHERE id = $2", thread.UUID, conversationID) return err } func createTeamGraph(ctx context.Context, zepV3 *zepv3client.Client, db *sql.DB, teamID string) error { graph, err := zepV3.Graph.Create(ctx, &zepv3.CreateGraphRequest{GraphID: teamID, Name: zepv3.String("Team knowledge")}) if err != nil { return err } _, err = db.ExecContext(ctx, "UPDATE teams SET zep_graph_uuid = $1 WHERE id = $2", graph.CanonicalGraphUUID, teamID) return err } ``` **Finish this step when** the new code runs on every instance of your application. From this point, each new row gets its UUID at creation time. #### Backfill the existing rows **Purpose:** Give every row that existed before step 2 its UUID. Run a job outside your request path. The job uses the v4 SDK: 1. Read up to 1000 rows that have no UUID. Order the rows by your own identifier, and continue after the last identifier of the previous page. 2. Call `lookup.batch` with the identifiers of those rows. One request accepts up to 1000 identifiers in total across `users`, `threads`, and `graphs`. Do not put the same identifier two times in one list. 3. For each item with `found: true`, write the `uuid` into the row. Write only when the column is still `NULL`, so that the job never changes a value that step 2 wrote. 4. For each item with `found: false`, record the row as not found, for example in a `zep_lookup_status` column. The job does not read these rows again. 5. Continue with the next page until no row remains. `lookup.batch` returns only the UUID of each user. To fill `zep_graph_uuid` on the `users` table, call `user.get` on v4 for each user that has a `zep_user_uuid` and no `zep_graph_uuid`, and store the `graph_uuid` of the response. For graphs, `lookup.batch` returns the UUID that v4 uses. The job is safe to run again. It reads only rows without a UUID and rows that are not marked as not found. It writes only to empty columns. If the job stops, start it again from the beginning. An item with `found: false` means that the identifier does not resolve for the API key. The resource does not exist in the project of the key, the resource was deleted, or the key cannot read the resource. Examine each of these rows. Then create the resource again with the code from step 2, or remove the reference to Zep from the row. Send one lookup request at a time. When Zep limits the request rate, a request fails with HTTP 429, the error code `rate_limited`, and a `Retry-After` header that gives the number of seconds to wait. The SDKs retry HTTP 429 automatically. If the error still reaches your job, wait the number of seconds in `Retry-After`, and send the same request again. A request that fails with HTTP 403 `permission_denied` or HTTP 503 `service_unavailable` did not resolve any identifier. Do not mark rows as not found for this failure. Stop the job, correct the cause, and run the job again. **`Python`** ```python Python import time from zep_cloud.client import Zep # v4 from zep_cloud.core.api_error import ApiError zep = Zep(api_key=ZEP_API_KEY) PAGE_SIZE = 1000 def backfill_users(db) -> None: last_id = "" while True: rows = db.fetch_all( "SELECT id FROM users" " WHERE zep_user_uuid IS NULL AND zep_lookup_status IS NULL AND id > %s" " ORDER BY id LIMIT %s", (last_id, PAGE_SIZE), ) if not rows: return ids = [row.id for row in rows] result = lookup_with_retry(ids) for item in result.items or []: if item.found: db.execute( "UPDATE users SET zep_user_uuid = %s" " WHERE id = %s AND zep_user_uuid IS NULL", (item.uuid_, item.legacy_id), ) else: db.execute( "UPDATE users SET zep_lookup_status = 'not_found' WHERE id = %s", (item.legacy_id,), ) last_id = ids[-1] def lookup_with_retry(ids: list[str]): while True: try: return zep.lookup.batch(users=ids) except ApiError as err: if err.status_code != 429: raise # 403 and 503 stop the job. No row is marked. time.sleep(int((err.headers or {}).get("retry-after", "1"))) ``` **`TypeScript`** ```typescript TypeScript import { ZepClient, ZepError } from "@getzep/zep-cloud"; // v4 const zep = new ZepClient({ apiKey: ZEP_API_KEY }); const PAGE_SIZE = 1000; async function backfillUsers() { let lastId = ""; for (;;) { const { rows } = await db.query( "SELECT id FROM users" + " WHERE zep_user_uuid IS NULL AND zep_lookup_status IS NULL AND id > $1" + " ORDER BY id LIMIT $2", [lastId, PAGE_SIZE], ); if (rows.length === 0) return; const ids: string[] = rows.map((row) => row.id); const result = await lookupWithRetry(ids); for (const item of result.items ?? []) { if (item.found) { await db.query( "UPDATE users SET zep_user_uuid = $1 WHERE id = $2 AND zep_user_uuid IS NULL", [item.uuid, item.legacyId], ); } else { await db.query( "UPDATE users SET zep_lookup_status = 'not_found' WHERE id = $1", [item.legacyId], ); } } lastId = ids[ids.length - 1]; } } async function lookupWithRetry(ids: string[]) { for (;;) { try { return await zep.lookup.batch({ users: ids }); } catch (err) { if (!(err instanceof ZepError) || err.statusCode !== 429) { throw err; // 403 and 503 stop the job. No row is marked. } const seconds = Number(err.rawResponse?.headers.get("retry-after") ?? "1"); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } } } ``` **`Go`** ```go Go import ( "github.com/getzep/zep-go/v4" zepclient "github.com/getzep/zep-go/v4/client" "github.com/getzep/zep-go/v4/core" ) const pageSize = 1000 func usersWithoutUUID(ctx context.Context, db *sql.DB, lastID string, limit int) ([]string, error) { rows, err := db.QueryContext(ctx, "SELECT id FROM users"+ " WHERE zep_user_uuid IS NULL AND zep_lookup_status IS NULL AND id > $1"+ " ORDER BY id LIMIT $2", lastID, limit) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func backfillUsers(ctx context.Context, client *zepclient.Client, db *sql.DB) error { lastID := "" for { ids, err := usersWithoutUUID(ctx, db, lastID, pageSize) if err != nil { return err } if len(ids) == 0 { return nil } result, err := lookupWithRetry(ctx, client, ids) if err != nil { return err // 403 and 503 stop the job. No row is marked. } for _, item := range result.Items { if item.LegacyID == nil { continue } if item.Found != nil && *item.Found && item.UUID != nil { _, err = db.ExecContext(ctx, "UPDATE users SET zep_user_uuid = $1 WHERE id = $2 AND zep_user_uuid IS NULL", *item.UUID, *item.LegacyID) } else { _, err = db.ExecContext(ctx, "UPDATE users SET zep_lookup_status = 'not_found' WHERE id = $1", *item.LegacyID) } if err != nil { return err } } lastID = ids[len(ids)-1] } } func lookupWithRetry(ctx context.Context, client *zepclient.Client, ids []string) (*zep.LookupBatchResponse, error) { for { result, err := client.Lookup.Batch(ctx, &zep.BatchLookupRequest{Users: ids}) var apiErr *core.APIError if err == nil || !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusTooManyRequests { return result, err } seconds, convErr := strconv.Atoi(apiErr.Header.Get("Retry-After")) if convErr != nil { seconds = 1 } time.Sleep(time.Duration(seconds) * time.Second) } } ``` The examples show the `users` table. Run the same job for your conversation table with `threads`, and for your graph tables with `graphs`. **Finish this step when** the job finds no row without a UUID, and you resolved every row that is marked as not found. #### Move reads and writes to v4 **Purpose:** Remove every v3 call that reads or changes an existing resource. Change each read and write call to v4, and pass the stored UUID. Use the [method map](#method-map) for the v4 method, and read [Changes that need more than a rename](#changes-that-need-more-than-a-rename) for the changes in behavior. Keep the v3 create calls from step 2. Put the v4 code behind a feature flag, or roll it out to a part of your traffic at a time. v3 and v4 use the same data, so a message that v3 adds is available on v4, and the reverse is also true. You can move the traffic back to v3 during this step, because every resource still has its v3 identifier. A row can have no UUID at this time, for example when step 3 did not process the row. Do not call `lookup` in the request path for that row. Resolve the row outside the request path: send it to a background task that calls `user.lookup`, `thread.lookup`, or `graph.lookup` one time and writes the UUID into the row, or run the step 3 job again. Until the row has its UUID, serve that request with the v3 code. This safety net is temporary. Remove it in step 6. **Finish this step when** all of your traffic uses the v4 code for reads and writes, the only remaining v3 calls are the create calls, and the safety net resolved no row for a period that you choose, for example one week. #### Move resource creation to v4 **Purpose:** Create new users, threads, and graphs on v4, with no identifier. Change each create call to v4. Send no `user_id`, `thread_id`, `graph_id`, or `target_graph_id`. Read the UUID from the response, and store it in the row: | v4 create call | Value to store | Column | | -------------------------------------------------------- | -------------- | -------------------------------- | | `user.create` | `uuid` | `zep_user_uuid` | | `user.create` | `graph_uuid` | `zep_graph_uuid` on the user row | | `thread.create` (takes the `user_uuid`) | `uuid` | `zep_thread_uuid` | | `graph.create` | `uuid` | `zep_graph_uuid` | | `graph.clone` (takes the source `graph_uuid`, no target) | `graph.uuid` | `zep_graph_uuid` of the copy | A create call is a network call, so do not keep a database transaction open while it runs. Use this order of operations: 1. Insert your row with the Zep UUID column set to `NULL`. 2. Call the v4 create with the profile fields, such as the email address. 3. Write the UUID from the response into the row. If the process stops after step 1 or step 2, the row has a `NULL` UUID. A repair task finds these rows and repeats steps 2 and 3. The SDK sends an idempotency key on its own retries, so a retried request does not make a second resource. A create that your application repeats, such as the repair task after a stop between steps 2 and 3, makes a second user. To prevent that, you can pass an optional idempotency key that you store with the row. See [Repeated creates](#repeated-creates). This order prevents orphans. If you create the Zep resource before you insert your row, and the insert fails, no row refers to the Zep resource. **`Python`** ```python Python from zep_cloud.client import Zep # v4 zep = Zep(api_key=ZEP_API_KEY) def create_user(db, user_id: str, email: str) -> None: db.execute( "INSERT INTO users (id, email) VALUES (%s, %s)", (user_id, email), ) provision_zep_user(db, user_id) def provision_zep_user(db, user_id: str) -> None: """Called at creation time, and again by the repair task.""" row = db.fetch_one( "SELECT email FROM users WHERE id = %s", (user_id,) ) user = zep.user.create(email=row.email) db.execute( "UPDATE users SET zep_user_uuid = %s, zep_graph_uuid = %s" " WHERE id = %s AND zep_user_uuid IS NULL", (user.uuid_, user.graph_uuid, user_id), ) def provision_zep_thread(db, conversation_id: str) -> None: row = db.fetch_one( "SELECT u.zep_user_uuid FROM conversations c" " JOIN users u ON u.id = c.user_id WHERE c.id = %s", (conversation_id,), ) thread = zep.thread.create(user_uuid=row.zep_user_uuid) db.execute( "UPDATE conversations SET zep_thread_uuid = %s" " WHERE id = %s AND zep_thread_uuid IS NULL", (thread.uuid_, conversation_id), ) ``` **`TypeScript`** ```typescript TypeScript import { ZepClient } from "@getzep/zep-cloud"; // v4 const zep = new ZepClient({ apiKey: ZEP_API_KEY }); async function createUser(userId: string, email: string) { await db.query( "INSERT INTO users (id, email) VALUES ($1, $2)", [userId, email], ); await provisionZepUser(userId); } // Called at creation time, and again by the repair task. async function provisionZepUser(userId: string) { const { rows } = await db.query( "SELECT email FROM users WHERE id = $1", [userId], ); const user = await zep.user.create({ email: rows[0].email }); await db.query( "UPDATE users SET zep_user_uuid = $1, zep_graph_uuid = $2" + " WHERE id = $3 AND zep_user_uuid IS NULL", [user.uuid, user.graphUuid, userId], ); } async function provisionZepThread(conversationId: string) { const { rows } = await db.query( "SELECT u.zep_user_uuid FROM conversations c" + " JOIN users u ON u.id = c.user_id WHERE c.id = $1", [conversationId], ); const thread = await zep.thread.create({ userUuid: rows[0].zep_user_uuid }); await db.query( "UPDATE conversations SET zep_thread_uuid = $1" + " WHERE id = $2 AND zep_thread_uuid IS NULL", [thread.uuid, conversationId], ); } ``` **`Go`** ```go Go import ( "github.com/getzep/zep-go/v4" zepclient "github.com/getzep/zep-go/v4/client" ) func createUser(ctx context.Context, client *zepclient.Client, db *sql.DB, userID, email string) error { _, err := db.ExecContext(ctx, "INSERT INTO users (id, email) VALUES ($1, $2)", userID, email) if err != nil { return err } return provisionZepUser(ctx, client, db, userID) } // provisionZepUser runs at creation time, and again in the repair task. func provisionZepUser(ctx context.Context, client *zepclient.Client, db *sql.DB, userID string) error { var email string err := db.QueryRowContext(ctx, "SELECT email FROM users WHERE id = $1", userID).Scan(&email) if err != nil { return err } user, err := client.User.Create(ctx, &zep.CreateUserRequest{Email: &email}) if err != nil { return err } _, err = db.ExecContext(ctx, "UPDATE users SET zep_user_uuid = $1, zep_graph_uuid = $2 WHERE id = $3 AND zep_user_uuid IS NULL", user.UUID, user.GraphUUID, userID) return err } func provisionZepThread(ctx context.Context, client *zepclient.Client, db *sql.DB, conversationID string) error { var userUUID string err := db.QueryRowContext(ctx, "SELECT u.zep_user_uuid FROM conversations c"+ " JOIN users u ON u.id = c.user_id WHERE c.id = $1", conversationID).Scan(&userUUID) if err != nil { return err } thread, err := client.Thread.Create(ctx, &zep.CreateThreadRequest{UserUUID: userUUID}) if err != nil { return err } _, err = db.ExecContext(ctx, "UPDATE conversations SET zep_thread_uuid = $1 WHERE id = $2 AND zep_thread_uuid IS NULL", thread.UUID, conversationID) return err } ``` Use the same pattern for `graph.create` and `graph.clone`. A clone request has no body fields: Zep creates the target graph, and the response contains the new graph and a task. The response contains the UUID of the copy at once, so store it before the task completes. See [Cloning graphs](/cloning-graphs). > **Warning** > > Do not move reads or writes back to v3 after this step starts. v3 cannot > address a resource that v4 created. **Finish this step when** your application sends no v3 create call, and the repair task finds no row that has a `NULL` UUID. #### Remove v3 and lookup **Purpose:** Leave only the long-term pattern in your application. Remove these items from your application: * The v3 SDK and every v3 code path. * Every `lookup` call, including the safety net from step 4. * The backfill job from step 3. Keep the repair task from step 5. It is part of the create pattern. Keep your own identifiers in your own database, next to the Zep UUIDs. A resource that v3 created still has its legacy name as a read-only field. Do not use that field to find a resource. Read the UUID from your own record. **Finish this step when** a search of your code finds no v3 SDK import and no `lookup` call. ## Method map Use the language tab that matches your SDK. Each row is one v3 method. TypeScript POST list and search methods take pagination on the request object and the JSON body under `body`. The SDK throws `JsonError: Expected object` when `body` is missing. Use `{ limit: 1, body: {} }` for an unfiltered list and `{ limit: 1, body: { query: "..." } }` for search. Filter keys in that body stay snake\_case (`connected_node_uuids`), because `filters` is an untyped object. Go search methods put the query on `Body`, for example `GraphSearchEdgesRequest{Body: &SearchRequest{Query: "..."}}`. Artifact list `Filters` is `map[string]any`. Use the snake\_case keys `connected_node_uuids`, `mentioned_node_uuids`, and `episode_uuids`. ### Users #### Python | v3 | v4 | | --------------------------------------- | -------------------------------------------------------------------------------------------------- | | `user.add` | `user.create` (no `user_id`) | | `user.list_ordered` | `user.list` | | `user.get` | `user.get` (takes `user_uuid`) | | `user.update` | `user.update` (takes `user_uuid`) | | `user.delete` | `user.delete` (takes `user_uuid`; returns a task) | | `user.get_node` | `user.get_node` (takes `user_uuid`) | | `user.get_threads` | `thread.list(user_uuid=)` | | `user.warm` | `graph.warm(graph_uuid=user.graph_uuid)` | | `user.list_user_summary_instructions` | `user.get_summary_instructions`, or `project.get_user_summary_instructions` | | `user.add_user_summary_instructions` | `user.set_summary_instructions`, or `project.set_user_summary_instructions` (writes the whole set) | | `user.delete_user_summary_instructions` | `user.set_summary_instructions`, or `project.set_user_summary_instructions` (writes the whole set) | #### TypeScript | v3 | v4 | | ------------------------------------ | --------------------------------------------------------------------------------------------- | | `user.add` | `user.create` (no `user_id`) | | `user.listOrdered` | `user.list` | | `user.get` | `user.get` (takes `userUuid`) | | `user.update` | `user.update` (takes `userUuid`) | | `user.delete` | `user.delete` (takes `userUuid`; returns a task) | | `user.getNode` | `user.getNode` (takes `userUuid`) | | `user.getThreads` | `thread.list({ userUuid })` | | `user.warm` | `graph.warm(user.graphUuid)` | | `user.listUserSummaryInstructions` | `user.getSummaryInstructions`, or `project.getUserSummaryInstructions` | | `user.addUserSummaryInstructions` | `user.setSummaryInstructions`, or `project.setUserSummaryInstructions` (writes the whole set) | | `user.deleteUserSummaryInstructions` | `user.setSummaryInstructions`, or `project.setUserSummaryInstructions` (writes the whole set) | #### Go | v3 | v4 | | ------------------------------------ | --------------------------------------------------------------------------------------------- | | `User.Add` | `User.Create` (no `UserID`) | | `User.ListOrdered` | `User.List` | | `User.Get` | `User.Get` (takes user UUID) | | `User.Update` | `User.Update` (takes user UUID) | | `User.Delete` | `User.Delete` (takes user UUID; returns a task) | | `User.GetNode` | `User.GetNode` (takes user UUID) | | `User.GetThreads` | `Thread.List` with `UserUUID` | | `User.Warm` | `Graph.Warm(*user.GraphUUID)` after a nil check. `user.GraphUUID` is `*string`. | | `User.ListUserSummaryInstructions` | `User.GetSummaryInstructions`, or `Project.GetUserSummaryInstructions` | | `User.AddUserSummaryInstructions` | `User.SetSummaryInstructions`, or `Project.SetUserSummaryInstructions` (writes the whole set) | | `User.DeleteUserSummaryInstructions` | `User.SetSummaryInstructions`, or `Project.SetUserSummaryInstructions` (writes the whole set) | ### Threads #### Python | v3 | v4 | | ------------------------------- | --------------------------------------------------------------------- | | `thread.create` | `thread.create` (requires `user_uuid`, no `thread_id`) | | `thread.delete` | `thread.delete` (takes `thread_uuid`; returns a task) | | `thread.list_all` | `thread.list` | | `thread.get` (returns messages) | `thread.list_messages`. `thread.get` in v4 returns the thread record. | | `thread.add_messages` | `thread.add_messages` (takes `thread_uuid`) | | `thread.add_messages_batch` | `batch.create`, then `batch.add_items`, then `batch.process` | | `thread.get_user_context` | `thread.get_context` | | `thread.get_summary` | `thread.get_summary` | | `thread.message.update` | `thread.message.update` (takes `thread_uuid` and `message_uuid`) | #### TypeScript | v3 | v4 | | ------------------------------- | -------------------------------------------------------------------- | | `thread.create` | `thread.create` (requires `userUuid`, no `threadId`) | | `thread.delete` | `thread.delete` (takes `threadUuid`; returns a task) | | `thread.listAll` | `thread.list` | | `thread.get` (returns messages) | `thread.listMessages`. `thread.get` in v4 returns the thread record. | | `thread.addMessages` | `thread.addMessages` (takes `threadUuid`) | | `thread.addMessagesBatch` | `batch.create`, then `batch.addItems`, then `batch.process` | | `thread.getUserContext` | `thread.getContext` | | `thread.getSummary` | `thread.getSummary` | | `thread.message.update` | `thread.message.update` (takes `threadUuid` and `messageUuid`) | #### Go | v3 | v4 | | ------------------------------- | -------------------------------------------------------------------- | | `Thread.Create` | `Thread.Create` (requires `UserUUID`, no `ThreadID`) | | `Thread.Delete` | `Thread.Delete` (takes thread UUID; returns a task) | | `Thread.ListAll` | `Thread.List` | | `Thread.Get` (returns messages) | `Thread.ListMessages`. `Thread.Get` in v4 returns the thread record. | | `Thread.AddMessages` | `Thread.AddMessages` (takes thread UUID) | | `Thread.AddMessagesBatch` | `Batch.Create`, then `Batch.AddItems`, then `Batch.Process` | | `Thread.GetUserContext` | `Thread.GetContext` | | `Thread.GetSummary` | `Thread.GetSummary` | | `Thread.Message.Update` | `Thread.Message.Update` (takes thread UUID and message UUID) | ### Graphs and ingestion #### Python | v3 | v4 | | ----------------------- | ------------------------------------------------------------ | | `graph.create` | `graph.create` (no `graph_id`) | | `graph.get` | `graph.get` (takes `graph_uuid`) | | `graph.update` | `graph.update` (takes `graph_uuid`) | | `graph.delete` | `graph.delete` (takes `graph_uuid`; returns a task) | | `graph.list_all` | `graph.list` | | `graph.warm` | `graph.warm` (takes `graph_uuid`) | | `graph.clone` | `graph.clone` (source `graph_uuid`, no target) | | `graph.add` | `graph.episode.add` | | `graph.add_batch` | `batch.create`, then `batch.add_items`, then `batch.process` | | `graph.add_nodes` | `graph.node.add` | | `graph.add_fact_triple` | `graph.edge.add` | | `graph.get_subgraph` | `graph.get_subgraph` | | `graph.detect_patterns` | removed | #### TypeScript | v3 | v4 | | ---------------------- | ----------------------------------------------------------- | | `graph.create` | `graph.create` (no `graph_id`) | | `graph.get` | `graph.get` (takes `graphUuid`) | | `graph.update` | `graph.update` (takes `graphUuid`) | | `graph.delete` | `graph.delete` (takes `graphUuid`; returns a task) | | `graph.listAll` | `graph.list` | | `graph.warm` | `graph.warm` (takes `graphUuid`) | | `graph.clone` | `graph.clone` (source `graph_uuid`, no target) | | `graph.add` | `graph.episode.add` | | `graph.addBatch` | `batch.create`, then `batch.addItems`, then `batch.process` | | `graph.addNodes` | `graph.node.add` | | `graph.addFactTriple` | `graph.edge.add` | | `graph.getSubgraph` | `graph.getSubgraph` | | `graph.detectPatterns` | removed | #### Go | v3 | v4 | | ---------------------- | ----------------------------------------------------------- | | `Graph.Create` | `Graph.Create` (no `GraphID`) | | `Graph.Get` | `Graph.Get` (takes graph UUID) | | `Graph.Update` | `Graph.Update` (takes graph UUID) | | `Graph.Delete` | `Graph.Delete` (takes graph UUID; returns a task) | | `Graph.ListAll` | `Graph.List` | | `Graph.Warm` | `Graph.Warm` (takes graph UUID) | | `Graph.Clone` | `Graph.Clone` (source `graphUUID`, no target) | | `Graph.Add` | `Graph.Episode.Add` | | `Graph.AddBatch` | `Batch.Create`, then `Batch.AddItems`, then `Batch.Process` | | `Graph.AddNodes` | `Graph.Node.Add` | | `Graph.AddFactTriple` | `Graph.Edge.Add` | | `Graph.GetSubgraph` | `Graph.GetSubgraph` | | `Graph.DetectPatterns` | removed | ### Search and context #### Python | v3 | v4 | | -------------------------------------------- | ------------------------------- | | `graph.search` with scope `edges` | `graph.search_edges` | | `graph.search` with scope `nodes` | `graph.search_nodes` | | `graph.search` with scope `episodes` | `graph.search_episodes` | | `graph.search` with scope `observations` | `graph.search_observations` | | `graph.search` with scope `thread_summaries` | `graph.search_thread_summaries` | | `graph.search` with scope `auto` | `graph.get_context` | #### TypeScript | v3 | v4 | | -------------------------------------------- | ------------------------------------------------------------- | | `graph.search` with scope `edges` | `graph.searchEdges(graphUuid, { body: { query } })` | | `graph.search` with scope `nodes` | `graph.searchNodes(graphUuid, { body: { query } })` | | `graph.search` with scope `episodes` | `graph.searchEpisodes(graphUuid, { body: { query } })` | | `graph.search` with scope `observations` | `graph.searchObservations(graphUuid, { body: { query } })` | | `graph.search` with scope `thread_summaries` | `graph.searchThreadSummaries(graphUuid, { body: { query } })` | | `graph.search` with scope `auto` | `graph.getContext` | #### Go | v3 | v4 | | -------------------------------------------- | ----------------------------------------------- | | `Graph.Search` with scope `edges` | `Graph.SearchEdges` with `Body.Query` | | `Graph.Search` with scope `nodes` | `Graph.SearchNodes` with `Body.Query` | | `Graph.Search` with scope `episodes` | `Graph.SearchEpisodes` with `Body.Query` | | `Graph.Search` with scope `observations` | `Graph.SearchObservations` with `Body.Query` | | `Graph.Search` with scope `thread_summaries` | `Graph.SearchThreadSummaries` with `Body.Query` | | `Graph.Search` with scope `auto` | `Graph.GetContext` | ### Graph artifacts #### Python | v3 | v4 | | -------------------------------------- | -------------------------------------------------------------------- | | `graph.edge.get_by_graph_id` | `graph.edge.list` | | `graph.edge.get_by_user_id` | `graph.edge.list` | | `graph.edge.get` | `graph.edge.get` (also requires `graph_uuid`) | | `graph.edge.update` | `graph.edge.update` (also requires `graph_uuid`) | | `graph.edge.delete` | `graph.edge.delete` (also requires `graph_uuid`; returns a task) | | `graph.node.get_by_graph_id` | `graph.node.list` | | `graph.node.get_by_user_id` | `graph.node.list` | | `graph.node.get` | `graph.node.get` (also requires `graph_uuid`) | | `graph.node.update` | `graph.node.update` (also requires `graph_uuid`) | | `graph.node.delete` | `graph.node.delete` (also requires `graph_uuid`; returns a task) | | `graph.node.get_edges` | `graph.edge.list` with `filters.connected_node_uuids` | | `graph.node.get_episodes` | `graph.episode.list` with `filters.mentioned_node_uuids` | | `graph.node.get_neighbors` | `graph.node.list_neighbors` | | `graph.episode.get_by_graph_id` | `graph.episode.list` | | `graph.episode.list_by_graph_id` | `graph.episode.list` | | `graph.episode.get_by_user_id` | `graph.episode.list` | | `graph.episode.list_by_user_id` | `graph.episode.list` | | `graph.episode.get` | `graph.episode.get` (also requires `graph_uuid`) | | `graph.episode.update` | `graph.episode.update` (also requires `graph_uuid`) | | `graph.episode.delete` | `graph.episode.delete` (also requires `graph_uuid`; returns a task) | | `graph.episode.get_nodes_and_edges` | `graph.node.list` and `graph.edge.list` with `filters.episode_uuids` | | `graph.observation.get_by_graph_id` | `graph.observation.list` | | `graph.observation.get_by_user_id` | `graph.observation.list` | | `graph.observation.get` | `graph.observation.get` (also requires `graph_uuid`) | | `graph.thread_summary.get_by_graph_id` | `graph.thread_summary.list` | | `graph.thread_summary.get_by_user_id` | `graph.thread_summary.list` | #### TypeScript | v3 | v4 | | ---------------------------------- | --------------------------------------------------------------------------------------- | | `graph.edge.getByGraphId` | `graph.edge.list(graphUuid, { body: {} })` | | `graph.edge.getByUserId` | `graph.edge.list(graphUuid, { body: {} })` | | `graph.edge.get` | `graph.edge.get` (also requires `graphUuid`) | | `graph.edge.update` | `graph.edge.update` (also requires `graphUuid`) | | `graph.edge.delete` | `graph.edge.delete` (also requires `graphUuid`; returns a task) | | `graph.node.getByGraphId` | `graph.node.list(graphUuid, { body: {} })` | | `graph.node.getByUserId` | `graph.node.list(graphUuid, { body: {} })` | | `graph.node.get` | `graph.node.get` (also requires `graphUuid`) | | `graph.node.update` | `graph.node.update` (also requires `graphUuid`) | | `graph.node.delete` | `graph.node.delete` (also requires `graphUuid`; returns a task) | | `graph.node.getEdges` | `graph.edge.list(graphUuid, { body: { filters: { connected_node_uuids: [...] } } })` | | `graph.node.getEpisodes` | `graph.episode.list(graphUuid, { body: { filters: { mentioned_node_uuids: [...] } } })` | | `graph.node.getNeighbors` | `graph.node.listNeighbors` | | `graph.episode.getByGraphId` | `graph.episode.list(graphUuid, { body: {} })` | | `graph.episode.listByGraphId` | `graph.episode.list(graphUuid, { body: {} })` | | `graph.episode.getByUserId` | `graph.episode.list(graphUuid, { body: {} })` | | `graph.episode.listByUserId` | `graph.episode.list(graphUuid, { body: {} })` | | `graph.episode.get` | `graph.episode.get` (also requires `graphUuid`) | | `graph.episode.update` | `graph.episode.update` (also requires `graphUuid`) | | `graph.episode.delete` | `graph.episode.delete` (also requires `graphUuid`; returns a task) | | `graph.episode.getNodesAndEdges` | `graph.node.list` and `graph.edge.list` with `body.filters.episode_uuids` | | `graph.observation.getByGraphId` | `graph.observation.list(graphUuid, { body: {} })` | | `graph.observation.getByUserId` | `graph.observation.list(graphUuid, { body: {} })` | | `graph.observation.get` | `graph.observation.get` (also requires `graphUuid`) | | `graph.threadSummary.getByGraphId` | `graph.threadSummary.list(graphUuid, { body: {} })` | | `graph.threadSummary.getByUserId` | `graph.threadSummary.list(graphUuid, { body: {} })` | #### Go | v3 | v4 | | ---------------------------------- | ----------------------------------------------------------------------- | | `Graph.Edge.GetByGraphID` | `Graph.Edge.List` | | `Graph.Edge.GetByUserID` | `Graph.Edge.List` | | `Graph.Edge.Get` | `Graph.Edge.Get` (also requires graph UUID) | | `Graph.Edge.Update` | `Graph.Edge.Update` (also requires graph UUID) | | `Graph.Edge.Delete` | `Graph.Edge.Delete` (also requires graph UUID; returns a task) | | `Graph.Node.GetByGraphID` | `Graph.Node.List` | | `Graph.Node.GetByUserID` | `Graph.Node.List` | | `Graph.Node.Get` | `Graph.Node.Get` (also requires graph UUID) | | `Graph.Node.Update` | `Graph.Node.Update` (also requires graph UUID) | | `Graph.Node.Delete` | `Graph.Node.Delete` (also requires graph UUID; returns a task) | | `Graph.Node.GetEdges` | `Graph.Edge.List` with `Filters["connected_node_uuids"]` | | `Graph.Node.GetEpisodes` | `Graph.Episode.List` with `Filters["mentioned_node_uuids"]` | | `Graph.Node.GetNeighbors` | `Graph.Node.ListNeighbors` | | `Graph.Episode.GetByGraphID` | `Graph.Episode.List` | | `Graph.Episode.ListByGraphID` | `Graph.Episode.List` | | `Graph.Episode.GetByUserID` | `Graph.Episode.List` | | `Graph.Episode.ListByUserID` | `Graph.Episode.List` | | `Graph.Episode.Get` | `Graph.Episode.Get` (also requires graph UUID) | | `Graph.Episode.Update` | `Graph.Episode.Update` (also requires graph UUID) | | `Graph.Episode.Delete` | `Graph.Episode.Delete` (also requires graph UUID; returns a task) | | `Graph.Episode.GetNodesAndEdges` | `Graph.Node.List` and `Graph.Edge.List` with `Filters["episode_uuids"]` | | `Graph.Observation.GetByGraphID` | `Graph.Observation.List` | | `Graph.Observation.GetByUserID` | `Graph.Observation.List` | | `Graph.Observation.Get` | `Graph.Observation.Get` (also requires graph UUID) | | `Graph.ThreadSummary.GetByGraphID` | `Graph.ThreadSummary.List` | | `Graph.ThreadSummary.GetByUserID` | `Graph.ThreadSummary.List` | ### Configuration #### Python | v3 | v4 | | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `graph.list_entity_types` | `graph.get_ontology`, or `project.get_ontology` | | `graph.set_entity_types_internal` | `graph.set_ontology`, or `project.set_ontology` | | `graph.set_entity_types` | `graph.set_ontology`, or `project.set_ontology` | | `graph.set_ontology` | `graph.set_ontology`, or `project.set_ontology` | | `zep_cloud.external_clients.ontology` (`EntityModel`, `EdgeModel`, `EntityText`) | `zep_cloud.ontology` (`EntityModel`, `EdgeModel`, `EntityText`, `build_ontology`); pass the `entity_types` and `edge_types` that `build_ontology` returns to `set_ontology` | | `graph.list_custom_instructions` | `graph.get_instructions`, or `project.get_instructions` | | `graph.add_custom_instructions` | `graph.set_instructions`, or `project.set_instructions` (writes the whole set) | | `graph.delete_custom_instructions` | `graph.set_instructions`, or `project.set_instructions` (writes the whole set) | | `project.get` | `project.get` | | `project.update` | `project.update` | | `project.get_observation_steering` | `project.get_observation_steering`, or `graph.get_observation_steering` | | `project.set_observation_steering` | `project.set_observation_steering`, or `graph.set_observation_steering` | | `context.list_context_templates` | `context.list_templates` (filter with `name`, not `search`) | | `context.create_context_template` | `context.create_template` | | `context.get_context_template` | `context.get_template` | | `context.update_context_template` | `context.update_template` | | `context.delete_context_template` | `context.delete_template` | #### TypeScript | v3 | v4 | | -------------------------------- | ----------------------------------------------------------------------------------------------------- | | `graph.listEntityTypes` | `graph.getOntology`, or `project.getOntology` | | `graph.setEntityTypesInternal` | `graph.setOntology`, or `project.setOntology` | | `graph.setEntityTypes` | `graph.setOntology`, or `project.setOntology` | | `graph.setOntology` | `graph.setOntology`, or `project.setOntology` | | `entityFields` | `entityFields` and `buildOntology`; pass the `Ontology` that `buildOntology` returns to `setOntology` | | `graph.listCustomInstructions` | `graph.getInstructions`, or `project.getInstructions` | | `graph.addCustomInstructions` | `graph.setInstructions`, or `project.setInstructions` (writes the whole set) | | `graph.deleteCustomInstructions` | `graph.setInstructions`, or `project.setInstructions` (writes the whole set) | | `project.get` | `project.get` | | `project.update` | `project.update` | | `project.getObservationSteering` | `project.getObservationSteering`, or `graph.getObservationSteering` | | `project.setObservationSteering` | `project.setObservationSteering`, or `graph.setObservationSteering` | | `context.listContextTemplates` | `context.listTemplates` (filter with `name`, not `search`) | | `context.createContextTemplate` | `context.createTemplate` | | `context.getContextTemplate` | `context.getTemplate` | | `context.updateContextTemplate` | `context.updateTemplate` | | `context.deleteContextTemplate` | `context.deleteTemplate` | #### Go | v3 | v4 | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Graph.ListEntityTypes` | `Graph.GetOntology`, or `Project.GetOntology` | | `Graph.SetEntityTypesInternal` | `Graph.SetOntology`, or `Project.SetOntology` | | `Graph.SetEntityTypes` | `Graph.SetOntology`, or `Project.SetOntology` | | `Graph.SetOntology` | `Graph.SetOntology`, or `Project.SetOntology` | | `zep.BaseEntity`, `zep.BaseEdge` | `zep.EntityBase`, `zep.EdgeBase`, `zep.Entities`, `zep.Edges`, and `zep.BuildOntology`; pass the `*zep.Ontology` that `zep.BuildOntology` returns to `SetOntology` | | `Graph.ListCustomInstructions` | `Graph.GetInstructions`, or `Project.GetInstructions` | | `Graph.AddCustomInstructions` | `Graph.SetInstructions`, or `Project.SetInstructions` (writes the whole set) | | `Graph.DeleteCustomInstructions` | `Graph.SetInstructions`, or `Project.SetInstructions` (writes the whole set) | | `Project.Get` | `Project.Get` | | `Project.Update` | `Project.Update` | | `Project.GetObservationSteering` | `Project.GetObservationSteering`, or `Graph.GetObservationSteering` | | `Project.SetObservationSteering` | `Project.SetObservationSteering`, or `Graph.SetObservationSteering` | | `Context.ListContextTemplates` | `Context.ListTemplates` (filter with `Name`, not `Search`) | | `Context.CreateContextTemplate` | `Context.CreateTemplate` | | `Context.GetContextTemplate` | `Context.GetTemplate` | | `Context.UpdateContextTemplate` | `Context.UpdateTemplate` | | `Context.DeleteContextTemplate` | `Context.DeleteTemplate` | ### Batches, tasks, and user groups #### Python | v3 | v4 | | ----------------------------------- | ---------------------------------------------------------- | | `batch.list` | `batch.list` | | `batch.create` | `batch.create` | | `batch.get` | `batch.get` | | `batch.delete` | `batch.delete` | | `batch.list_items` | `batch.list_items` | | `batch.add` | `batch.add_items` | | `batch.process` | `batch.process` | | `task.get` | `task.get` | | `user_group.list` | `user_group.list` | | `user_group.create` | `user_group.create` | | `user_group.get` | `user_group.get` | | `user_group.delete` | `user_group.delete` | | `user_group.update` | `user_group.update` | | `user_group.list_for_user` | `user_group.list_for_user` | | `user_group.list_members` | `user_group.list_members` | | `user_group.add_members` | `user_group.add_members` | | `user_group.remove_members` | `user_group.remove_members` | | `user_group.remove_member` | `user_group.remove_member` | | `user_group.list_member_candidates` | `user_group.list_member_candidates` | | `user_group.list_policy_sets` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `user_group.attach_policy_set` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `user_group.detach_policy_set` | no v4 equivalent. Manage policy sets in the Zep dashboard. | #### TypeScript | v3 | v4 | | -------------------------------- | ---------------------------------------------------------- | | `batch.list` | `batch.list` | | `batch.create` | `batch.create` | | `batch.get` | `batch.get` | | `batch.delete` | `batch.delete` | | `batch.listItems` | `batch.listItems` | | `batch.add` | `batch.addItems` | | `batch.process` | `batch.process` | | `task.get` | `task.get` | | `userGroup.list` | `userGroup.list` | | `userGroup.create` | `userGroup.create` | | `userGroup.get` | `userGroup.get` | | `userGroup.delete` | `userGroup.delete` | | `userGroup.update` | `userGroup.update` | | `userGroup.listForUser` | `userGroup.listForUser` | | `userGroup.listMembers` | `userGroup.listMembers` | | `userGroup.addMembers` | `userGroup.addMembers` | | `userGroup.removeMembers` | `userGroup.removeMembers` | | `userGroup.removeMember` | `userGroup.removeMember` | | `userGroup.listMemberCandidates` | `userGroup.listMemberCandidates` | | `userGroup.listPolicySets` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `userGroup.attachPolicySet` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `userGroup.detachPolicySet` | no v4 equivalent. Manage policy sets in the Zep dashboard. | #### Go | v3 | v4 | | -------------------------------- | ---------------------------------------------------------- | | `Batch.List` | `Batch.List` | | `Batch.Create` | `Batch.Create` | | `Batch.Get` | `Batch.Get` | | `Batch.Delete` | `Batch.Delete` | | `Batch.ListItems` | `Batch.ListItems` | | `Batch.Add` | `Batch.AddItems` | | `Batch.Process` | `Batch.Process` | | `Task.Get` | `Task.Get` | | `UserGroup.List` | `UserGroup.List` | | `UserGroup.Create` | `UserGroup.Create` | | `UserGroup.Get` | `UserGroup.Get` | | `UserGroup.Delete` | `UserGroup.Delete` | | `UserGroup.Update` | `UserGroup.Update` | | `UserGroup.ListForUser` | `UserGroup.ListForUser` | | `UserGroup.ListMembers` | `UserGroup.ListMembers` | | `UserGroup.AddMembers` | `UserGroup.AddMembers` | | `UserGroup.RemoveMembers` | `UserGroup.RemoveMembers` | | `UserGroup.RemoveMember` | `UserGroup.RemoveMember` | | `UserGroup.ListMemberCandidates` | `UserGroup.ListMemberCandidates` | | `UserGroup.ListPolicySets` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `UserGroup.AttachPolicySet` | no v4 equivalent. Manage policy sets in the Zep dashboard. | | `UserGroup.DetachPolicySet` | no v4 equivalent. Manage policy sets in the Zep dashboard. | ### New in v4 These methods have no v3 counterpart. #### Python * `user.lookup`, `thread.lookup`, `graph.lookup`, `lookup.batch` * `thread.get` (thread record by UUID) * `thread.list_episodes` * `thread.message.get` * `task.list` * `graph.document_summary.list` * `graph.episode.list_for_document` #### TypeScript * `user.lookup`, `thread.lookup`, `graph.lookup`, `lookup.batch` * `thread.get` (thread record by UUID) * `thread.listEpisodes` * `thread.message.get` * `task.list` * `graph.documentSummary.list` * `graph.episode.listForDocument` #### Go * `User.Lookup`, `Thread.Lookup`, `Graph.Lookup`, `Lookup.Batch` * `Thread.Get` (thread record by UUID) * `Thread.ListEpisodes` * `Thread.Message.Get` * `Task.List` * `Graph.DocumentSummary.List` * `Graph.Episode.ListForDocument` ## Changes that need more than a rename ### Pagination Loops written against `page_number` or `lastn`, or against the `Zep-Next-Cursor` header, do not port. Replace them with the SDK pager. The pager walks an opaque cursor. Do not increment a page number. #### Python **v3.** Advance `page_number` until a page is empty. ```python page_number = 1 while True: page = client.user.list_ordered(page_number=page_number, page_size=50) users = page.users or [] if not users: break for user in users: print(user.user_id) page_number += 1 ``` **v4.** Iterate the pager. `lastn` episode reads become `graph.episode.list`. ```python for user in client.user.list(limit=50): print(user.user_id) ``` #### TypeScript **v3.** Advance `pageNumber` until a page is empty. ```typescript let pageNumber = 1; for (;;) { const page = await client.user.listOrdered({ pageNumber, pageSize: 50 }); const users = page.users ?? []; if (users.length === 0) { break; } for (const user of users) { console.log(user.userId); } pageNumber += 1; } ``` **v4.** Iterate the pager. `lastn` episode reads become `graph.episode.list`. ```typescript const page = await client.user.list({ limit: 50 }); for await (const user of page) { console.log(user.userId); } ``` #### Go **v3.** Advance `PageNumber` until a page is empty. ```go for pageNumber := 1; ; pageNumber++ { page, err := client.User.ListOrdered(ctx, &zep.UserListOrderedRequest{ PageNumber: zep.Int(pageNumber), PageSize: zep.Int(50), }) if err != nil { return err } if page == nil || len(page.Users) == 0 { break } for _, user := range page.Users { fmt.Println(*user.UserID) } } ``` **v4.** Use the page iterator. `lastn` episode reads become `Graph.Episode.List`. ```go page, err := client.User.List(ctx, &zep.UserListRequest{Limit: zep.Int(50)}) if err != nil { return err } iter := page.Iterator() for iter.Next(ctx) { fmt.Println(*iter.Current().UserID) } if err := iter.Err(); err != nil { return err } ``` ### Deletes A delete returns a task rather than a success envelope. The resource is not gone when the call returns. Poll the returned task until it reaches `succeeded`, `partial`, or `failed`. Bound the wait. Treat a missing `status` as not finished. #### Python **v3.** The call returns a success envelope. The next line can assume the user is gone. ```python client.user.delete("user_1234") ``` **v4.** Delete by UUID, then poll the task. ```python import time TERMINAL = {"succeeded", "partial", "failed"} deadline = time.monotonic() + 600 result = client.user.delete(user.uuid_) task = client.task.get(result.task.uuid_) while task.status not in TERMINAL: if time.monotonic() > deadline: raise TimeoutError(f"task {result.task.uuid_} is still {task.status}") time.sleep(1) task = client.task.get(result.task.uuid_) ``` #### TypeScript **v3.** The call returns a success envelope. The next line can assume the user is gone. ```typescript await client.user.delete("user_1234"); ``` **v4.** Delete by UUID, then poll the task. ```typescript const TERMINAL = new Set(["succeeded", "partial", "failed"]); const deadline = Date.now() + 600_000; const result = await client.user.delete(user.uuid); let task = await client.task.get(result.task.uuid); while (!task.status || !TERMINAL.has(task.status)) { if (Date.now() > deadline) { throw new Error(`task ${result.task.uuid} is still ${task.status ?? "unset"}`); } await new Promise((resolve) => setTimeout(resolve, 1000)); task = await client.task.get(result.task.uuid); } ``` #### Go **v3.** The call returns a success envelope. The next line can assume the user is gone. ```go _, err := client.User.Delete(ctx, "user_1234") if err != nil { return err } ``` **v4.** Delete by UUID, then poll the task. ```go terminal := map[string]bool{"succeeded": true, "partial": true, "failed": true} result, err := client.User.Delete(ctx, *user.UUID) if err != nil { return err } deadline := time.Now().Add(10 * time.Minute) task, err := client.Task.Get(ctx, *result.Task.UUID) for err == nil && (task.Status == nil || !terminal[*task.Status]) { if time.Now().After(deadline) { return fmt.Errorf("task %s has not reached a terminal status", *result.Task.UUID) } time.Sleep(time.Second) task, err = client.Task.Get(ctx, *result.Task.UUID) } if err != nil { return err } ``` ### Instruction updates v3 added and deleted individual instructions by name. v4 reads and writes the whole set. An update is a read, a change, and a write. A write that omits an existing name removes that instruction. #### Python **v3.** Add or delete one instruction by `name`. Other instructions stay in place. ```python from zep_cloud import CustomInstruction client.graph.add_custom_instructions( instructions=[ CustomInstruction(name="tone", text="Prefer concise facts."), ] ) client.graph.delete_custom_instructions(instruction_names=["tone"]) ``` **v4.** Read the current list, change it, and write the full list back. `project.set_instructions` sets the project default. `graph.set_instructions` sets one graph. ```python from zep_cloud import CustomInstruction current = client.project.get_instructions() updated = [i for i in (current.instructions or []) if i.name != "tone"] updated.append(CustomInstruction(name="tone", text="Prefer concise facts.")) client.project.set_instructions(instructions=updated) ``` #### TypeScript **v3.** Add or delete one instruction by `name`. Other instructions stay in place. ```typescript await client.graph.addCustomInstructions({ instructions: [{ name: "tone", text: "Prefer concise facts." }], }); await client.graph.deleteCustomInstructions({ instructionNames: ["tone"] }); ``` **v4.** Read the current list, change it, and write the full list back. `project.setInstructions` sets the project default. `graph.setInstructions` sets one graph. ```typescript const current = await client.project.getInstructions(); const updated = (current.instructions ?? []).filter((i) => i.name !== "tone"); updated.push({ name: "tone", text: "Prefer concise facts." }); await client.project.setInstructions({ instructions: updated }); ``` #### Go **v3.** Add or delete one instruction by name. Other instructions stay in place. ```go _, err := client.Graph.AddCustomInstructions(ctx, &zep.AddCustomInstructionsRequest{ Instructions: []*zep.CustomInstruction{ {Name: "tone", Text: "Prefer concise facts."}, }, }) if err != nil { return err } _, err = client.Graph.DeleteCustomInstructions(ctx, &zep.DeleteCustomInstructionsRequest{ InstructionNames: []string{"tone"}, }) ``` **v4.** Read the current list, change it, and write the full list back. `Project.SetInstructions` sets the project default. `Graph.SetInstructions` sets one graph. ```go current, err := client.Project.GetInstructions(ctx) if err != nil { return err } updated := []*zep.CustomInstruction{} for _, instruction := range current.Instructions { if instruction != nil && instruction.Name != "tone" { updated = append(updated, instruction) } } updated = append(updated, &zep.CustomInstruction{ Name: "tone", Text: "Prefer concise facts.", }) _, err = client.Project.SetInstructions(ctx, &zep.Instructions{Instructions: updated}) ``` ### Declaring entity and edge types in code v3 took your model classes or structs and derived the payload inside the SDK call. v4 separates the two steps. You declare the types with the SDK ontology helpers, you build the payload, and you pass the payload to `graph.set_ontology` or `project.set_ontology`. The helper names changed with the step: | Language | v3 | v4 | | ---------- | ------------------------------------- | -------------------------------------------------------------------------------------- | | Python | `zep_cloud.external_clients.ontology` | `zep_cloud.ontology` (`EntityModel`, `EdgeModel`, `EntityText`), and `build_ontology` | | TypeScript | `entityFields` | `entityFields`, and `buildOntology` | | Go | `zep.BaseEntity`, `zep.BaseEdge` | `zep.EntityBase`, `zep.EdgeBase`, `zep.Entities`, `zep.Edges`, and `zep.BuildOntology` | The v3 Python module `zep_cloud.external_clients.ontology` does not exist in v4. Import the models and `build_ontology` from `zep_cloud.ontology`. The builder returns the payload. It does not call the API. `build_ontology` returns the `entity_types` list and the `edge_types` list. `buildOntology` and `zep.BuildOntology` return one `Ontology` object. A type name is now a key of the builder input, not a struct tag or a class name. A property that tells two nodes of the same type apart is marked as an identity property in the declaration. #### Python **v3.** `graph.set_ontology` took the model classes. ```python from pydantic import Field from zep_cloud.external_clients.ontology import EntityModel, EntityText class Traveler(EntityModel): """Someone who takes trips.""" home_city: EntityText = Field(description="The city they live in", default=None) client.graph.set_ontology(entities={"Traveler": Traveler}) ``` **v4.** Import the models from `zep_cloud.ontology`, call `build_ontology`, and pass the two lists it returns. ```python from pydantic import Field from typing_extensions import Annotated from zep_cloud.ontology import EntityModel, EntityText, Identity, build_ontology class Traveler(EntityModel): """Someone who takes trips.""" home_city: Annotated[EntityText, Identity] = Field( default=None, description="The city they live in" ) entity_types, edge_types = build_ontology(entities={"Traveler": Traveler}) client.graph.set_ontology( graph_uuid, entity_types=entity_types, edge_types=edge_types, ) ``` #### TypeScript **v3.** `graph.setOntology` took the entity map and the edge map. ```typescript import { entityFields } from "@getzep/zep-cloud"; const Traveler = { description: "Someone who takes trips.", fields: { homeCity: entityFields.text("The city they live in") }, }; await client.graph.setOntology({ Traveler }, {}); ``` **v4.** Call `buildOntology`, then pass the one `Ontology` object it returns. ```typescript import { buildOntology, entityFields } from "@getzep/zep-cloud"; const Traveler = { description: "Someone who takes trips.", fields: { homeCity: entityFields.text("The city they live in", { identity: true }), }, } as const; const ontology = buildOntology({ entities: { Traveler } }); await client.graph.setOntology(graphUuid, ontology); ``` #### Go **v3.** The struct embedded `zep.BaseEntity` and carried the type name in a tag. ```go type Traveler struct { zep.BaseEntity `name:"Traveler" description:"Someone who takes trips."` HomeCity string `description:"The city they live in"` } _, err := client.Graph.SetOntology(ctx, []zep.EntityDefinition{Traveler{}}, nil) ``` **v4.** The struct embeds `zep.EntityBase`, the map key holds the type name, and `zep.BuildOntology` returns the `*zep.Ontology` that the call takes. ```go type Traveler struct { zep.EntityBase `description:"Someone who takes trips."` HomeCity string `description:"The city they live in" identity:"true"` } ontology, err := zep.BuildOntology(zep.Entities{"Traveler": Traveler{}}, nil) if err != nil { return err } _, err = client.Graph.SetOntology(ctx, graphUUID, ontology) if err != nil { return err } ``` An edge type is declared the same way. Use `EdgeModel` in Python, an edge declaration with `sourceTargets` in TypeScript, and `zep.EdgeBase` with `zep.Edges` in Go. Give each edge type the entity type pairs it connects. ### Ontology across many graphs v3 applied one ontology to many graphs in a single call. v4 has one ontology operation per scope. Set the [project ontology](/customizing-graph-structure) once for a shared default, or loop over the graphs that need an override. #### Python **v3.** One call can target many `user_ids` and `graph_ids`. ```python client.graph.set_ontology( user_ids=["user_1234", "user_5678"], graph_ids=["support-kb", "sales-kb"], entities=entities, edges=edges, ) ``` **v4.** Set the project default, or call `graph.set_ontology` once per graph UUID. ```python client.project.set_ontology(entity_types=entity_types, edge_types=edge_types) for graph_uuid in graph_uuids: client.graph.set_ontology( graph_uuid, entity_types=entity_types, edge_types=edge_types, ) ``` #### TypeScript **v3.** One call can target many `userIds` and `graphIds`. ```typescript await client.graph.setOntology( { Restaurant: RestaurantSchema }, { RESTAURANT_VISIT: RestaurantVisit }, { userIds: ["user_1234"], graphIds: ["support-kb", "sales-kb"] }, ); ``` **v4.** Set the project default, or call `graph.setOntology` once per graph UUID. ```typescript await client.project.setOntology(ontology); for (const graphUuid of graphUuids) { await client.graph.setOntology(graphUuid, ontology); } ``` #### Go **v3.** `ForUsers` and `ForGraphs` apply the same ontology to many identifiers. ```go _, err := client.Graph.SetOntology( ctx, entities, edges, zep.ForUsers([]string{"user_1234"}), zep.ForGraphs([]string{"support-kb", "sales-kb"}), ) if err != nil { return err } ``` **v4.** Set the project default, or call `Graph.SetOntology` once per graph UUID. ```go _, err := client.Project.SetOntology(ctx, ontology) if err != nil { return err } for _, graphUUID := range graphUUIDs { _, err = client.Graph.SetOntology(ctx, graphUUID, ontology) if err != nil { return err } } ``` ### List filters v3 had one artifact getter per scope, and a getter took no filter. v4 has one list per artifact type, and the list takes a `filters` object in the request body. The server applies the object, and combines each key with the scope of the list with a logical AND. The SDK types `filters` as a free-form object in all three languages: `Dict[str, Any]` in Python, `Record` in TypeScript, and `map[string]any` in Go. A key is therefore checked by the server, not by the compiler or the type checker. Three rules apply: * A key that no filter defines gets HTTP 400 with the code `unsupported_filter` and the name of the key. * A key that the result type cannot enforce gets the same error. `graph.edge.list` is the only list that takes `connected_node_uuids`, `source_node_uuids`, and `target_node_uuids`, because the other lists return no edges. `graph.observation.list`, `graph.thread_summary.list`, and `graph.document_summary.list` also reject `episode_uuids`, because an observation and a summary are never in the mention set of an episode. * `metadata_filters` selects results whose associated episodes' stored metadata matches the filter. The `episode_metadata_filters` name from earlier revisions is rejected: rename the key. `graph.node.list` takes `episode_uuids`, which selects the nodes that the listed episodes mention. `graph.episode.list` takes `mentioned_node_uuids` and the metadata filter, and rejects every other key. ### Searching for a name The user, graph, and context template lists are `POST .../list` with the filter term in the body. A term you search by is customer data, and a query string is logged. Pass the term on the SDK list call. Do not put the term in a URL. #### Python **v3.** `graph.list_all` sent `search` as a query parameter. ```python page = client.graph.list_all(search="EMEA support", page_number=1, page_size=20) ``` **v4.** `graph.list` puts `search` in the request body. Iterate the pager. ```python for graph in client.graph.list(search="EMEA support", limit=20): print(graph.uuid_, graph.name) ``` #### TypeScript **v3.** `graph.listAll` sent `search` as a query parameter. ```typescript const page = await client.graph.listAll({ search: "EMEA support", pageNumber: 1, pageSize: 20, }); ``` **v4.** `graph.list` puts `search` in the request body. Iterate the pager. ```typescript const page = await client.graph.list({ search: "EMEA support", limit: 20 }); for await (const graph of page) { console.log(graph.uuid, graph.name); } ``` #### Go **v3.** `Graph.ListAll` sent `Search` as a query parameter. ```go page, err := client.Graph.ListAll(ctx, &zep.GraphListAllRequest{ Search: zep.String("EMEA support"), PageNumber: zep.Int(1), PageSize: zep.Int(20), }) ``` **v4.** `Graph.List` puts `Search` in the request body. ```go page, err := client.Graph.List(ctx, &zep.GraphListRequest{ Search: zep.String("EMEA support"), Limit: zep.Int(20), }) if err != nil { return err } iter := page.Iterator() for iter.Next(ctx) { graph := iter.Current() if graph.UUID != nil && graph.Name != nil { fmt.Println(*graph.UUID, *graph.Name) } } ``` `user.list` and `graph.list` take the same `search` field. `context.list_templates` has no `search` field. It takes `name`, which selects the template with that exact name. ### Recency bias v3 accepted a `recency_bias` object on `graph.search` with scope `auto`. v4 takes a preset of `off`, `mild`, or `strong` on `graph.get_context`. There is no object with `strength`, half-lives, or per-scope bands. #### Python **v3.** Auto search accepted a `recency_bias` object. ```python result = client.graph.search( user_id="user_1234", query="recent account issues", scope="auto", recency_bias={"strength": 0.8}, ) ``` **v4.** Assemble context with a preset. ```python result = client.graph.get_context( user.graph_uuid, query="recent account issues", recency_bias="strong", ) ``` #### TypeScript **v3.** Auto search accepted a `recencyBias` object. ```typescript const result = await client.graph.search({ userId: "user_1234", query: "recent account issues", scope: "auto", recencyBias: { strength: 0.8 }, }); ``` **v4.** Assemble context with a preset. ```typescript const result = await client.graph.getContext(user.graphUuid, { query: "recent account issues", recencyBias: "strong", }); ``` #### Go **v3.** Auto search accepted a `recency_bias` object on the search body, with a required `strength` field in `[0, 1]`. ```go scope := zep.GraphSearchScopeAuto result, err := client.Graph.Search(ctx, &zep.GraphSearchQuery{ UserID: zep.String("user_1234"), Query: "recent account issues", Scope: &scope, }) ``` **v4.** Assemble context with a preset. ```go result, err := client.Graph.GetContext(ctx, *user.GraphUUID, &zep.GraphContextRequest{ Query: "recent account issues", RecencyBias: zep.GraphContextRequestRecencyBiasStrong.Ptr(), }) if err != nil { return err } ``` ### Document identifiers v4 rejects a `document_id` that contains `/`, `?`, `#`, or an ASCII control character, and rejects a whitespace-only value. v3 accepted any string of 1 to 100 characters. ### Repeated creates In v3, a retried create with the same identifier failed because the identifier was already in use. v4 creates carry no identifier, so each create call makes a new resource. The SDK sends an idempotency key on its own retries, so a retried request does not make a second resource. You can also pass an `Idempotency-Key` on a create that your application can repeat. See [Move resource creation to v4](#migrate-a-production-application) for the pattern. ### Defaults and caps Search `limit` defaults to 50, where v3 defaulted to 10. The maximum is 50 in both. A graph artifact list defaults to 50 with a maximum of 100. ## What has no v4 equivalent * **`graph.detect_patterns` / `graph.detectPatterns` / `Graph.DetectPatterns`.** The experimental endpoint is removed. * **User-group policy sets** (`list_policy_sets`, `attach_policy_set`, `detach_policy_set` and the TypeScript / Go equivalents). Manage policy sets from the Zep dashboard. > Every v3 SDK method mapped to its v4 replacement