Packages

A complete, production-grade Elixir client for the Zep AI memory API (Threads, Users, Graph, Context, Batch, Project, Task) with typed structs, retries, telemetry, and strict dialyzer/credo compliance.

Current section

Files

Jump to
zep README.md
Raw

README.md

# Zep
A complete, production-grade Elixir client for the [Zep](https://www.getzep.com)
AI memory API — Threads, Users, Context Templates, the temporal Knowledge
Graph, Batch ingestion, Projects, and Tasks — with typed structs, automatic
retries, `:telemetry` instrumentation, and a strict `credo --strict` /
`dialyzer` toolchain.
## Installation
```elixir
def deps do
[
{:zep, "~> 1.0.0"}
]
end
```
## Configuration
```elixir
# config/config.exs
config :zep,
api_key: System.get_env("ZEP_API_KEY"),
base_url: "https://api.getzep.com",
receive_timeout: 30_000,
max_retries: 2
```
Or build a client explicitly (useful for multi-tenant apps talking to more
than one Zep project):
```elixir
client = Zep.Client.new(api_key: "z_...")
```
Every resource function accepts either a `%Zep.Client{}` or a plain keyword
list of client options as its first argument — the latter resolves
configuration from `Application` env / the `ZEP_API_KEY` environment
variable on each call. See `Zep.Config` for the full precedence order.
## Quick start
```elixir
client = Zep.Client.new(api_key: "z_...")
{:ok, user} = Zep.User.add(client, "user-1", first_name: "Ada", last_name: "Lovelace")
{:ok, thread} = Zep.Thread.create(client, "thread-1", "user-1")
{:ok, _} =
Zep.Thread.add_messages(client, "thread-1", [
%{role: "user", content: "Hi, I'm Ada. I love analytical engines."}
])
{:ok, %{context: context}} = Zep.Thread.get_user_context(client, "thread-1")
```
## Resources
| Module | Covers |
|---|---|
| `Zep.Thread` | list, create, delete, get/add messages, user context, rolling summary |
| `Zep.Thread.Message` | update an individual message by `uuid` |
| `Zep.User` | add, get, update, delete, `list_ordered`, `get_threads`, summary instructions |
| `Zep.Context` | context-rendering templates (list/get/create/update/delete) |
| `Zep.Graph` | create/get/update/delete/list, `add`, `add_batch`, `add_fact_triple`, `clone`, `search`, ontology, pattern detection, cache warming |
| `Zep.Graph.Edge` | individual fact/edge CRUD |
| `Zep.Graph.Episode` | individual episode CRUD + streaming |
| `Zep.Graph.Node` | individual node CRUD + connected edges |
| `Zep.Graph.CustomInstructions` | per-graph extraction instructions |
| `Zep.Graph.Observations` | standalone timestamped graph notes |
| `Zep.Graph.ThreadSummaries` | thread summaries folded into a graph |
| `Zep.Project` | current project settings |
| `Zep.Task` | async task status + polling (`await/3`) |
| `Zep.Batch` | bulk ingestion jobs: create → add → process → poll/list |
| `Zep.Webhook` | verify incoming Zep webhook deliveries (HMAC-SHA256 / Svix) |
## Pagination
Paginated list endpoints (`Zep.Thread.list/2`, `Zep.User.list_ordered/2`,
`Zep.Graph.list_all/2`) each have a lazy `stream/2` counterpart that walks
every page automatically:
```elixir
Zep.Thread.stream(client, page_size: 100)
|> Stream.filter(&(&1.user_id == "user-1"))
|> Enum.to_list()
```
## Error handling
Every function returns `{:ok, result}` or `{:error, error}`, where `error`
is either a `%Zep.Error{}` (the API responded with a non-2xx status) or a
`%Zep.TransportError{}` (the request never reached the API):
```elixir
case Zep.Thread.get_summary(client, "thread-1") do
{:ok, summary} -> summary
{:error, %Zep.Error{reason: :not_found}} -> nil
{:error, %Zep.Error{reason: :forbidden}} -> {:error, :plan_upgrade_required}
{:error, error} -> raise error
end
```
`Zep.Error` implements the `Exception` behaviour, so it can also be raised
directly. 429 and 5xx responses are retried automatically with exponential
backoff (`:max_retries`, default 2); 4xx errors are not retried.
## Telemetry
Every request emits `[:zep, :request, :start | :stop | :exception]` events
via `:telemetry.span/3`, tagged with `:resource` and `:action` metadata —
attach a handler for logging, metrics, or tracing:
```elixir
:telemetry.attach(
"zep-logger",
[:zep, :request, :stop],
fn _event, measurements, metadata, _config ->
Logger.info("zep.#{metadata.resource}.#{metadata.action} -> #{metadata.status} (#{measurements.duration})")
end,
nil
)
```
## Batch ingestion
The Batch API is a three-step lifecycle - create an empty batch, add up to
500 items per call (up to 50,000 per batch), then start processing:
```elixir
{:ok, batch} = Zep.Batch.create(client, metadata: %{description: "Support backfill"})
{:ok, _} =
Zep.Batch.add(client, batch.batch_id, [
%{type: :graph_episode, user_id: "alice", data: "Alice upgraded to Pro.", data_type: :text},
%{type: :thread_message, thread_id: "alice-support-42", content: "Dashboard won't load.", role: "user", name: "Alice"}
])
{:ok, _} = Zep.Batch.process(client, batch.batch_id)
{:ok, final} = Zep.Batch.await(client, batch.batch_id)
```
`Zep.Graph.add_batch/3` remains fine for small (≤20 episodes), same-graph,
order-independent batches. `Zep.Thread.add_messages_batch/4` and the
pre-2026 single-call `Zep.Graph.add_batch/3` for large jobs are both
deprecated in favor of `Zep.Batch` for anything larger or mixing
destinations. For batches with thousands of items, prefer subscribing to
the `ingest.batch.completed` webhook over polling `await/3`.
## Webhooks
Zep signs webhook deliveries via Svix (HMAC-SHA256). Endpoint management
(creating/rotating endpoints) happens in the Zep dashboard; `Zep.Webhook`
verifies deliveries your server receives:
```elixir
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
headers = %{
"svix-id" => List.first(Plug.Conn.get_req_header(conn, "svix-id")),
"svix-timestamp" => List.first(Plug.Conn.get_req_header(conn, "svix-timestamp")),
"svix-signature" => List.first(Plug.Conn.get_req_header(conn, "svix-signature"))
}
case Zep.Webhook.verify(raw_body, headers, signing_secret) do
:ok -> handle_event(Jason.decode!(raw_body))
{:error, reason} -> send_resp(conn, 400, "invalid signature: #{reason}")
end
```
Always verify against the **raw** request body - many web frameworks parse
JSON before your handler runs, which breaks verification.
## Development
```
mix deps.get
mix test
mix credo --strict
mix dialyzer
```