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

# Advanced Context Block construction

> **Info**
>
> This guide covers building context blocks from scratch using graph search for maximum customization. See [Choosing a retrieval method](/retrieving-context#choosing-a-retrieval-method) for a comparison of all three context retrieval approaches.

When [searching the graph](/searching-the-graph) instead of [using Zep's Context Block](/retrieving-context#zeps-context-block), you need to use the search results to create a custom context block. In this recipe, we will demonstrate how to build a custom context block using the [graph search API](/searching-the-graph). We will also use the [custom entity and edge types feature](/customizing-graph-structure#custom-entity-and-edge-types), though using this feature is optional.

> **Note**
>
> **Include fact validity.** When you build a custom context block, include each fact's `valid_at` and `invalid_at` dates, and clearly mark any fact with a non-null `invalid_at` as no longer valid. This lets the agent tell current facts from outdated ones instead of treating every fact as true now. The examples below format validity as a date range: a range ending in `present` is currently valid, and a past end date means the fact is no longer valid.

# Add data

First, we define our [custom entity and edge types](/customizing-graph-structure#definition-1), create a user, and add some example data. Zep generates the user UUID, the user graph UUID, and the thread UUIDs. The example stores them in variables. In your application, store them in your database next to your own user and conversation IDs:

```python
from pydantic import Field
from zep_cloud.ontology import EdgeModel, EntityBoolean, EntityModel, EntityText, build_ontology
from zep_cloud.types import AddMessage, EdgeSourceTarget

class Restaurant(EntityModel):
    """
    Represents a specific restaurant.
    """
    cuisine_type: EntityText = Field(description="The cuisine type of the restaurant, for example: American, Mexican, Indian, etc.", default=None)
    dietary_accommodation: EntityText = Field(description="The dietary accommodation of the restaurant, if any, for example: vegetarian, vegan, etc.", default=None)

class RestaurantVisit(EdgeModel):
    """
    Represents the fact that the user visited a restaurant.
    """
    restaurant_name: EntityText = Field(description="The name of the restaurant the user visited", default=None)

class DietaryPreference(EdgeModel):
    """
    Represents the fact that the user has a dietary preference or dietary restriction.
    """
    preference_type: EntityText = Field(description="Preference type of the user: anything, vegetarian, vegan, peanut allergy, etc.", default=None)
    allergy: EntityBoolean = Field(description="Whether this dietary preference represents a user allergy: True or false", default=None)

entity_types, edge_types = build_ontology(
    entities={
        "Restaurant": Restaurant,
    },
    edges={
        "RESTAURANT_VISIT": (
            RestaurantVisit,
            [EdgeSourceTarget(source="User", target="Restaurant")]
        ),
        "DIETARY_PREFERENCE": (
            DietaryPreference,
            [EdgeSourceTarget(source="User")]
        ),
    }
)

# Zep generates the user UUID and the user graph UUID.
# Store them in your database next to your own user ID.
user = client.user.create(first_name="John", last_name="Doe", email="john.doe@example.com")
zep_user_uuid = user.uuid_
zep_graph_uuid = user.graph_uuid

client.graph.set_ontology(zep_graph_uuid, entity_types=entity_types, edge_types=edge_types)

messages_thread1 = [
    AddMessage(content="Take me to a lunch place", role="user", name="John Doe"),
    AddMessage(content="How about Panera Bread, Chipotle, or Green Leaf Cafe, which are nearby?", role="assistant", name="Assistant"),
    AddMessage(content="Do any of those have vegetarian options? I’m vegetarian", role="user", name="John Doe"),
    AddMessage(content="Yes, Green Leaf Cafe has vegetarian options", role="assistant", name="Assistant"),
    AddMessage(content="Let’s go to Green Leaf Cafe", role="user", name="John Doe"),
    AddMessage(content="Navigating to Green Leaf Cafe", role="assistant", name="Assistant"),
]

messages_thread2 = [
    AddMessage(content="Take me to dessert", role="user", name="John Doe"),
    AddMessage(content="How about getting some ice cream?", role="assistant", name="Assistant"),
    AddMessage(content="I can't have ice cream, I'm lactose intolerant, but I'm craving a chocolate chip cookie", role="user", name="John Doe"),
    AddMessage(content="Sure, there's Insomnia Cookies nearby.", role="assistant", name="Assistant"),
    AddMessage(content="Perfect, let's go to Insomnia Cookies", role="user", name="John Doe"),
    AddMessage(content="Navigating to Insomnia Cookies.", role="assistant", name="Assistant"),
]

# Zep generates each thread UUID. Store it next to your own conversation ID.
zep_thread1_uuid = client.thread.create(user_uuid=zep_user_uuid).uuid_
zep_thread2_uuid = client.thread.create(user_uuid=zep_user_uuid).uuid_

client.thread.add_messages(zep_thread1_uuid, messages=messages_thread1, ignore_roles=["assistant"])
client.thread.add_messages(zep_thread2_uuid, messages=messages_thread2, ignore_roles=["assistant"])
```

```typescript
import { buildOntology, entityFields, Zep } from "@getzep/zep-cloud";
import type { EdgeDefinition, EntityDefinition } from "@getzep/zep-cloud";

const RestaurantSchema: EntityDefinition = {
    description: "Represents a specific restaurant.",
    fields: {
        cuisine_type: entityFields.text("The cuisine type of the restaurant, for example: American, Mexican, Indian, etc."),
        dietary_accommodation: entityFields.text("The dietary accommodation of the restaurant, if any, for example: vegetarian, vegan, etc."),
    },
};

const RestaurantVisit: EdgeDefinition = {
    description: "Represents the fact that the user visited a restaurant.",
    fields: {
        restaurant_name: entityFields.text("The name of the restaurant the user visited"),
    },
    sourceTargets: [
        { source: "User", target: "Restaurant" },
    ],
};

const DietaryPreference: EdgeDefinition = {
    description: "Represents the fact that the user has a dietary preference or dietary restriction.",
    fields: {
        preference_type: entityFields.text("Preference type of the user: anything, vegetarian, vegan, peanut allergy, etc."),
        allergy: entityFields.boolean("Whether this dietary preference represents a user allergy: True or false"),
    },
    sourceTargets: [
        { source: "User" },
    ],
};

const ontology = buildOntology({
    entities: {
        Restaurant: RestaurantSchema,
    },
    edges: {
        RESTAURANT_VISIT: RestaurantVisit,
        DIETARY_PREFERENCE: DietaryPreference,
    },
});

// Zep generates the user UUID and the user graph UUID.
// Store them in your database next to your own user ID.
const user = await client.user.create({ firstName: "John", lastName: "Doe", email: "john.doe@example.com" });
const zepUserUuid = user.uuid!;
const zepGraphUuid = user.graphUuid!;

await client.graph.setOntology(zepGraphUuid, ontology);

const messagesThread1: Zep.AddMessage[] = [
    { content: "Take me to a lunch place", role: "user", name: "John Doe" },
    { content: "How about Panera Bread, Chipotle, or Green Leaf Cafe, which are nearby?", role: "assistant", name: "Assistant" },
    { content: "Do any of those have vegetarian options? I’m vegetarian", role: "user", name: "John Doe" },
    { content: "Yes, Green Leaf Cafe has vegetarian options", role: "assistant", name: "Assistant" },
    { content: "Let’s go to Green Leaf Cafe", role: "user", name: "John Doe" },
    { content: "Navigating to Green Leaf Cafe", role: "assistant", name: "Assistant" },
];

const messagesThread2: Zep.AddMessage[] = [
    { content: "Take me to dessert", role: "user", name: "John Doe" },
    { content: "How about getting some ice cream?", role: "assistant", name: "Assistant" },
    { content: "I can't have ice cream, I'm lactose intolerant, but I'm craving a chocolate chip cookie", role: "user", name: "John Doe" },
    { content: "Sure, there's Insomnia Cookies nearby.", role: "assistant", name: "Assistant" },
    { content: "Perfect, let's go to Insomnia Cookies", role: "user", name: "John Doe" },
    { content: "Navigating to Insomnia Cookies.", role: "assistant", name: "Assistant" },
];

// Zep generates each thread UUID. Store it next to your own conversation ID.
const zepThread1Uuid = (await client.thread.create({ userUuid: zepUserUuid })).uuid!;
const zepThread2Uuid = (await client.thread.create({ userUuid: zepUserUuid })).uuid!;

await client.thread.addMessages(zepThread1Uuid, { messages: messagesThread1, ignoreRoles: ["assistant"] });
await client.thread.addMessages(zepThread2Uuid, { messages: messagesThread2, ignoreRoles: ["assistant"] });
```

```go
import (
	zep "github.com/getzep/zep-go/v4"
)

type Restaurant struct {
	zep.EntityBase       `description:"Represents a specific restaurant."`
	CuisineType          string `description:"The cuisine type of the restaurant, for example: American, Mexican, Indian, etc." json:"cuisine_type,omitempty"`
	DietaryAccommodation string `description:"The dietary accommodation of the restaurant, if any, for example: vegetarian, vegan, etc." json:"dietary_accommodation,omitempty"`
}

type RestaurantVisit struct {
	zep.EdgeBase   `description:"Represents the fact that the user visited a restaurant."`
	RestaurantName string `description:"The name of the restaurant the user visited" json:"restaurant_name,omitempty"`
}

type DietaryPreference struct {
	zep.EdgeBase   `description:"Represents the fact that the user has a dietary preference or dietary restriction."`
	PreferenceType string `description:"Preference type of the user: anything, vegetarian, vegan, peanut allergy, etc." json:"preference_type,omitempty"`
	Allergy        bool   `description:"Whether this dietary preference represents a user allergy: True or false" json:"allergy,omitempty"`
}

ontology, err := zep.BuildOntology(
	zep.Entities{
		"Restaurant": Restaurant{},
	},
	zep.Edges{
		"RESTAURANT_VISIT": {
			Model: RestaurantVisit{},
			SourceTargets: []*zep.EdgeSourceTarget{
				{Source: zep.String("User"), Target: zep.String("Restaurant")},
			},
		},
		"DIETARY_PREFERENCE": {
			Model: DietaryPreference{},
			SourceTargets: []*zep.EdgeSourceTarget{
				{Source: zep.String("User")},
			},
		},
	},
)
if err != nil {
	fmt.Printf("Error building ontology: %v\n", err)
	return
}

// Zep generates the user UUID and the user graph UUID.
// Store them in your database next to your own user ID.
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
}
zepUserUUID := *user.UUID
zepGraphUUID := *user.GraphUUID

_, err = client.Graph.SetOntology(ctx, zepGraphUUID, ontology)
if err != nil {
	fmt.Printf("Error setting ontology: %v\n", err)
	return
}

userRole := zep.RoleTypeUser.Ptr()
assistantRole := zep.RoleTypeAssistant.Ptr()

messagesThread1 := []*zep.AddMessage{
	{Content: zep.String("Take me to a lunch place"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("How about Panera Bread, Chipotle, or Green Leaf Cafe, which are nearby?"), Role: assistantRole, Name: zep.String("Assistant")},
	{Content: zep.String("Do any of those have vegetarian options? I'm vegetarian"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("Yes, Green Leaf Cafe has vegetarian options"), Role: assistantRole, Name: zep.String("Assistant")},
	{Content: zep.String("Let's go to Green Leaf Cafe"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("Navigating to Green Leaf Cafe"), Role: assistantRole, Name: zep.String("Assistant")},
}
messagesThread2 := []*zep.AddMessage{
	{Content: zep.String("Take me to dessert"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("How about getting some ice cream?"), Role: assistantRole, Name: zep.String("Assistant")},
	{Content: zep.String("I can't have ice cream, I'm lactose intolerant, but I'm craving a chocolate chip cookie"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("Sure, there's Insomnia Cookies nearby."), Role: assistantRole, Name: zep.String("Assistant")},
	{Content: zep.String("Perfect, let's go to Insomnia Cookies"), Role: userRole, Name: zep.String("John Doe")},
	{Content: zep.String("Navigating to Insomnia Cookies."), Role: assistantRole, Name: zep.String("Assistant")},
}

// Zep generates each thread UUID. Store it next to your own conversation ID.
thread1, err := client.Thread.Create(ctx, &zep.CreateThreadRequest{UserUUID: zepUserUUID})
if err != nil {
	fmt.Printf("Error creating thread 1: %v\n", err)
	return
}
thread2, err := client.Thread.Create(ctx, &zep.CreateThreadRequest{UserUUID: zepUserUUID})
if err != nil {
	fmt.Printf("Error creating thread 2: %v\n", err)
	return
}
zepThread1UUID := *thread1.UUID
zepThread2UUID := *thread2.UUID

_, err = client.Thread.AddMessages(ctx, zepThread1UUID, &zep.AddMessagesRequest{
	Messages:    messagesThread1,
	IgnoreRoles: []string{"assistant"},
})
if err != nil {
	fmt.Printf("Error adding messages to thread 1: %v\n", err)
	return
}

_, err = client.Thread.AddMessages(ctx, zepThread2UUID, &zep.AddMessagesRequest{
	Messages:    messagesThread2,
	IgnoreRoles: []string{"assistant"},
})
if err != nil {
	fmt.Printf("Error adding messages to thread 2: %v\n", err)
	return
}
```

# Example 1: Basic custom context block

## Search

For a basic custom context block, we search the graph for edges and nodes relevant to our custom query string, which typically represents a user message. Note that the default [Context Block](/retrieving-context#zeps-context-block) returned by `thread.get_context` uses the past few messages as the query instead.

> **Tip**
>
> Run these searches in parallel with the [asynchronous Python client](/quick-start-guide#initialize-the-zep-client), TypeScript promises, or goroutines.

```python
query = "Find some food around here"

search_results_nodes = client.graph.search_nodes(
    zep_graph_uuid,
    query=query,
    reranker="cross_encoder",
    limit=10,
)
search_results_edges = client.graph.search_edges(
    zep_graph_uuid,
    query=query,
    reranker="cross_encoder",
    limit=10,
)
```

```typescript
const query = "Find some food around here";

const searchResultsNodes = await client.graph.searchNodes(zepGraphUuid, {
    limit: 10,
    body: { query, reranker: "cross_encoder" },
});

const searchResultsEdges = await client.graph.searchEdges(zepGraphUuid, {
    limit: 10,
    body: { query, reranker: "cross_encoder" },
});
```

```go
import (
	zep "github.com/getzep/zep-go/v4"
)

query := "Find some food around here"

searchResultsNodes, err := client.Graph.SearchNodes(ctx, zepGraphUUID, &zep.GraphSearchNodesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:    query,
		Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
	},
})
if err != nil {
	fmt.Printf("Error searching nodes: %v\n", err)
	return
}

searchResultsEdges, err := client.Graph.SearchEdges(ctx, zepGraphUUID, &zep.GraphSearchEdgesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:    query,
		Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
	},
})
if err != nil {
	fmt.Printf("Error searching edges: %v\n", err)
	return
}
```

## Build the context block

Using the search results and a few helper functions, we can build the context block. Note that for nodes, we typically want to unpack the node name and node summary, and for edges we typically want to unpack the fact and the temporal validity information:

```python
from zep_cloud.types import Edge, Node

CONTEXT_STRING_TEMPLATE = """
FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>
"""


def format_fact(edge: Edge) -> str:
    valid_at = edge.valid_at if edge.valid_at is not None else "date unknown"
    invalid_at = edge.invalid_at if edge.invalid_at is not None else "present"
    formatted_fact = f"  - {edge.fact} (Date range: {valid_at} - {invalid_at})"
    return formatted_fact

def format_entity(node: Node) -> str:
    formatted_entity = f"  - {node.name}: {node.summary}"
    return formatted_entity

def compose_context_block(edges: list[Edge], nodes: list[Node]) -> str:
    facts = [format_fact(edge) for edge in edges]
    entities = [format_entity(node) for node in nodes]
    return CONTEXT_STRING_TEMPLATE.format(facts='\n'.join(facts), entities='\n'.join(entities))

# Each search returns one page of results. items holds the results of that page.
edges = search_results_edges.items or []
nodes = search_results_nodes.items or []

context_block = compose_context_block(edges, nodes)
print(context_block)
```

```typescript
import type { Zep } from "@getzep/zep-cloud";

const CONTEXT_STRING_TEMPLATE_1 = `FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>
# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>`;

function formatFact(edge: Zep.Edge): string {
    const validAt = edge.validAt ?? "date unknown";
    const invalidAt = edge.invalidAt ?? "present";
    return `  - ${edge.fact} (Date range: ${validAt} - ${invalidAt})`;
}

function formatEntity(node: Zep.Node): string {
    return `  - ${node.name}: ${node.summary}`;
}

function composeContextBlock1(edges: Zep.Edge[], nodes: Zep.Node[]): string {
    const facts = edges.map(formatFact).join('\n');
    const entities = nodes.map(formatEntity).join('\n');
    return CONTEXT_STRING_TEMPLATE_1
        .replace('{facts}', facts)
        .replace('{entities}', entities);
}

// Each search returns one page of results. data holds the results of that page.
const edges: Zep.Edge[] = searchResultsEdges.data;
const nodes: Zep.Node[] = searchResultsNodes.data;

const contextBlock1 = composeContextBlock1(edges, nodes);
console.log(contextBlock1);
```

```go
import (
	"strings"

	zep "github.com/getzep/zep-go/v4"
)

valueOrEmpty := func(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

const CONTEXT_STRING_TEMPLATE_1 = `FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>
`

formatFact := func(edge *zep.Edge) string {
	validAt := "date unknown"
	if edge.ValidAt != nil && *edge.ValidAt != "" {
		validAt = *edge.ValidAt
	}
	invalidAt := "present"
	if edge.InvalidAt != nil && *edge.InvalidAt != "" {
		invalidAt = *edge.InvalidAt
	}
	return fmt.Sprintf("  - %s (Date range: %s - %s)", valueOrEmpty(edge.Fact), validAt, invalidAt)
}

formatEntity := func(node *zep.Node) string {
	return fmt.Sprintf("  - %s: %s", valueOrEmpty(node.Name), valueOrEmpty(node.Summary))
}

composeContextBlock1 := func(edges []*zep.Edge, nodes []*zep.Node) string {
	var facts []string
	for _, edge := range edges {
		facts = append(facts, formatFact(edge))
	}
	var entities []string
	for _, node := range nodes {
		entities = append(entities, formatEntity(node))
	}
	result := strings.ReplaceAll(CONTEXT_STRING_TEMPLATE_1, "{facts}", strings.Join(facts, "\n"))
	result = strings.ReplaceAll(result, "{entities}", strings.Join(entities, "\n"))
	return result
}

// Each search returns one page of results. Results holds the results of that page.
edges := searchResultsEdges.Results
nodes := searchResultsNodes.Results

contextBlock1 := composeContextBlock1(edges, nodes)
fmt.Println(contextBlock1)
```

```text
FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
  - User wants to go to dessert (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe wants to go to a lunch place (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe said 'Perfect, let's go to Insomnia Cookies' indicating he will visit Insomnia Cookies. (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe said 'Let’s go to Green Leaf Cafe' indicating intention to visit (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe is craving a chocolate chip cookie (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe states that he is vegetarian. (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe is lactose intolerant (Date range: 2025-06-16T02:17:25Z - present)
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
  - lunch place: The entity is a lunch place, but no specific details about its cuisine or dietary accommodations are provided.
  - dessert: The entity 'dessert' refers to a preference related to sweet courses typically served at the end of a meal. The context indicates that the user has expressed an interest in going to a dessert place, but no specific dessert or place has been named. The entity is categorized as a Preference and Entity, but no additional attributes are provided or inferred from the messages.
  - Green Leaf Cafe: Green Leaf Cafe is a restaurant that offers vegetarian options, making it suitable for vegetarian diners.
  - user: The user is John Doe, with the email john.doe@example.com. He has shown interest in visiting Green Leaf Cafe, which offers vegetarian options, and has also expressed a preference for lactose-free options, craving a chocolate chip cookie. The user has decided to go to Insomnia Cookies.
  - vegetarian: The user is interested in lunch places such as Panera Bread, Chipotle, and Green Leaf Cafe. They are specifically looking for vegetarian options at these restaurants.
  - chocolate chip cookie: The entity is a chocolate chip cookie, which the user desires as a snack. The user is lactose intolerant and cannot have ice cream, but is craving a chocolate chip cookie.
  - Insomnia Cookies: Insomnia Cookies is a restaurant that offers cookies, including chocolate chip cookies. The user is interested in a dessert and has chosen to go to Insomnia Cookies. No specific cuisine type or dietary accommodations are mentioned in the messages.
  - lactose intolerant: The entity is a preference indicating lactose intolerance, which is a dietary restriction that prevents the individual from consuming lactose, a sugar found in milk and dairy products. The person is specifically craving a chocolate chip cookie but cannot have ice cream due to lactose intolerance.
  - John Doe: The user is John Doe, with user ID user-34c7a6c1-ded6-4797-9620-8b80a5e7820f, email john.doe@example.com, and role user. He inquired about nearby lunch options and vegetarian choices, and expressed a preference for a chocolate chip cookie due to lactose intolerance.
</ENTITIES>
```

# Example 2: Utilizing custom entity and edge types

## Search

For a custom context block that uses custom entity and edge types, we perform multiple searches (with our custom query string) filtering to the custom entity or edge type we want to include in the context block:

> **Tip**
>
> Run these searches in parallel with the [asynchronous Python client](/quick-start-guide#initialize-the-zep-client), TypeScript promises, or goroutines.

```python
from zep_cloud.types import SearchFilters

query = "Find some food around here"

search_results_restaurant_visits = client.graph.search_edges(
    zep_graph_uuid,
    query=query,
    filters=SearchFilters(edge_types=["RESTAURANT_VISIT"]),
    reranker="cross_encoder",
    limit=10,
)
search_results_dietary_preferences = client.graph.search_edges(
    zep_graph_uuid,
    query=query,
    filters=SearchFilters(edge_types=["DIETARY_PREFERENCE"]),
    reranker="cross_encoder",
    limit=10,
)
search_results_restaurants = client.graph.search_nodes(
    zep_graph_uuid,
    query=query,
    filters=SearchFilters(node_labels=["Restaurant"]),
    reranker="cross_encoder",
    limit=10,
)
```

```typescript
const foodQuery = "Find some food around here";

const searchResultsRestaurantVisits = await client.graph.searchEdges(zepGraphUuid, {
    limit: 10,
    body: {
        query: foodQuery,
        filters: { edgeTypes: ["RESTAURANT_VISIT"] },
        reranker: "cross_encoder",
    },
});

const searchResultsDietaryPreferences = await client.graph.searchEdges(zepGraphUuid, {
    limit: 10,
    body: {
        query: foodQuery,
        filters: { edgeTypes: ["DIETARY_PREFERENCE"] },
        reranker: "cross_encoder",
    },
});

const searchResultsRestaurants = await client.graph.searchNodes(zepGraphUuid, {
    limit: 10,
    body: {
        query: foodQuery,
        filters: { nodeLabels: ["Restaurant"] },
        reranker: "cross_encoder",
    },
});
```

```go
query := "Find some food around here"

searchResultsRestaurantVisits, err := client.Graph.SearchEdges(ctx, zepGraphUUID, &zep.GraphSearchEdgesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:    query,
		Filters:  &zep.SearchFilters{EdgeTypes: []string{"RESTAURANT_VISIT"}},
		Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
	},
})
if err != nil {
	fmt.Printf("Error searching RESTAURANT_VISIT edges: %v\n", err)
	return
}

searchResultsDietaryPreferences, err := client.Graph.SearchEdges(ctx, zepGraphUUID, &zep.GraphSearchEdgesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:    query,
		Filters:  &zep.SearchFilters{EdgeTypes: []string{"DIETARY_PREFERENCE"}},
		Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
	},
})
if err != nil {
	fmt.Printf("Error searching DIETARY_PREFERENCE edges: %v\n", err)
	return
}

searchResultsRestaurants, err := client.Graph.SearchNodes(ctx, zepGraphUUID, &zep.GraphSearchNodesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:    query,
		Filters:  &zep.SearchFilters{NodeLabels: []string{"Restaurant"}},
		Reranker: zep.SearchRequestRerankerCrossEncoder.Ptr(),
	},
})
if err != nil {
	fmt.Printf("Error searching Restaurant nodes: %v\n", err)
	return
}
```

## Build the context block

Using the search results and a few helper functions, we can compose the context block. Note that in this example, we focus on unpacking the custom attributes of the nodes and edges, but this is a design choice that you can experiment with for your use case.

Note also that we designed the context block template around the custom entity and edge types that we are unpacking into the context block:

```python
from zep_cloud.types import Edge, Node

CONTEXT_STRING_TEMPLATE = """
PREVIOUS_RESTAURANT_VISITS, DIETARY_PREFERENCES, and RESTAURANTS represent relevant context to the current conversation.
# These are the most relevant restaurants the user has previously visited
# format: restaurant_name: RESTAURANT_NAME
<PREVIOUS_RESTAURANT_VISITS>
{restaurant_visits}
</PREVIOUS_RESTAURANT_VISITS>

# These are the most relevant dietary preferences of the user, whether they represent an allergy, and their valid date ranges
# format: allergy: True/False; preference_type: PREFERENCE_TYPE (Date range: from - to)
<DIETARY_PREFERENCES>
{dietary_preferences}
</DIETARY_PREFERENCES>

# These are the most relevant restaurants the user has discussed previously
# format: name: RESTAURANT_NAME; cuisine_type: CUISINE_TYPE; dietary_accommodation: DIETARY_ACCOMMODATION
<RESTAURANTS>
{restaurants}
</RESTAURANTS>
"""

def format_edge_with_attributes(edge: Edge, include_timestamps: bool = True) -> str:
    attrs_str = '; '.join(f"{k}: {v}" for k, v in sorted((edge.attributes or {}).items()))
    if include_timestamps:
        valid_at = edge.valid_at if edge.valid_at is not None else "date unknown"
        invalid_at = edge.invalid_at if edge.invalid_at is not None else "present"
        return f"  - {attrs_str} (Date range: {valid_at} - {invalid_at})"
    return f"  - {attrs_str}"

def format_node_with_attributes(node: Node) -> str:
    attributes = {k: v for k, v in (node.attributes or {}).items() if k != "labels"}
    attrs_str = '; '.join(f"{k}: {v}" for k, v in sorted(attributes.items()))
    base = f"  - name: {node.name}; {attrs_str}"
    return base

def compose_context_block(restaurant_visit_edges: list[Edge], dietary_preference_edges: list[Edge], restaurant_nodes: list[Node]) -> str:
    restaurant_visits = [format_edge_with_attributes(edge, include_timestamps=False) for edge in restaurant_visit_edges]
    dietary_preferences = [format_edge_with_attributes(edge, include_timestamps=True) for edge in dietary_preference_edges]
    restaurant_nodes = [format_node_with_attributes(node) for node in restaurant_nodes]
    return CONTEXT_STRING_TEMPLATE.format(restaurant_visits='\n'.join(restaurant_visits), dietary_preferences='\n'.join(dietary_preferences), restaurants='\n'.join(restaurant_nodes))


# Each search returns one page of results. items holds the results of that page.
restaurant_visit_edges = search_results_restaurant_visits.items or []
dietary_preference_edges = search_results_dietary_preferences.items or []
restaurant_nodes = search_results_restaurants.items or []

context_block = compose_context_block(restaurant_visit_edges, dietary_preference_edges, restaurant_nodes)
print(context_block)
```

```typescript
import type { Zep } from "@getzep/zep-cloud";

const CONTEXT_STRING_TEMPLATE_2 = `PREVIOUS_RESTAURANT_VISITS, DIETARY_PREFERENCES, and RESTAURANTS represent relevant context to the current conversation.
# These are the most relevant restaurants the user has previously visited
# format: restaurant_name: RESTAURANT_NAME
<PREVIOUS_RESTAURANT_VISITS>
{restaurant_visits}
</PREVIOUS_RESTAURANT_VISITS>

# These are the most relevant dietary preferences of the user, whether they represent an allergy, and their valid date ranges
# format: allergy: True/False; preference_type: PREFERENCE_TYPE (Date range: from - to)
<DIETARY_PREFERENCES>
{dietary_preferences}
</DIETARY_PREFERENCES>

# These are the most relevant restaurants the user has discussed previously
# format: name: RESTAURANT_NAME; cuisine_type: CUISINE_TYPE; dietary_accommodation: DIETARY_ACCOMMODATION
<RESTAURANTS>
{restaurants}
</RESTAURANTS>`;

function formatEdgeWithAttributes(edge: Zep.Edge, includeTimestamps = true): string {
    const attrs = Object.entries(edge.attributes ?? {})
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([k, v]) => `${k}: ${v}`)
        .join('; ');
    if (includeTimestamps) {
        const validAt = edge.validAt ?? "date unknown";
        const invalidAt = edge.invalidAt ?? "present";
        return `  - ${attrs} (Date range: ${validAt} - ${invalidAt})`;
    }
    return `  - ${attrs}`;
}

function formatNodeWithAttributes(node: Zep.Node): string {
    const attributes = Object.entries(node.attributes ?? {})
        .filter(([k]) => k !== "labels")
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([k, v]) => `${k}: ${v}`)
        .join('; ');
    return `  - name: ${node.name}; ${attributes}`;
}

function composeContextBlock2(
    restaurantVisitEdges: Zep.Edge[],
    dietaryPreferenceEdges: Zep.Edge[],
    restaurantNodes: Zep.Node[]
): string {
    const restaurantVisits = restaurantVisitEdges.map(e => formatEdgeWithAttributes(e, false)).join('\n');
    const dietaryPreferences = dietaryPreferenceEdges.map(e => formatEdgeWithAttributes(e, true)).join('\n');
    const restaurants = restaurantNodes.map(n => formatNodeWithAttributes(n)).join('\n');
    return CONTEXT_STRING_TEMPLATE_2
        .replace('{restaurant_visits}', restaurantVisits)
        .replace('{dietary_preferences}', dietaryPreferences)
        .replace('{restaurants}', restaurants);
}

// Each search returns one page of results. data holds the results of that page.
const restaurantVisitEdges: Zep.Edge[] = searchResultsRestaurantVisits.data;
const dietaryPreferenceEdges: Zep.Edge[] = searchResultsDietaryPreferences.data;
const restaurantNodes: Zep.Node[] = searchResultsRestaurants.data;

const contextBlock2 = composeContextBlock2(restaurantVisitEdges, dietaryPreferenceEdges, restaurantNodes);
console.log(contextBlock2);
```

```go
import (
	"strings"

	zep "github.com/getzep/zep-go/v4"
)

const CONTEXT_STRING_TEMPLATE_2 = `PREVIOUS_RESTAURANT_VISITS, DIETARY_PREFERENCES, and RESTAURANTS represent relevant context to the current conversation.
# These are the most relevant restaurants the user has previously visited
# format: restaurant_name: RESTAURANT_NAME
<PREVIOUS_RESTAURANT_VISITS>
{restaurant_visits}
</PREVIOUS_RESTAURANT_VISITS>

# These are the most relevant dietary preferences of the user, whether they represent an allergy, and their valid date ranges
# format: allergy: True/False; preference_type: PREFERENCE_TYPE (Date range: from - to)
<DIETARY_PREFERENCES>
{dietary_preferences}
</DIETARY_PREFERENCES>

# These are the most relevant restaurants the user has discussed previously
# format: name: RESTAURANT_NAME; cuisine_type: CUISINE_TYPE; dietary_accommodation: DIETARY_ACCOMMODATION
<RESTAURANTS>
{restaurants}
</RESTAURANTS>`

formatEdgeWithAttributes := func(edge *zep.Edge, includeTimestamps bool) string {
	attrs := make([]string, 0)
	for _, k := range []string{"allergy", "preference_type", "restaurant_name"} {
		if v, ok := edge.Attributes[k]; ok {
			attrs = append(attrs, fmt.Sprintf("%s: %v", k, v))
		}
	}
	attrsStr := strings.Join(attrs, "; ")
	if includeTimestamps {
		validAt := "date unknown"
		if edge.ValidAt != nil && *edge.ValidAt != "" {
			validAt = *edge.ValidAt
		}
		invalidAt := "present"
		if edge.InvalidAt != nil && *edge.InvalidAt != "" {
			invalidAt = *edge.InvalidAt
		}
		return fmt.Sprintf("  - %s (Date range: %s - %s)", attrsStr, validAt, invalidAt)
	}
	return fmt.Sprintf("  - %s", attrsStr)
}

formatNodeWithAttributes := func(node *zep.Node) string {
	attrs := make([]string, 0)
	for k, v := range node.Attributes {
		if k == "labels" {
			continue
		}
		attrs = append(attrs, fmt.Sprintf("%s: %v", k, v))
	}
	attrsStr := strings.Join(attrs, "; ")
	name := ""
	if node.Name != nil {
		name = *node.Name
	}
	return fmt.Sprintf("  - name: %s; %s", name, attrsStr)
}

composeContextBlock2 := func(restaurantVisitEdges []*zep.Edge, dietaryPreferenceEdges []*zep.Edge, restaurantNodes []*zep.Node) string {
	restaurantVisits := make([]string, 0)
	for _, edge := range restaurantVisitEdges {
		restaurantVisits = append(restaurantVisits, formatEdgeWithAttributes(edge, false))
	}
	dietaryPreferences := make([]string, 0)
	for _, edge := range dietaryPreferenceEdges {
		dietaryPreferences = append(dietaryPreferences, formatEdgeWithAttributes(edge, true))
	}
	restaurants := make([]string, 0)
	for _, node := range restaurantNodes {
		restaurants = append(restaurants, formatNodeWithAttributes(node))
	}
	result := strings.ReplaceAll(CONTEXT_STRING_TEMPLATE_2, "{restaurant_visits}", strings.Join(restaurantVisits, "\n"))
	result = strings.ReplaceAll(result, "{dietary_preferences}", strings.Join(dietaryPreferences, "\n"))
	result = strings.ReplaceAll(result, "{restaurants}", strings.Join(restaurants, "\n"))
	return result
}

// Each search returns one page of results. Results holds the results of that page.
restaurantVisitEdges := searchResultsRestaurantVisits.Results
dietaryPreferenceEdges := searchResultsDietaryPreferences.Results
restaurantNodes := searchResultsRestaurants.Results

contextBlock2 := composeContextBlock2(restaurantVisitEdges, dietaryPreferenceEdges, restaurantNodes)
fmt.Println(contextBlock2)
```

```text
PREVIOUS_RESTAURANT_VISITS, DIETARY_PREFERENCES, and RESTAURANTS represent relevant context to the current conversation.
# These are the most relevant restaurants the user has previously visited
# format: restaurant_name: RESTAURANT_NAME
<PREVIOUS_RESTAURANT_VISITS>
  - restaurant_name: Insomnia Cookies
  - restaurant_name: Green Leaf Cafe
</PREVIOUS_RESTAURANT_VISITS>

# These are the most relevant dietary preferences of the user, whether they represent an allergy, and their valid date ranges
# format: allergy: True/False; preference_type: PREFERENCE_TYPE (Date range: from - to)
<DIETARY_PREFERENCES>
  - allergy: False; preference_type: vegetarian (Date range: 2025-06-16T02:17:25Z - present)
  - allergy: False; preference_type: lactose intolerance (Date range: 2025-06-16T02:17:25Z - present)
</DIETARY_PREFERENCES>

# These are the most relevant restaurants the user has discussed previously
# format: name: RESTAURANT_NAME; cuisine_type: CUISINE_TYPE; dietary_accommodation: DIETARY_ACCOMMODATION
<RESTAURANTS>
  - name: Green Leaf Cafe; dietary_accommodation: vegetarian
  - name: Insomnia Cookies;
</RESTAURANTS>
```

# Example 3: Basic custom context block with BFS

## Search

You can use breadth-first search (BFS) to expand results around recent history. This example lists the [episodes](/episodes) of the current thread and uses the UUIDs of up to five user episodes as BFS origins.

The [BFS section](/searching-the-graph#breadth-first-search-bfs) explains the search behavior.

> **Tip**
>
> Run these searches in parallel with the [asynchronous Python client](/quick-start-guide#initialize-the-zep-client), TypeScript promises, or goroutines.

```python
query = "Find some food around here"

# Read the episodes of the current thread. BFS accepts up to five origin UUIDs.
episodes = client.thread.list_episodes(zep_thread2_uuid, limit=10).items or []
episode_uuids = [episode.uuid_ for episode in episodes if episode.role == "user"][:5]

search_results_nodes = client.graph.search_nodes(
    zep_graph_uuid,
    query=query,
    reranker="cross_encoder",
    limit=10,
    bfs_origin_node_uuids=episode_uuids,
)
search_results_edges = client.graph.search_edges(
    zep_graph_uuid,
    query=query,
    reranker="cross_encoder",
    limit=10,
    bfs_origin_node_uuids=episode_uuids,
)
```

```typescript
const query = "Find some food around here";

// Read the episodes of the current thread. BFS accepts up to five origin UUIDs.
const episodePage = await client.thread.listEpisodes(zepThread2Uuid, { limit: 10 });
const episodeUuids = episodePage.data
    .filter((episode) => episode.role === "user" && episode.uuid)
    .map((episode) => episode.uuid!)
    .slice(0, 5);

const searchResultsNodes = await client.graph.searchNodes(zepGraphUuid, {
    limit: 10,
    body: {
        query,
        reranker: "cross_encoder",
        bfsOriginNodeUuids: episodeUuids,
    },
});

const searchResultsEdges = await client.graph.searchEdges(zepGraphUuid, {
    limit: 10,
    body: {
        query,
        reranker: "cross_encoder",
        bfsOriginNodeUuids: episodeUuids,
    },
});
```

```go
import (
	zep "github.com/getzep/zep-go/v4"
)

query := "Find some food around here"

// Read the episodes of the current thread. BFS accepts up to five origin UUIDs.
episodePage, err := client.Thread.ListEpisodes(ctx, zepThread2UUID, &zep.ThreadListEpisodesRequest{
	Limit: zep.Int(10),
})
if err != nil {
	fmt.Printf("Error listing episodes: %v\n", err)
	return
}

var episodeUUIDs []string
for _, episode := range episodePage.Results {
	if len(episodeUUIDs) == 5 {
		break
	}
	if episode.Role != nil && *episode.Role == zep.RoleTypeUser && episode.UUID != nil {
		episodeUUIDs = append(episodeUUIDs, *episode.UUID)
	}
}

searchResultsNodes, err := client.Graph.SearchNodes(ctx, zepGraphUUID, &zep.GraphSearchNodesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:              query,
		Reranker:           zep.SearchRequestRerankerCrossEncoder.Ptr(),
		BfsOriginNodeUUIDs: episodeUUIDs,
	},
})
if err != nil {
	fmt.Printf("Error searching nodes: %v\n", err)
	return
}

searchResultsEdges, err := client.Graph.SearchEdges(ctx, zepGraphUUID, &zep.GraphSearchEdgesRequest{
	Limit: zep.Int(10),
	Body: &zep.SearchRequest{
		Query:              query,
		Reranker:           zep.SearchRequestRerankerCrossEncoder.Ptr(),
		BfsOriginNodeUUIDs: episodeUUIDs,
	},
})
if err != nil {
	fmt.Printf("Error searching edges: %v\n", err)
	return
}
```

## Build the context block

Using the search results and a few helper functions, we can build the context block. Note that for nodes, we typically want to unpack the node name and node summary, and for edges we typically want to unpack the fact and the temporal validity information:

```python
from zep_cloud.types import Edge, Node

CONTEXT_STRING_TEMPLATE = """
FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>
"""


def format_fact(edge: Edge) -> str:
    valid_at = edge.valid_at if edge.valid_at is not None else "date unknown"
    invalid_at = edge.invalid_at if edge.invalid_at is not None else "present"
    formatted_fact = f"  - {edge.fact} (Date range: {valid_at} - {invalid_at})"
    return formatted_fact

def format_entity(node: Node) -> str:
    formatted_entity = f"  - {node.name}: {node.summary}"
    return formatted_entity

def compose_context_block(edges: list[Edge], nodes: list[Node]) -> str:
    facts = [format_fact(edge) for edge in edges]
    entities = [format_entity(node) for node in nodes]
    return CONTEXT_STRING_TEMPLATE.format(facts='\n'.join(facts), entities='\n'.join(entities))

# Each search returns one page of results. items holds the results of that page.
edges = search_results_edges.items or []
nodes = search_results_nodes.items or []

context_block = compose_context_block(edges, nodes)
print(context_block)
```

```typescript
import type { Zep } from "@getzep/zep-cloud";

const CONTEXT_STRING_TEMPLATE_1 = `FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>
# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>`;

function formatFact(edge: Zep.Edge): string {
    const validAt = edge.validAt ?? "date unknown";
    const invalidAt = edge.invalidAt ?? "present";
    return `  - ${edge.fact} (Date range: ${validAt} - ${invalidAt})`;
}

function formatEntity(node: Zep.Node): string {
    return `  - ${node.name}: ${node.summary}`;
}

function composeContextBlock1(edges: Zep.Edge[], nodes: Zep.Node[]): string {
    const facts = edges.map(formatFact).join('\n');
    const entities = nodes.map(formatEntity).join('\n');
    return CONTEXT_STRING_TEMPLATE_1
        .replace('{facts}', facts)
        .replace('{entities}', entities);
}

// Each search returns one page of results. data holds the results of that page.
const edges: Zep.Edge[] = searchResultsEdges.data;
const nodes: Zep.Node[] = searchResultsNodes.data;

const contextBlock1 = composeContextBlock1(edges, nodes);
console.log(contextBlock1);
```

```go
import (
	"strings"

	zep "github.com/getzep/zep-go/v4"
)

valueOrEmpty := func(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

const CONTEXT_STRING_TEMPLATE_1 = `FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
{facts}
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
{entities}
</ENTITIES>
`

formatFact := func(edge *zep.Edge) string {
	validAt := "date unknown"
	if edge.ValidAt != nil && *edge.ValidAt != "" {
		validAt = *edge.ValidAt
	}
	invalidAt := "present"
	if edge.InvalidAt != nil && *edge.InvalidAt != "" {
		invalidAt = *edge.InvalidAt
	}
	return fmt.Sprintf("  - %s (Date range: %s - %s)", valueOrEmpty(edge.Fact), validAt, invalidAt)
}

formatEntity := func(node *zep.Node) string {
	return fmt.Sprintf("  - %s: %s", valueOrEmpty(node.Name), valueOrEmpty(node.Summary))
}

composeContextBlock1 := func(edges []*zep.Edge, nodes []*zep.Node) string {
	var facts []string
	for _, edge := range edges {
		facts = append(facts, formatFact(edge))
	}
	var entities []string
	for _, node := range nodes {
		entities = append(entities, formatEntity(node))
	}
	result := strings.ReplaceAll(CONTEXT_STRING_TEMPLATE_1, "{facts}", strings.Join(facts, "\n"))
	result = strings.ReplaceAll(result, "{entities}", strings.Join(entities, "\n"))
	return result
}

// Each search returns one page of results. Results holds the results of that page.
edges := searchResultsEdges.Results
nodes := searchResultsNodes.Results

contextBlock1 := composeContextBlock1(edges, nodes)
fmt.Println(contextBlock1)
```

```text
FACTS and ENTITIES represent relevant context to the current conversation.
# These are the most relevant facts and their valid date ranges
# format: FACT (Date range: from - to)
# NOTE: Facts ending in "present" are currently valid (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - present)" means Jane currently prefers coffee with milk)
#       Facts with a past end date used to be valid but are NOT CURRENTLY VALID (e.g., "Jane prefers her coffee with milk (2024-01-15 10:30:00 - 2024-06-20 14:00:00)" means Jane no longer prefers coffee with milk)
<FACTS>
  - User wants to go to dessert (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe wants to go to a lunch place (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe said 'Perfect, let's go to Insomnia Cookies' indicating he will visit Insomnia Cookies. (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe said 'Let's go to Green Leaf Cafe' indicating intention to visit (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe is craving a chocolate chip cookie (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe states that he is vegetarian. (Date range: 2025-06-16T02:17:25Z - present)
  - John Doe is lactose intolerant (Date range: 2025-06-16T02:17:25Z - present)
</FACTS>

# These are the most relevant entities
# ENTITY_NAME: entity summary
<ENTITIES>
  - lunch place: The entity is a lunch place, but no specific details about its cuisine or dietary accommodations are provided.
  - dessert: The entity 'dessert' refers to a preference related to sweet courses typically served at the end of a meal. The context indicates that the user has expressed an interest in going to a dessert place, but no specific dessert or place has been named. The entity is categorized as a Preference and Entity, but no additional attributes are provided or inferred from the messages.
  - Green Leaf Cafe: Green Leaf Cafe is a restaurant that offers vegetarian options, making it suitable for vegetarian diners.
  - user: The user is John Doe, with the email john.doe@example.com. He has shown interest in visiting Green Leaf Cafe, which offers vegetarian options, and has also expressed a preference for lactose-free options, craving a chocolate chip cookie. The user has decided to go to Insomnia Cookies.
  - vegetarian: The user is interested in lunch places such as Panera Bread, Chipotle, and Green Leaf Cafe. They are specifically looking for vegetarian options at these restaurants.
  - chocolate chip cookie: The entity is a chocolate chip cookie, which the user desires as a snack. The user is lactose intolerant and cannot have ice cream, but is craving a chocolate chip cookie.
  - Insomnia Cookies: Insomnia Cookies is a restaurant that offers cookies, including chocolate chip cookies. The user is interested in a dessert and has chosen to go to Insomnia Cookies. No specific cuisine type or dietary accommodations are mentioned in the messages.
  - lactose intolerant: The entity is a preference indicating lactose intolerance, which is a dietary restriction that prevents the individual from consuming lactose, a sugar found in milk and dairy products. The person is specifically craving a chocolate chip cookie but cannot have ice cream due to lactose intolerance.
  - John Doe: The user is John Doe, with user ID user-34c7a6c1-ded6-4797-9620-8b80a5e7820f, email john.doe@example.com, and role user. He inquired about nearby lunch options and vegetarian choices, and expressed a preference for a chocolate chip cookie due to lactose intolerance.
</ENTITIES>
```

# Example 4: Using user summary in context block

## Get user node

Retrieve the user node when you need its summary in a custom Context Block. [User summary instructions](/user-summary-instructions) control the generated summary.

> **Note**
>
> **About the user node**
>
> Each user has a single unique user node in their graph representing the user themselves. The user summary generated from user summary instructions lives on this user node. When you call `client.user.get_node()` with the user UUID, you are retrieving this special node that contains the user's summary.

**`Python`**

```python Python
from zep_cloud.client import Zep

client = Zep(api_key=API_KEY)

# Get the user node and extract the summary
user_node = client.user.get_node(zep_user_uuid)
user_summary = user_node.summary
```

**`TypeScript`**

```typescript TypeScript
import { ZepClient } from "@getzep/zep-cloud";

const client = new ZepClient({
  apiKey: API_KEY,
});

// Get the user node and extract the summary
const userNode = await client.user.getNode(zepUserUuid);
const userSummary = userNode.summary;
```

**`Go`**

```go Go
import (
	"context"
	"log"

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

client := zepclient.NewClient(option.WithAPIKey(apiKey))

// Get the user node and extract the summary
userNode, err := client.User.GetNode(context.TODO(), zepUserUUID)
if err != nil {
	log.Fatalf("Failed to get user node: %v", err)
}

var userSummary string
if userNode.Summary != nil {
	userSummary = *userNode.Summary
}
```

## Build the context block

Using the user summary, you can create a simple context block that provides personalized user information:

**`Python`**

```python Python
# Build a simple context block with user summary
context_block = f"""USER_SUMMARY represents relevant context about the user.
# This is a high-level summary of the user
<USER_SUMMARY>
{user_summary if user_summary else "No user summary available"}
</USER_SUMMARY>
"""

print(context_block)
```

**`TypeScript`**

```typescript TypeScript
// Build a simple context block with user summary
const contextBlock = `USER_SUMMARY represents relevant context about the user.
# This is a high-level summary of the user
<USER_SUMMARY>
${userSummary || "No user summary available"}
</USER_SUMMARY>
`;

console.log(contextBlock);
```

**`Go`**

```go Go
import "fmt"

// Build a simple context block with user summary
summaryText := userSummary
if summaryText == "" {
	summaryText = "No user summary available"
}

contextBlock := fmt.Sprintf(`USER_SUMMARY represents relevant context about the user.
# This is a high-level summary of the user
<USER_SUMMARY>
%s
</USER_SUMMARY>
`, summaryText)

fmt.Println(contextBlock)
```

```text
USER_SUMMARY represents relevant context about the user.
# This is a high-level summary of the user
<USER_SUMMARY>
John Doe is a software engineer who enjoys hiking and photography. He is vegetarian and lactose intolerant. He prefers detailed technical discussions and values efficiency in communication. He has requested that the AI provide concise answers with code examples when discussing programming topics.
</USER_SUMMARY>
```

## Use the custom context block

The Context Block can contain text that came from end users, documents, tools, or other external sources. A privileged message gives that text higher instruction priority than ordinary input. Keep the Context Block out of system messages, developer messages, and other privileged instruction channels.

Follow your model provider's documented method for separating instructions from data:

* For the OpenAI Responses API, send preloaded context through ordinary `input` or a `user` message. Use `function_call_output` only for the result of an actual function call.
* For the Anthropic Messages API, design retrieval as a tool call when context can contain third-party data. Return the context in a `tool_result` block linked to the original `tool_use_id`.
* For other providers, use the documented untrusted-data channel. If the provider does not define one, use an ordinary user-level message with explicit data framing.

### OpenAI with preloaded context

| Message type                  | Content                                        |
| ----------------------------- | ---------------------------------------------- |
| `Developer` or `instructions` | Stable application policy. No Zep context.     |
| `Assistant`                   | An assistant message stored in Zep             |
| `User`                        | A user message stored in Zep                   |
| ...                           | ...                                            |
| `User` or ordinary `input`    | `{Zep Context Block}` framed as reference data |
| `User`                        | The latest user request                        |

Place the Context Block after the conversation history and before the latest user request. Everything before the block stays unchanged between turns, so this order preserves the cacheable prefix that [prompt caching](https://platform.openai.com/docs/guides/prompt-caching) needs. Replace the previous turn's block instead of appending a second one.

If the model requests memory through a function, return the Context Block as `function_call_output` linked to the original `call_id`.

### OpenAI Chat Completions with tool-retrieved context

| Message type | Content                                         |
| ------------ | ----------------------------------------------- |
| `Developer`  | Stable application policy. No Zep context.      |
| `User`       | The latest user request                         |
| `Assistant`  | A tool call requesting Zep retrieval            |
| `Tool`       | `{Zep Context Block}`, linked by `tool_call_id` |
| `Assistant`  | The response to the user                        |

### Anthropic with tool-retrieved context

| Message type | Content                                                                   |
| ------------ | ------------------------------------------------------------------------- |
| `System`     | Stable application policy. No Zep context.                                |
| `User`       | The latest user request                                                   |
| `Assistant`  | A `tool_use` block requesting Zep retrieval                               |
| `User`       | A `tool_result` block with `{Zep Context Block}`, linked by `tool_use_id` |
| `Assistant`  | The response to the user                                                  |

Do not create a tool message for preloaded context unless the provider documents that pattern. A tool-result type must remain linked to the model's actual tool request.

Read [Memory security best practices](/memory-security) for provider-specific mappings, write controls, action authorization, and recovery guidance.