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

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://help.getzep.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://help.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 v3 column matches `zep-cloud` / `@getzep/zep-cloud` / `zep-go`
**v3.30.0**.

UUID addressing, pagination, and asynchronous deletes account for most of the work in a port. Read [Migration steps](#migration-steps), [Pagination](#pagination), and [Deletes](#deletes) first.

#### Let a coding agent do the migration

The Build with Zep plugin for Claude Code, Codex, and Cursor contains the `migrating-zep-v3-to-v4` skill. The skill tells your coding agent how to migrate your application from v3 to v4 with this guide.

To use the skill, [install the Build with Zep plugin](/implement-zep-with-agents). Then ask your agent to migrate your application to Zep v4.

## Migration steps

v4 addresses every user, thread, and graph by a UUID that Zep generates. Your
v3 `user_id`, `thread_id`, and `graph_id` values are not v4 addresses. The
optional v4 `user_id` field supports identity-provider (IdP) matching. It does
not change the user's v4 address. This section explains the changes and gives
the steps to migrate an application from v3 to v4.

**What is true in v4:**

* The methods that took your identifier in v3, such as `get`, `update`,
  `delete`, and `add_messages`, take a UUID. If you pass the old identifier,
  the call fails with HTTP 400 and the error code `invalid_identifier`.
* Zep generates the UUID of every user, thread, and graph. `user.create`
  accepts an optional `user_id` for IdP matching. This value is unique within
  the project, has a 500-character limit, and cannot change after creation.
  It is not the user's address. `thread.create` rejects `thread_id`,
  `graph.create` rejects `graph_id`, and `graph.clone` rejects
  `target_graph_id`.
* A v4 user response includes `user_id` when it has a value. A v4 create can
  set this field, and a v3-created user keeps its existing value. The field is
  read-only in v4. A v4-created thread or graph has no legacy ID.
* A resource from v3 keeps its v3 identifier as read-only data in v4. This
  value is not a v4 address.
* v3 and v4 serve the same project data at the same time. The v4 API addresses
  a v3 resource by its UUID. The v3 `user.get` method can find a v4-created
  user when the request uses the same `user_id`. The v3 API cannot address a
  v4-created thread or graph.
* The `lookup` operations resolve a legacy name to a UUID: `user.lookup`,
  `thread.lookup`, `graph.lookup`, and `lookup.batch`. `lookup` will become
  deprecated when the v3 surface retires, and a later major version will
  remove it.

**Why it changes:** Each identifier is an opaque UUID. A UUID 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.

**The pattern after the migration:** Your application stores each Zep UUID in
its own database, next to its own identifier. It reads the UUID from that row
and passes it on every v4 call. 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. Zep stores an optional `user_id` in the
same field that v3 uses. Set it only when the IdP must match a user. Use the
UUID to address the user in v4. If you need an application label on a Zep
user, put it in the user `metadata`. A thread has no `metadata` field. A graph
has a `name` and a `description`; these fields describe the graph.

**How to migrate:** Do the steps below in order. Your application stores the
UUID of each new resource while it still uses v3. Then a backfill job gives
the existing rows their UUIDs. The backfill is the only step that uses
`lookup`. Because `lookup` will become deprecated, your application code must
never call it, also not as a fallback. Then your application moves its reads
and writes to v4. Move resource creation to v4 after your remaining v3 calls
no longer need identifiers that v4 does not set. The v3 `user.get` method can
find a v4-created user when `user_id` matches. The v3 API cannot address a
v4-created thread or graph.

> **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.

### Step 1: 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.

### Step 2: Store the UUID of each new resource on v3

**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.

### Step 3: Backfill the existing rows with lookup

**Purpose:** Give every row that existed before step 2 its UUID.

This step is the only step that uses `lookup`. `lookup.batch` resolves up to
1000 identifiers of users, threads, and graphs in one request. The response
has one item for each identifier. An identifier that does not resolve returns
`found: false` with no 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.

### Step 4: 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.

Start this step only after the step 3 job finds no more rows without a
UUID. Before you start, resolve each row that the job marked as not found:
create the resource again with the code from step 2, or remove the
reference to Zep from the row. Step 2 continues to store the UUID of each
new resource, so every row then has its UUID. The v4 code reads the UUID
from your table and does not call `lookup`. Do not add a `lookup` fallback
to your application, because `lookup` will become deprecated.

A new row can still have no UUID, for example because a write in step 2
failed. The v4 code cannot address that row. Serve the requests for that
row with the v3 code, and run the step 3 job again to fill the UUID.

The example below moves one call to v4. The v4 code reads the thread UUID
from your own table, and then passes it to `thread.add_messages`.

#### 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.** Read the thread UUID from your conversation record, and pass it.

```python
from zep_cloud import AddMessage

row = db.fetch_one(
    "SELECT zep_thread_uuid FROM conversations WHERE id = %s",
    ("thread_1234",),
)

client.thread.add_messages(
    row.zep_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.** Read the thread UUID from your conversation record, and pass it.

```typescript
const { rows } = await db.query(
  "SELECT zep_thread_uuid FROM conversations WHERE id = $1",
  ["thread_1234"],
);

await client.thread.addMessages(rows[0].zep_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.RoleTypeUserRole,
            Name:    zep.String("Ada"),
            Content: "I prefer window seats.",
        },
    },
})
```

**v4.** Read the thread UUID from your conversation record, and pass it.

```go
var threadUUID string
err := db.QueryRowContext(ctx,
    "SELECT zep_thread_uuid FROM conversations WHERE id = $1",
    "thread_1234").Scan(&threadUUID)
if err != nil {
    return err
}

_, err = client.Thread.AddMessages(ctx, threadUUID, &zep.AddMessagesRequest{
    Messages: []*zep.AddMessage{
        {
            Role:    zep.RoleTypeUser.Ptr(),
            Name:    zep.String("Ada"),
            Content: zep.String("I prefer window seats."),
        },
    },
})
```

**Finish this step when** all of your traffic uses the v4 code for reads
and writes, and the only remaining v3 calls are the create calls.

### Step 5: Move resource creation and the remaining v3 code to v4

**Purpose:** Create new users, threads, and graphs on v4, and move the
remaining v3 code to v4.

Change each create call to v4. Set `user_id` only when it must match an IdP
claim. Do not send `thread_id`, `graph_id`, or `target_graph_id`. Read each
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).

In the same step, move every other v3 call that remains in your application
to v4. Use the [method map](#method-map) for the v4 method of each call, and
read
[Changes that need more than a rename](#changes-that-need-more-than-a-rename)
for the changes in behavior. After this step, your application sends no v3
call.

> **Warning**
>
> Do not move reads or writes back to v3 after this step starts. The v3
> `user.get` method can find a v4-created user only when `user_id` matches. The
> v3 API cannot address a v4-created thread or graph.

When your application sends no v3 call, complete the migration:

* Remove the v3 SDK package from your dependencies. In Python, also remove
  the separate process that sent the v3 calls.
* Delete the backfill job from step 3. Your application does not need it
  after this step.
* Keep the repair task from this step. It is part of the create pattern.

A v3-created user keeps its `user_id` as a read-only field in v4. V3-created
threads and graphs also keep their v3 identifiers as read-only fields. These
values are not v4 addresses. 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, and the repair task finds no row that has a `NULL` UUID.

## Method map

Use the language tab that matches your SDK. Each row is one v3 method.

TypeScript search and graph-list methods take one request object. Put
`pageSize`, `query`, and `filters` on that object. Use `{ pageSize: 1 }` for
an unfiltered list and `{ pageSize: 1, query: "..." }` for search. Do not nest
these fields under `body`. Filter names use camelCase, such as
`connectedNodeUuids`. The SDK ignores unknown filter keys, so a snake\_case key
has no effect.

Go search methods take request structs with top-level fields. For example, use
`&zep.SearchEdgesRequest{Query: "..."}`. Graph artifact-list methods use
resource-specific request structs. For example, use
`&graph.EdgeListRequest{Filters: &zep.EdgeListFilters{ConnectedNodeUUIDs: []string{...}}}`.
The episode list also takes `Filters` directly, for example
`&graph.EpisodeListRequest{Filters: &zep.EpisodeListFilters{MentionedNodeUUIDs: []string{...}}}`.

### Users

#### Python

| v3                                      | v4                                                                                                 |
| --------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `user.add`                              | `user.create` (optional `user_id` for IdP matching)                                                |
| `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.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` (optional `userId` for IdP matching)                                            |
| `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.uuid)`                                                                       |
| `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` (optional `UserID` for IdP matching)                                            |
| `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.UUID)` after a nil check. `user.UUID` 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.get_episodes`           | `thread.list_episodes` (takes `thread_uuid`)                          |
| `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.getEpisodes`            | `thread.listEpisodes` (takes `threadUuid`)                           |
| `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.GetEpisodes`            | `Thread.ListEpisodes` (takes thread UUID)                            |
| `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` (returns an `AddEpisodeResult`; see [Ingestion results](#ingestion-results)) |
| `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` (takes an `edges` list; see [Adding edges](#adding-edges))                      |
| `graph.get_episodes_for_document` | `graph.episode.list_for_document` (also requires `graph_uuid`)                                   |
| `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` (returns an `AddEpisodeResult`; see [Ingestion results](#ingestion-results)) |
| `graph.addBatch`               | `batch.create`, then `batch.addItems`, then `batch.process`                                      |
| `graph.addNodes`               | `graph.node.add`                                                                                 |
| `graph.addFactTriple`          | `graph.edge.add` (takes an `edges` list; see [Adding edges](#adding-edges))                      |
| `graph.getEpisodesForDocument` | `graph.episode.listForDocument` (also requires `graphUuid`)                                      |
| `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` (returns an `AddEpisodeResult`; see [Ingestion results](#ingestion-results)) |
| `Graph.AddBatch`               | `Batch.Create`, then `Batch.AddItems`, then `Batch.Process`                                      |
| `Graph.AddNodes`               | `Graph.Node.Add`                                                                                 |
| `Graph.AddFactTriple`          | `Graph.Edge.Add` (takes an `Edges` list; see [Adding edges](#adding-edges))                      |
| `Graph.GetEpisodesForDocument` | `Graph.Episode.ListForDocument` (also requires graph UUID)                                       |
| `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, { pageSize: 10, query })`           |
| `graph.search` with scope `nodes`            | `graph.searchNodes(graphUuid, { pageSize: 10, query })`           |
| `graph.search` with scope `episodes`         | `graph.searchEpisodes(graphUuid, { pageSize: 10, query })`        |
| `graph.search` with scope `observations`     | `graph.searchObservations(graphUuid, { pageSize: 10, query })`    |
| `graph.search` with scope `thread_summaries` | `graph.searchThreadSummaries(graphUuid, { pageSize: 10, query })` |
| `graph.search` with scope `auto`             | `graph.getContext`                                                |

#### Go

| v3                                           | v4                                                                                                          |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Graph.Search` with scope `edges`            | `Graph.SearchEdges` with `&zep.SearchEdgesRequest{PageSize: zep.Int(10), Query: query}`                     |
| `Graph.Search` with scope `nodes`            | `Graph.SearchNodes` with `&zep.SearchNodesRequest{PageSize: zep.Int(10), Query: query}`                     |
| `Graph.Search` with scope `episodes`         | `Graph.SearchEpisodes` with `&zep.SearchEpisodesRequest{PageSize: zep.Int(10), Query: query}`               |
| `Graph.Search` with scope `observations`     | `Graph.SearchObservations` with `&zep.SearchObservationsRequest{PageSize: zep.Int(10), Query: query}`       |
| `Graph.Search` with scope `thread_summaries` | `Graph.SearchThreadSummaries` with `&zep.SearchThreadSummariesRequest{PageSize: zep.Int(10), Query: 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`                                          |
| `graph.document_summary.get_by_graph_id` | `graph.document_summary.list`                                        |

#### TypeScript

| v3                                   | v4                                                                                                                                                                      |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graph.edge.getByGraphId`            | `graph.edge.list(graphUuid, { pageSize: 100 })`                                                                                                                         |
| `graph.edge.getByUserId`             | `graph.edge.list(graphUuid, { pageSize: 100 })`                                                                                                                         |
| `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, { pageSize: 100 })`                                                                                                                         |
| `graph.node.getByUserId`             | `graph.node.list(graphUuid, { pageSize: 100 })`                                                                                                                         |
| `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, { pageSize: 100, filters: { connectedNodeUuids: [...] } })`                                                                                 |
| `graph.node.getEpisodes`             | `graph.episode.list(graphUuid, { pageSize: 100, filters: { mentionedNodeUuids: [...] } })`                                                                              |
| `graph.node.getNeighbors`            | `graph.node.listNeighbors`                                                                                                                                              |
| `graph.episode.getByGraphId`         | `graph.episode.list(graphUuid, { pageSize: 100 })`                                                                                                                      |
| `graph.episode.listByGraphId`        | `graph.episode.list(graphUuid, { pageSize: 100 })`                                                                                                                      |
| `graph.episode.getByUserId`          | `graph.episode.list(graphUuid, { pageSize: 100 })`                                                                                                                      |
| `graph.episode.listByUserId`         | `graph.episode.list(graphUuid, { pageSize: 100 })`                                                                                                                      |
| `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(graphUuid, { pageSize: 100, filters: { episodeUuids: [...] } })` and `graph.edge.list(graphUuid, { pageSize: 100, filters: { episodeUuids: [...] } })` |
| `graph.observation.getByGraphId`     | `graph.observation.list(graphUuid, { pageSize: 100 })`                                                                                                                  |
| `graph.observation.getByUserId`      | `graph.observation.list(graphUuid, { pageSize: 100 })`                                                                                                                  |
| `graph.observation.get`              | `graph.observation.get` (also requires `graphUuid`)                                                                                                                     |
| `graph.threadSummary.getByGraphId`   | `graph.threadSummary.list(graphUuid, { pageSize: 100 })`                                                                                                                |
| `graph.threadSummary.getByUserId`    | `graph.threadSummary.list(graphUuid, { pageSize: 100 })`                                                                                                                |
| `graph.documentSummary.getByGraphId` | `graph.documentSummary.list(graphUuid, { pageSize: 100 })`                                                                                                              |

#### 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 `&graph.EdgeListRequest{Filters: &zep.EdgeListFilters{ConnectedNodeUUIDs: []string{...}}}`                                                                                                           |
| `Graph.Node.GetEpisodes`             | `Graph.Episode.List` with `graph.EpisodeListRequest{Filters: &zep.EpisodeListFilters{MentionedNodeUUIDs: []string{...}}}`                                                                                                   |
| `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` with `&graph.NodeListRequest{Filters: &zep.NodeListFilters{EpisodeUUIDs: []string{...}}}` and `Graph.Edge.List` with `&graph.EdgeListRequest{Filters: &zep.EdgeListFilters{EpisodeUUIDs: []string{...}}}` |
| `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`                                                                                                                                                                                                  |
| `Graph.DocumentSummary.GetByGraphID` | `Graph.DocumentSummary.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 SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `user_group.attach_policy_set`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `user_group.detach_policy_set`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |

#### 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 SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `userGroup.attachPolicySet`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `userGroup.detachPolicySet`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |

#### 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 SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `UserGroup.AttachPolicySet`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |
| `UserGroup.DetachPolicySet`      | no SDK method. Manage policy sets in the Zep dashboard or with `zepctl`. |

### 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.message.get`
* `task.list`
* `debug_log.enable`, `graph.episode.get_debug_logs`,
  `graph.episode.list_ingestion_traces`

#### TypeScript

* `user.lookup`, `thread.lookup`, `graph.lookup`, `lookup.batch`
* `thread.get` (thread record by UUID)
* `thread.message.get`
* `task.list`
* `debugLog.enable`, `graph.episode.getDebugLogs`,
  `graph.episode.listIngestionTraces`

#### Go

* `User.Lookup`, `Thread.Lookup`, `Graph.Lookup`, `Lookup.Batch`
* `Thread.Get` (thread record by UUID)
* `Thread.Message.Get`
* `Task.List`
* `DebugLog.Enable`, `Graph.Episode.GetDebugLogs`,
  `Graph.Episode.ListIngestionTraces`

`graph.node.list` also takes two new parameters. `order_by` sets the sort key:
`uuid` (the default) or `degree`. `order` sets the direction: `asc` or `desc`
(the default). When `order_by` is `degree`, each node in the result has a
`degree` field. This field is the number of live entity edges that touch the
node, in the two directions. The SDK parameters are `order_by` and `order` in
Python, `orderBy` and `order` in TypeScript, and `OrderBy` and `Order` in Go.
In Go, the values are enums, for example
`graph.NodeListRequestOrderByDegree.Ptr()` and
`graph.NodeListRequestOrderAsc.Ptr()`.

## 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(page_size=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({ pageSize: 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{PageSize: 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 SDKs give `filters` a type. The 5 artifact lists take `SearchFilters`,
and `graph.episode.list` takes `EpisodeListFilters`. In Python, `filters`
also accepts a dictionary. In TypeScript, the field names are camelCase, for
example `connectedNodeUuids`, and the SDK removes a key that it does not know.
The type does not show which keys a list accepts, so the server checks the
keys. 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.

The search methods, `graph.get_subgraph`, `graph.node.list_neighbors`, and
`graph.get_context` take the same `filters` object. v4 does not accept the v3
`search_filters` name. The filter grammar also changes:

* Operators are lowercase names: `eq`, `ne`, `gt`, `lt`, `gte`, `lte`,
  `is_null`, and `is_not_null`. `property_filters` and `metadata_filters`
  also take `in`. Only `metadata_filters` takes `contains`. v4 rejects
  uppercase and symbolic operators, such as `EQ`, `=`, `IN`, and `CONTAINS`.
* Replace the v3 `CONTAINS` operator and its comma-separated value with `in`
  and an array value. The v4 `contains` operator is a case-sensitive
  substring match on `metadata_filters`.
* `date_filters` replaces the v3 date arrays. Its `any_of` list holds groups,
  and each group has an `all_of` list of conditions. Each condition has a
  `field` (`created_at`, `valid_at`, `invalid_at`, or `expired_at`), an
  `operator`, and a `value` in RFC 3339 format. One group can contain
  conditions on more than one field. A request with more than one `any_of`
  group must use one field in every condition of every group. Otherwise, the
  request fails with HTTP 400 and the code `unsupported_filter`.
  In TypeScript, the names are `dateFilters`, `anyOf`, and `allOf`, and
  `value` is a `Date`. In Go, `field` and `operator` are enums, for example
  `zep.DateFilterFieldCreatedAt.Ptr()` and `zep.DateFilterOperatorGte.Ptr()`,
  and `value` takes `zep.Time(...)`.
* Each `property_filters` item has a `property_name`, an `operator`, and a
  `value`.
* `metadata_filters` has a `type` of `and` or `or`, a `filters` list, and a
  `groups` list for nested groups.

An unknown key at the top level of `filters` fails with HTTP 400 and the code
`unsupported_filter`. An unknown key in a nested filter object fails with HTTP
400 and the code `invalid_request`. See
[Searching the graph](/searching-the-graph).

### 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", page_size=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", pageSize: 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"),
    PageSize: 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.

### Reference times

v3 took the time of a message or an episode in a `created_at` field. v4
takes the same value in `reference_time`. The rename applies to these inputs:

* Each message in `thread.add_messages`.
* `graph.episode.add`.
* Each item in `batch.add_items`.

The `Message` and `Episode` objects return the value in `reference_time`. A
request that sets `created_at` on one of these inputs fails with HTTP 400 and
the error code `invalid_request`. The SDK field is `reference_time` in Python,
`referenceTime` in TypeScript, and `ReferenceTime` in Go.

### Adding edges

v3 `graph.add_fact_triple` added one fact triple in each call. v4
`graph.edge.add` takes an `edges` list with 1 to 100 edges. Each edge has a
`fact`, a `fact_name`, a `source_node`, and a `target_node`. An edge can also have
`valid_at`, `invalid_at`, `expired_at`, `attributes`, and `metadata`.

A node reference contains the `uuid` of an existing node, or the `name` of a
node to create. A reference with a `name` can also contain `labels`, a
`summary`, and `attributes`.

The `deduplicate` field is `false` by default. The field applies to the nodes
and to the edges:

* When `deduplicate` is `false`, Zep does not compare the request with the
  graph. Each node `name` creates a new node. In one request, the node
  references that have the same `name` and `labels` use one node. Each edge is
  a new edge. The response gives the `source_node_uuid` and `target_node_uuid`
  of each edge.
* When `deduplicate` is `true`, Zep matches each node `name` with an existing
  node in the graph. Zep can also merge a duplicate edge, and Zep can
  invalidate facts in the graph that the new fact contradicts. Deduplication
  adds LLM processing for each edge. The task result gives the node UUIDs.

v3 `graph.add_fact_triple` always matched each node name with an existing
node, and it could invalidate contradicted facts. To keep this behavior in v4,
set `deduplicate` to `true`.

These v3 checks also changed:

* v3 rejected a node UUID that was not in the graph. v4 accepts the request,
  and the task fails with the `node_not_found` error.
* v3 rejected an edge when the source node and the target node had the same
  name or the same UUID. v4 accepts this edge.

See [Adding fact triplets](/adding-fact-triplets).

### Context assembly

`graph.get_context` replaces `graph.search` with scope `auto`. The Context
Block from `graph.get_context` contains no episodes. To get episodes, call
`graph.search_episodes` or `graph.episode.list`.

The v4 SDKs do not have the v3 context string helper: `compose_context_string`
in Python, `composeContextString` in TypeScript, and `ComposeContextString` in
Go. Use the Context Block from `graph.get_context`. If you make your own
context string from the results of a search operation, you must write
[your own Context Block formatter](/advanced-context-block-construction).

### Ingestion results

In v3, `graph.add` returned an `Episode`. In v4, `graph.episode.add` returns an
`AddEpisodeResult`. Its `episode` field has the new episode, and its `task`
field has the task that tracks graph extraction. Code that read `episode.uuid_`
in v3 must read `result.episode.uuid_` in v4. In TypeScript, read
`result.episode?.uuid`. In Go, read `result.Episode.UUID`.

### Errors

Every v4 error response has an `error` object with these fields: `code`,
`message`, `param`, `details`, and `request_id`. The `code` is a stable value
that your code can compare. `param` names the request field that caused the
error, when one field caused it. Every v4 response has an `X-Zep-Request-ID`
header with the same value as `request_id`.

| HTTP status | Error codes                                                                       |
| ----------- | --------------------------------------------------------------------------------- |
| 400         | `invalid_request`, `invalid_cursor`, `unsupported_filter`, `invalid_identifier`   |
| 401         | `unauthenticated`                                                                 |
| 403         | `permission_denied`                                                               |
| 404         | `not_found`                                                                       |
| 409         | `resource_already_exists`, `cursor_stale`, `conflict`, `idempotency_key_conflict` |
| 413         | `payload_too_large`                                                               |
| 429         | `rate_limited`                                                                    |
| 500         | `internal_error`                                                                  |
| 503         | `service_unavailable`                                                             |

### Renamed fields

Each row is one field that v4 renames. The v3 column gives the v3 field, and
the v4 column gives the field that replaces it.

| Object or request               | v3                               | v4                                         |
| ------------------------------- | -------------------------------- | ------------------------------------------ |
| Node                            | `episodes`, `episodes_truncated` | `episode_uuids`, `episode_uuids_truncated` |
| Edge                            | `episodes`                       | `episode_uuids`                            |
| Task                            | `task_id`, `params`              | `uuid`, `result`                           |
| Context template                | `template_id`                    | `uuid` (address) and `name`                |
| Search and subgraph request     | `search_filters`                 | `filters`                                  |
| Auto search and context request | `return_raw_results`             | `include_results`                          |
| List response                   | `total_count`, `row_count`       | `total_size`                               |

The user, thread, graph, user group, task, batch, and context template lists
return `total_size`.
The graph artifact lists, the search operations, and the ingestion trace list
return no total count. Use `next_cursor` to get the next page of these lists.

### 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. In v4, a repeated `user.create` with the same
`user_id` returns HTTP 409 `resource_already_exists`. A create without
`user_id`, or a thread or graph create, can make a new resource when you send
it again with a new idempotency key. The SDK sends an idempotency key on its
own retries, so a retry does not make a second resource. You can also pass an
`Idempotency-Key` on a create that your application can repeat. See
[Step 5](#step-5-move-resource-creation-and-the-remaining-v3-code-to-v4) for the
pattern.

### Defaults and caps

Search `page_size` 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. The other
lists, such as `user.list`, `thread.list`, and `task.list`, default to 50 with
a maximum of 200. `lookup.batch` accepts up to 1000 identifiers in one
request.

## 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). The v4 SDKs have
  no method for these operations. Manage policy sets in the Zep dashboard or
  with `zepctl`.