Migrating from v3
Most v3 methods have a direct v4 replacement. This page maps all of them for Python, TypeScript, and Go, then covers the changes that need more than a rename.
The tables match zep-cloud / @getzep/zep-cloud / zep-go v3.28.0 and
the v4 alpha packages 4.0.0a5 / 4.0.0-alpha.5 / v4.0.0-alpha.5.
UUID addressing, pagination, and asynchronous deletes account for most of the work in a port. Read Identifiers, Pagination, and Deletes first.
Identifiers need to be adapted
Your existing user_id, thread_id, and graph_id values do not work the
way they did in v3. You cannot pass them to get, update, delete,
add_messages, or any other method that used to accept them. Those methods
now take a UUID. If you pass the old identifier, the call fails.
Zep generates the UUID of every user, thread, and graph. The client addresses each resource by that UUID. A v4 create call does not accept an identifier from the client:
user.createdoes not accept auser_id.thread.createdoes not accept athread_id.graph.createdoes not accept agraph_id.graph.clonedoes not accept atarget_graph_id.
A create request that contains one of these fields with a value fails with
HTTP 400 and the error code invalid_request.
A resource that v4 creates has no legacy name. The v4 response for that
resource contains no user_id, thread_id, or graph_id. A resource that v3 created keeps the identifier
that your application gave it in v3. In v4, that identifier is a legacy name:
a read-only field that you cannot change. A legacy name is not an address.
Only the transitional lookup operations accept a legacy name as input.
Two reasons drive the design. Every identifier is an opaque UUID that holds
no tenant, no resource type, no order, and no customer data. A
customer-supplied value can hold personal data, and a value in a URL path or
a query string goes into access logs, proxy logs, referrer headers, and error
reports. For the same reason, each lookup operation is a POST with the
identifier in the request body.
Store the UUID in your own database
Every v4 application uses the same pattern:
- Call
user.create,thread.create, orgraph.createwith no identifier. - Read the
uuidfrom the response. Theuser.createresponse also contains thegraph_uuidof the user graph. - Write the UUID into your own table. For example, use
zep_user_uuidandzep_graph_uuidcolumns on youruserstable, and azep_thread_uuidcolumn on the table that holds conversations. - Pass the stored UUID on every later call.
Your own identifiers stay in your database, in the same row as the Zep UUID. Zep does not store them, and no Zep operation accepts them. To find the Zep resource for one of your records, read the UUID from that record.
If you need a label on a Zep user, put the label in the user metadata. A
thread has no metadata field. A graph has a name and a description. The
graph name is descriptive text. It is not an identifier, and no operation
addresses a graph by its name.
Migrate a production application shows this pattern with code, including safe retries.
Resolve legacy names with lookup
The lookup operations resolve a legacy name to a UUID:
user.lookupresolves oneuser_id.thread.lookupresolves onethread_id.graph.lookupresolves onegraph_id.lookup.batchresolves up to 1000 identifiers of users, threads, and graphs in one request.
Use lookup only to migrate identifiers that your application stored in v3.
A resource that v4 creates has no legacy name, so lookup never finds it.
Do not call lookup in the request path of your application. A lookup before
every read adds a round trip to each request. Resolve each identifier one
time, keep the UUID, and address the resource by UUID.
lookup is transitional. It becomes deprecated when the v3 surface retires,
and a later major version removes it. Code that reads a stored UUID needs no
further change.
The example below resolves many identifiers in one request. The response has
one item for each identifier in the request. An identifier that does not
resolve returns found: false with no UUID.
The map key contains the resource type, because a user and a thread can have the same legacy name.
Migrate one call
The example below resolves one thread and then adds messages to it. The lookup is a one-time migration step. In production code, read the UUID from your own database, as the production procedure shows.
Python
TypeScript
Go
v3. thread.add_messages takes your thread_id.
v4. Look up the thread, then pass uuid_. Store uuid_ with your own
conversation record, and read it from there on every later call. Use
user.lookup(user_id=...) and graph.lookup(graph_id=...) the same way.
Migrate a production application
Use this procedure when your application runs on v3 in production and must continue to create users, threads, and graphs during the migration. At the end, your application uses only v4, and it addresses every resource by a UUID that it keeps in its own database.
Two facts set the order of the steps:
- v3 and v4 serve the same project data at the same time. A resource that v3 creates is available through v4, and v4 addresses it by UUID.
- v3 cannot address a resource that v4 creates. v3 addresses users, threads, and graphs by the identifier that your application gave them. A resource that v4 creates has no such identifier.
For these reasons, your application moves its reads and writes to v4 first. It moves resource creation to v4 only after no v3 read or write remains.
During steps 2 to 5, your application uses the v3 SDK and the v4 SDK. The Go
SDKs have different module paths (github.com/getzep/zep-go/v3 and
github.com/getzep/zep-go/v4), so one Go module can import both. In
TypeScript, install the v3 SDK under an alias, for example
npm install zep-cloud-v3@npm:@getzep/zep-cloud@^3.30.0, and install the v4
SDK as @getzep/zep-cloud. In Python, both SDKs are the zep-cloud package,
so one Python environment cannot contain both. Run the v3 calls and the v4
calls in different processes, for example in two services.
Add nullable UUID columns
Purpose: Give each of your records a place for the Zep UUID.
Add a nullable column for the Zep UUID to each table that refers to a Zep resource. For example:
- Add
zep_user_uuidandzep_graph_uuidto youruserstable. - Add
zep_thread_uuidto the table that holds conversations. - Add
zep_graph_uuidto each table that refers to a standalone graph. - Add
zep_lookup_statusto each table that the backfill reads. The backfill writes'not_found'into this column and skips those rows later.
Keep your own identifier columns. They stay the key of your records.
The columns are nullable because the existing rows have no UUID yet. Step 3 fills them.
Finish this step when the columns exist in production and your application runs with the new schema.
Capture the UUID of every new resource
Purpose: Stop the number of rows without a UUID from growing.
Keep the v3 create calls, and keep your v3 identifiers for now. Change each create call to write the UUID from the v3 response into the new column. The v3 response already contains the UUID that v4 uses, so no lookup is necessary. Use v3 SDK version 3.30.0 or later, which exposes the graph fields below.
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.
Finish this step when the new code runs on every instance of your application. From this point, each new row gets its UUID at creation time.
Backfill the existing rows
Purpose: Give every row that existed before step 2 its UUID.
Run a job outside your request path. The job uses the v4 SDK:
- 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.
- Call
lookup.batchwith the identifiers of those rows. One request accepts up to 1000 identifiers in total acrossusers,threads, andgraphs. Do not put the same identifier two times in one list. - For each item with
found: true, write theuuidinto the row. Write only when the column is stillNULL, so that the job never changes a value that step 2 wrote. - For each item with
found: false, record the row as not found, for example in azep_lookup_statuscolumn. The job does not read these rows again. - 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.
The examples show the users table. Run the same job for your
conversation table with threads, and for your graph tables with
graphs.
Finish this step when the job finds no row without a UUID, and you resolved every row that is marked as not found.
Move reads and writes to v4
Purpose: Remove every v3 call that reads or changes an existing resource.
Change each read and write call to v4, and pass the stored UUID. Use the method map for the v4 method, and read Changes that need more than a rename for the changes in behavior. Keep the v3 create calls from step 2.
Put the v4 code behind a feature flag, or roll it out to a part of your traffic at a time. v3 and v4 use the same data, so a message that v3 adds is available on v4, and the reverse is also true. You can move the traffic back to v3 during this step, because every resource still has its v3 identifier.
A row can have no UUID at this time, for example when step 3 did not
process the row. Do not call lookup in the request path for that row.
Resolve the row outside the request path: send it to a background task
that calls user.lookup, thread.lookup, or graph.lookup one time and
writes the UUID into the row, or run the step 3 job again. Until the row
has its UUID, serve that request with the v3 code. This safety net is
temporary. Remove it in step 6.
Finish this step when all of your traffic uses the v4 code for reads and writes, the only remaining v3 calls are the create calls, and the safety net resolved no row for a period that you choose, for example one week.
Move resource creation to v4
Purpose: Create new users, threads, and graphs on v4, with no identifier.
Change each create call to v4. Send no user_id, thread_id,
graph_id, or target_graph_id. Read the UUID from the response, and
store it in the row:
A create call is a network call, so do not keep a database transaction open while it runs. Use this order of operations:
- Insert your row with the Zep UUID column set to
NULL. - Call the v4 create with the profile fields, such as the email address.
- 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.
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.
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.
Do not move reads or writes back to v3 after this step starts. v3 cannot address a resource that v4 created.
Finish this step when your application sends no v3 create call, and
the repair task finds no row that has a NULL UUID.
Remove v3 and lookup
Purpose: Leave only the long-term pattern in your application.
Remove these items from your application:
- The v3 SDK and every v3 code path.
- Every
lookupcall, including the safety net from step 4. - The backfill job from step 3.
Keep the repair task from step 5. It is part of the create pattern.
Keep your own identifiers in your own database, next to the Zep UUIDs. A resource that v3 created still has its legacy name as a read-only field. Do not use that field to find a resource. Read the UUID from your own record.
Finish this step when a search of your code finds no v3 SDK import
and no lookup call.
Method map
Use the language tab that matches your SDK. Each row is one v3 method.
TypeScript POST list and search methods take pagination on the request
object and the JSON body under body. The SDK throws
JsonError: Expected object when body is missing. Use
{ limit: 1, body: {} } for an unfiltered list and
{ limit: 1, body: { query: "..." } } for search. Filter keys in that
body stay snake_case (connected_node_uuids), because filters is an
untyped object.
Go search methods put the query on Body, for example
GraphSearchEdgesRequest{Body: &SearchRequest{Query: "..."}}. Artifact
list Filters is map[string]any. Use the snake_case keys
connected_node_uuids, mentioned_node_uuids, and episode_uuids.
Users
Python
TypeScript
Go
Threads
Python
TypeScript
Go
Graphs and ingestion
Python
TypeScript
Go
Search and context
Python
TypeScript
Go
Graph artifacts
Python
TypeScript
Go
Configuration
Python
TypeScript
Go
Batches, tasks, and user groups
Python
TypeScript
Go
New in v4
These methods have no v3 counterpart.
Python
TypeScript
Go
user.lookup,thread.lookup,graph.lookup,lookup.batchthread.get(thread record by UUID)thread.list_episodesthread.message.gettask.listgraph.document_summary.listgraph.episode.list_for_document
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
TypeScript
Go
v3. Advance page_number until a page is empty.
v4. Iterate the pager. lastn episode reads become graph.episode.list.
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
TypeScript
Go
v3. The call returns a success envelope. The next line can assume the user is gone.
v4. Delete by UUID, then poll the task.
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
TypeScript
Go
v3. Add or delete one instruction by name. Other instructions stay in
place.
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.
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:
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
TypeScript
Go
v3. graph.set_ontology took the model classes.
v4. Import the models from zep_cloud.ontology, call build_ontology, and
pass the two lists it returns.
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 once for a shared default, or loop over the graphs that need an override.
Python
TypeScript
Go
v3. One call can target many user_ids and graph_ids.
v4. Set the project default, or call graph.set_ontology once per graph
UUID.
List filters
v3 had one artifact getter per scope, and a getter took no filter. v4 has one
list per artifact type, and the list takes a filters object in the request
body. The server applies the object, and combines each key with the scope of
the list with a logical AND.
The SDK types filters as a free-form object in all three languages:
Dict[str, Any] in Python, Record<string, unknown> in TypeScript, and
map[string]any in Go. A key is therefore checked by the server, not by the
compiler or the type checker. Three rules apply:
- A key that no filter defines gets HTTP 400 with the code
unsupported_filterand the name of the key. - A key that the result type cannot enforce gets the same error.
graph.edge.listis the only list that takesconnected_node_uuids,source_node_uuids, andtarget_node_uuids, because the other lists return no edges.graph.observation.list,graph.thread_summary.list, andgraph.document_summary.listalso rejectepisode_uuids, because an observation and a summary are never in the mention set of an episode. metadata_filtersselects results whose associated episodes’ stored metadata matches the filter. Theepisode_metadata_filtersname from earlier revisions is rejected: rename the key.
graph.node.list takes episode_uuids, which selects the nodes that the
listed episodes mention. graph.episode.list takes mentioned_node_uuids and
the metadata filter, and rejects every other key.
Searching for a name
The user, graph, and context template lists are POST .../list with the filter
term in the body. A term you search by is customer data, and a query string is
logged. Pass the term on the SDK list call. Do not put the term in a URL.
Python
TypeScript
Go
v3. graph.list_all sent search as a query parameter.
v4. graph.list puts search in the request body. Iterate the pager.
user.list and graph.list take the same search field.
context.list_templates has no search field. It takes name, which selects
the template with that exact name.
Recency bias
v3 accepted a recency_bias object on graph.search with scope auto. v4
takes a preset of off, mild, or strong on graph.get_context. There is
no object with strength, half-lives, or per-scope bands.
Python
TypeScript
Go
v3. Auto search accepted a recency_bias object.
v4. Assemble context with a preset.
Document identifiers
v4 rejects a document_id that contains /, ?, #, or an ASCII control
character, and rejects a whitespace-only value. v3 accepted any string of 1 to
100 characters.
Repeated creates
In v3, a retried create with the same identifier failed because the
identifier was already in use. v4 creates carry no identifier, so each
create call makes a new resource. The SDK sends an idempotency key on its
own retries, so a retried request does not make a second resource. You can
also pass an Idempotency-Key on a create that your application can
repeat. See
Move resource creation to v4 for the
pattern.
Defaults and caps
Search limit defaults to 50, where v3 defaulted to 10. The maximum is 50 in
both. A graph artifact list defaults to 50 with a maximum of 100.
What has no v4 equivalent
graph.detect_patterns/graph.detectPatterns/Graph.DetectPatterns. The experimental endpoint is removed.- User-group policy sets (
list_policy_sets,attach_policy_set,detach_policy_setand the TypeScript / Go equivalents). Manage policy sets from the Zep dashboard.