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

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

# Check Data Ingestion Status

> **Tip**
>
> For production use cases, we recommend [webhooks](/webhooks) instead of polling. Zep pushes an `episode.processed` event (and `ingest.batch.completed` for batch operations) as soon as processing finishes, so your application reacts immediately without the latency and wasted requests of polling in a loop. The polling approach shown in this recipe is best suited to testing and development.

Data added to Zep is processed asynchronously and can take a few seconds to a few minutes to finish processing. This recipe shows how to check whether data upload operations are finished processing.

Zep provides these methods for checking data ingestion status:

* **Task polling**: Use `client.task.get()` to check the status of clone operations, direct node additions, and fact triple additions
* **Episode polling**: Use `graph.episode.get()` to check individual episode processing status
* **Batch status**: Use `batch.get()` and `batch.list_items()` for Batch API imports

For tracking large historical ingestions, see the [Batch API](/adding-batch-data), which has its own progress reporting via `batch.get` and per-item status via `batch.list_items`.

## Submit many episodes, poll once

When you add several episodes to one graph at once — for example during a [backfill](/adding-batch-data#backfills) — submit every episode without polling between adds. To know when the import is retrievable, poll the episode that is **ingested last** — see the table below for which episode that is on each path.

| Ingestion method                                                             | Ingestion order                                          |
| ---------------------------------------------------------------------------- | -------------------------------------------------------- |
| [`graph.episode.add`](/adding-business-data) — one episode per call          | **Submission order**                                     |
| [`thread.add_messages`](/adding-messages) — one message per call             | **Submission order**                                     |
| [`thread.add_messages`](/adding-messages) — multiple messages in one request | **Request array order**                                  |
| [Batch API](/adding-batch-data) — `batch.add_items` + `batch.process`        | **`sequence_index`** (submission order within the batch) |

> **Note**
>
> Episodes that share a `document_id` or thread can accumulate in groups of up to 4 before dispatch. Within each group, episodes process in ascending **`created_at`** order; the groups themselves still dispatch in submission order.

### `graph.episode.add` and single-message `thread.add_messages`

Plain `graph.episode.add` calls (text or JSON, no `document_id`) dispatch **one episode per submit** in submission order. After every submit returns, poll the **last-submitted** episode's `processed` status.

Single-message `thread.add_messages` calls follow the same rule between groups. Poll the last-submitted message.

Scale your poll timeout and interval with the total episode count in that graph. As a starting point, allow several seconds per episode — a 100-episode import often needs minutes, not seconds.

A `processed: true` on that episode means extraction has reached that point. It does not guarantee every episode succeeded — check for failed episodes before you treat the import as complete.

Do not mix Batch API imports with `graph.episode.add` into the same graph and expect one poll to cover both paths. They are not globally serialized with each other.

### Multi-message `thread.add_messages`

When you pass multiple messages in one request, those messages run in **request array order**, not `reference_time` order. Poll the **last message in the last request**.

### Batch API and `zep-ingest`

Batch items are ingested in **submission order** (`sequence_index`). Monitor the batch with `batch.get`, `batch.list_items`, or `IngestResult.wait()` — not individual episode polling. See [Tracking progress](/adding-batch-data#tracking-progress) and [Monitor zep-ingest with `IngestResult`](#monitor-zep-ingest-with-ingestresult) below.

[`zep-ingest`](/zep-ingest) uses the Batch API on most deployments. Submit every source without waiting between them, then call `wait()` once (or monitor with `batch.get`). Use `failed_items()` to inspect per-item failures.

## Monitor zep-ingest with `IngestResult`

#### [Create an Ingestion Pipeline](/zep-ingest)

Prepare, preview, submit, and monitor an ordered import using one Python pipeline.

`zep-ingest` returns an `IngestResult` for Batch, episode, and task-backed operations:

```python
result.status
result.refresh()
result.wait(timeout=3600)
result.failed_items()
result.raise_for_status()
```

The result records the identifiers needed to continue monitoring in another process:

```python
print(result.batch_ids)
print(result.episode_uuids)
print(result.task_ids)
```

Persist the appropriate identifiers and reconstruct the result later:

```python
from zep_ingest import IngestResult

batch_result = IngestResult.from_batch_ids(client, saved_batch_ids)
batch_result.wait(timeout=3600)

task_result = IngestResult.from_task_ids(client, saved_task_ids)
task_result.wait(timeout=3600)
```

`wait()` polls the Batch, episode, or task handles in the result until they reach a terminal state. When Zep accepts a write but returns no handle for it, the result counts those items in `untracked_items`, `status` reports `untracked`, and `wait()` raises `IngestUntrackedError` rather than polling indefinitely. The submission succeeded in that case; only server-side extraction cannot be tracked, so confirm the data with a read of your own.

Search indexing can take additional time. `search_when_ready()` retries until a query returns any result or reaches the timeout; it does not verify that a specific imported record produced the result. Use a query unique to the imported data when checking indexing readiness.

## Checking Operation Status with Task Polling

When an operation returns a `task`, you can poll for completion status using `client.task.get()`. Examples of task-backed operations include:

* `graph.clone()` - Graph cloning operations
* `graph.node.add()` - Direct node additions
* `graph.edge.add()` - Custom fact triple additions
* `graph.hyperedge.add()` - Hyperedge additions

The pattern is the same for each operation: read the task UUID from `task.uuid` in the response, then poll `client.task.get(task_uuid)` until `status` is `succeeded`, `partial`, or `failed`.

Use `task.list` to read a page of the tasks of the project. The newest task comes first. The SDK paginates with `limit` and `cursor`.

**`Python`**

```python Python
tasks = client.task.list(limit=50)
```

**`TypeScript`**

```typescript TypeScript
const tasks = await client.task.list({ limit: 50 });
```

**`Go`**

```go Go
tasks, err := client.Task.List(ctx, &zep.TaskListRequest{
    Limit: zep.Int(50),
})
if err != nil {
    log.Fatal(err)
}
```

## Checking Individual Episode Status with Episode Polling

Use `graph.episode.get()` to check whether one episode has finished processing. When you submit multiple episodes to one graph, poll the last-submitted episode — see [Submit many episodes, poll once](#submit-many-episodes-poll-once).

First, let's create a user. Zep gives the user a UUID and a user graph UUID. Store both next to your own user ID:

```python
import os
import time
from dotenv import find_dotenv, load_dotenv
from zep_cloud.client import Zep

load_dotenv(dotenv_path=find_dotenv())

client = Zep(api_key=os.environ.get("ZEP_API_KEY"))
user = client.user.create(
    first_name="John",
    last_name="Doe",
    email="john.doe@example.com",
)

# Store these UUIDs next to your own user ID
zep_user_uuid = user.uuid_
zep_graph_uuid = user.graph_uuid
print(f"Created user {zep_user_uuid}")
```

```typescript
import { ZepClient } from "@getzep/zep-cloud";
import * as dotenv from "dotenv";

// Load environment variables
dotenv.config();

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

async function main() {
  // Create a user
  const user = await client.user.create({
    firstName: "John",
    lastName: "Doe",
    email: "john.doe@example.com"
  });

  // Store these UUIDs next to your own user ID
  const zepUserUuid = user.uuid!;
  const zepGraphUuid = user.graphUuid!;
  console.log(`Created user ${zepUserUuid}`);
```

```go
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	zep "github.com/getzep/zep-go/v4"
	zepclient "github.com/getzep/zep-go/v4/client"
	"github.com/getzep/zep-go/v4/graph"
	"github.com/getzep/zep-go/v4/option"
	"github.com/joho/godotenv"
)

func main() {
	// Load .env file
	err := godotenv.Load()
	if err != nil {
		fmt.Println("Warning: Error loading .env file:", err)
		// Continue execution as environment variables might be set in the system
	}

	// Get API key from environment variable
	apiKey := os.Getenv("ZEP_API_KEY")
	if apiKey == "" {
		fmt.Println("ZEP_API_KEY environment variable is not set")
		return
	}

	// Initialize Zep client
	client := zepclient.NewClient(
		option.WithAPIKey(apiKey),
	)

	// Create context
	ctx := context.Background()

	// Create a user
	user, err := client.User.Create(ctx, &zep.CreateUserRequest{
		FirstName: zep.String("John"),
		LastName:  zep.String("Doe"),
		Email:     zep.String("john.doe@example.com"),
	})
	if err != nil {
		fmt.Printf("Error creating user: %v\n", err)
		return
	}

	// Store these UUIDs next to your own user ID
	zepUserUUID := *user.UUID
	zepGraphUUID := *user.GraphUUID
	fmt.Printf("Created user %s\n", zepUserUUID)
```

Now, let's add some data and immediately try to search for that data; because data added to Zep is processed asynchronously and can take a few seconds to a few minutes to finish processing, our search results do not have the data we just added:

```python
result = client.graph.episode.add(
    zep_graph_uuid,
    type="text",
    data="The user is an avid fan of Eric Clapton",
)
episode_uuid = result.episode.uuid_

search_results = client.graph.search_nodes(
    zep_graph_uuid,
    query="Eric Clapton",
    limit=1,
    reranker="cross_encoder",
)

print(search_results.items)
```

```typescript
  // Add episode to graph
  const result = await client.graph.episode.add(zepGraphUuid, {
    type: "text",
    data: "The user is an avid fan of Eric Clapton"
  });
  const episodeUuid = result.episode!.uuid!;

  // Search for nodes related to Eric Clapton
  const searchResults = await client.graph.searchNodes(zepGraphUuid, {
    limit: 1,
    body: {
      query: "Eric Clapton",
      reranker: "cross_encoder"
    }
  });

  console.log(searchResults.data);
```

```go
	// Add a new episode to the graph
	result, err := client.Graph.Episode.Add(ctx, zepGraphUUID, &graph.AddEpisodeRequest{
		Type: graph.V4AddEpisodeRequestTypeText.Ptr(),
		Data: "The user is an avid fan of Eric Clapton",
	})
	if err != nil {
		fmt.Printf("Error adding episode to graph: %v\n", err)
		return
	}
	episodeUUID := *result.Episode.UUID

	// Search for the data
	searchResults, err := client.Graph.SearchNodes(ctx, zepGraphUUID, &zep.GraphSearchNodesRequest{
		Limit: zep.Int(1),
		Body: &zep.SearchRequest{
			Query:    "Eric Clapton",
			Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
		},
	})
	if err != nil {
		fmt.Printf("Error searching graph: %v\n", err)
		return
	}

	fmt.Println(searchResults.Results)
```

```text
[]
```

We can check the status of the episode to see when it has finished processing, using the episode UUID that the `graph.episode.add` method returns and the `graph.episode.get` method:

```python
while True:
    episode = client.graph.episode.get(zep_graph_uuid, episode_uuid)
    if episode.processed:
        print("Episode processed successfully")
        break
    print("Waiting for episode to process...")
    time.sleep(10)
```

```typescript
  // Check if episode is processed
  const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

  let processedEpisode = await client.graph.episode.get(zepGraphUuid, episodeUuid);

  while (!processedEpisode.processed) {
    console.log("Waiting for episode to process...");
    await sleep(10000); // Sleep for 10 seconds
    processedEpisode = await client.graph.episode.get(zepGraphUuid, episodeUuid);
  }

  console.log("Episode processed successfully");
```

```go
	// Wait for the episode to be processed
	for {
		episodeStatus, err := client.Graph.Episode.Get(
			ctx,
			zepGraphUUID,
			episodeUUID,
		)
		if err != nil {
			fmt.Printf("Error getting episode: %v\n", err)
			return
		}

		if episodeStatus.Processed != nil && *episodeStatus.Processed {
			fmt.Println("Episode processed successfully")
			break
		}

		fmt.Println("Waiting for episode to process...")
		time.Sleep(10 * time.Second)
	}
```

```text
Waiting for episode to process...
Waiting for episode to process...
Waiting for episode to process...
Waiting for episode to process...
Waiting for episode to process...
Episode processed successfully
```

Now that the episode has finished processing, we can search for the data we just added, and this time we get a result:

```python
search_results = client.graph.search_nodes(
    zep_graph_uuid,
    query="Eric Clapton",
    limit=1,
    reranker="cross_encoder",
)

print(search_results.items)
```

```typescript
  // Search again after processing
  const finalSearchResults = await client.graph.searchNodes(zepGraphUuid, {
    limit: 1,
    body: {
      query: "Eric Clapton",
      reranker: "cross_encoder"
    }
  });

  console.log(finalSearchResults.data);
}

// Execute the main function
main().catch(error => console.error("Error:", error));
```

```go
	// Search again after processing
	searchResults, err = client.Graph.SearchNodes(ctx, zepGraphUUID, &zep.GraphSearchNodesRequest{
		Limit: zep.Int(1),
		Body: &zep.SearchRequest{
			Query:    "Eric Clapton",
			Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
		},
	})
	if err != nil {
		fmt.Printf("Error searching graph: %v\n", err)
		return
	}

	fmt.Println(searchResults.Results)
}
```

```text
[Node(attributes={'category': 'Music'}, created_at='2025-04-05T00:17:59.66565Z', episode_uuids=['2c5f3d8e-7a41-4b9e-9d0c-5e1f6a7b8c9d'], episode_uuids_truncated=False, graph_uuid='6961b53f-df05-48bb-9b8d-b2702dd72045', labels=['Entity', 'Preference'], name='Eric Clapton', summary='The user is an avid fan of Eric Clapton.', uuid_='98808054-38ad-4cba-ba07-acd5f7a12bc0')]
```