Skip to navigation

Content policies

Define the categories of derived information that must not enter a Context Graph, and read what Zep dropped.

Available to Enterprise Plan customers only.

Overview

A content policy tells Zep what derived information must not enter a Context Graph. You define the categories and the rules. Zep evaluates every artifact that ingestion derives from an episode against the rules of the graph before the artifact is published. An artifact that matches a rule is dropped. The source episode stays stored.

Use a content policy when your application must keep a class of information out of agent memory, for example health conditions in a banking assistant or compensation in a support agent. The policy applies to the graph content that Zep derives. The policy does not filter, redact, or rewrite the text you send.

A content policy separates three surfaces:

  • The project policy is a set of categories and rules that you write at project scope. Each write creates a new immutable revision.
  • The bound policy is the policy of one graph. Zep copies the current project revision when the graph is created. The bound policy never changes after creation.
  • The episode state is the content_policy object on each episode of a protected graph. It records the evaluation status and what Zep dropped.

What Zep evaluates

Zep evaluates the artifacts that ingestion derives from an episode: entities, entity attributes, entity summaries, facts, thread summaries, document summaries, and observations. Zep does not evaluate the source episode text.

A classifier that Zep operates answers one question per artifact and rule: does the artifact state or imply the prohibited meaning about a named or identifiable person? Zep converts each answer into one of three decisions.

DecisionEffect on the artifactEffect on the episode
MatchZep drops the artifact.Zep flags the episode and records the category.
UncertainZep drops the artifact.The episode is not flagged.
No matchThe artifact is published when every other rule also returns no match.None

Zep never rewrites, redacts, or replaces a dropped artifact. A drop is complete:

  • A match on a fact drops the fact. The entities at each end stay under evaluation on their own.
  • A match on an entity drops the entity and every fact of the same episode that connects to that entity.
  • A match on one entity attribute drops that attribute only.
  • A match on an entity summary drops the summary. The entity is published without a summary update.
  • An entity that the episode creates is dropped when a match drops its summary and no published edge connects to it. An entity that existed before the episode stays.
  • An invalidation, expiration, or contradiction that depends on a dropped fact is not applied. Existing facts keep their state.

Permitted artifacts of the same episode are published. One episode can publish permitted facts and drop prohibited ones.

An artifact over 4000 characters is not truncated. Zep drops the artifact without a flag. A summary over 4000 characters is evaluated in segments, and a match on any segment drops the whole summary.

Source episodes stay stored as written, under the existing retention and deletion behavior and BYOK controls. A content policy is not a promise that prohibited text never reaches Zep. It prevents the derived graph content. An application that must filter source text does so before it sends data to Zep.

The policy model

A policy set has categories and rules.

  • A category has a key and an optional description. The key is a customer-defined code that matches ^[a-z][a-z0-9_]{1,63}$ and is unique in the policy set. You filter episodes and audit events by key. The description has at most 500 characters. Zep does not send the description to the classifier.
  • A rule belongs to one category through category_key. The rule has a description of at most 1000 characters that states the prohibited meaning and the subject scope. It has 1 to 8 prohibited_examples and 0 to 8 permitted_examples. Each example has at most 500 characters. The description and the examples are classifier inputs.

A policy set has at most 16 categories and at most 32 rules. A category without a rule is valid. A rule whose category_key names no category in the same body is rejected. An empty policy set has zero categories and zero rules and means “no content policy”.

Zep rejects a write with 400 invalid_request when a key repeats, a key does not match the pattern, a category_key names no category, a list or a string exceeds its limit, or prohibited_examples is empty. The param field of the error names the failing path, for example rules[2].prohibited_examples.

Write rules as you would brief a careful reviewer. Name the subject scope, such as “a named or identifiable person”. Write prohibited examples that read as extracted facts, and permitted examples that are close to the rule but do not state the prohibited meaning.

The project policy

The project policy is the default for every graph that you create later. The ContentPolicy response carries the server-owned fields uuid, revision, categories[].id, rules[].id, and created_at. A write body omits them. A write that echoes them is accepted, and Zep ignores the echoed values.

The content policy methods are in the v4 SDKs after this release. The current v3 SDKs do not include them. The examples below use the v4 method names.

Set the project policy

PUT /project/content-policy replaces the full policy set and returns the new revision with 200. There is no partial update. A body that equals the current revision still creates a new revision.

from zep_cloud import Zep
client = Zep(api_key="YOUR_API_KEY")
policy = client.project.set_content_policy(
categories=[
{
"key": "health_condition",
"description": "Physical or mental health conditions of a person",
}
],
rules=[
{
"category_key": "health_condition",
"description": "Statements that a named or identifiable person has, had, or was treated for a health condition.",
"prohibited_examples": [
"Jordan told the agent about a chronic back condition.",
"The customer takes medication for anxiety.",
],
"permitted_examples": [
"The customer asked which pharmacies are open late.",
"The user prefers text messages to phone calls.",
],
}
],
)
print(policy.revision)

The response is the new ContentPolicy:

{
"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"revision": 3,
"categories": [
{
"id": "cat_01j9z4x2",
"key": "health_condition",
"description": "Physical or mental health conditions of a person"
}
],
"rules": [
{
"id": "rule_01j9z4x9",
"category_key": "health_condition",
"description": "Statements that a named or identifiable person has, had, or was treated for a health condition.",
"prohibited_examples": [
"Jordan told the agent about a chronic back condition.",
"The customer takes medication for anxiety."
],
"permitted_examples": [
"The customer asked which pharmacies are open late.",
"The user prefers text messages to phone calls."
]
}
],
"created_at": "2026-09-22T10:00:00Z"
}

categories[].id and rules[].id are stable across revisions. Zep matches a category across revisions by key, and a rule by the pair (category_key, description). A category or rule that does not change keeps its identifier.

To clear the project policy, write the empty policy set. No body, {}, and {"categories": [], "rules": []} are the same write. Graphs that you create later bind the empty set unless the create body adds rules. Graphs that already exist keep their bound policy.

Read the project policy and its revisions

GET /project/content-policy returns the current revision. A project without a write returns revision 0, the empty policy set.

GET /project/content-policy/revisions lists revisions in descending revision order with limit and cursor. The list carries total_size. Zep stores every revision that a graph binds until the last graph that binds it is deleted. A revision that no graph binds and that is not current can be removed by retention, so a client accepts a gap in the list.

GET /project/content-policy/revisions/{revision_uuid} returns one revision, or 404 not_found.

current = client.project.get_content_policy()
revisions = client.project.list_content_policy_revisions(limit=20)
for revision in revisions.items:
print(revision.revision, revision.uuid, revision.created_at)
one = client.project.get_content_policy_revision(revision_uuid=current.uuid)

Bind a policy at graph creation

A graph copies the current project revision when you create it. graph.create and user.create accept an optional content_policy object with three fields:

  • project_revision is optional. When present, the create fails with 409 conflict and the code content_policy_revision_stale if the current project revision is different. Send it when the policy that an operator reviewed must be the policy that the graph binds.
  • categories and rules add to the project revision. A key that repeats a project category adds rules to that category. A rule that duplicates a project rule by (category_key, description) is ignored.
  • The combined set must stay inside the limits of the policy model. A combined set over a limit is 400 invalid_request with param set to content_policy.rules.

The create body cannot remove a project rule. Additions are additive only.

graph = client.graph.create(
graph_id="support-emea",
content_policy={
"project_revision": 3,
"categories": [{"key": "union_membership"}],
"rules": [
{
"category_key": "union_membership",
"description": "Statements about a person's trade union membership.",
"prohibited_examples": ["The user is a shop steward."],
}
],
},
)

The create and the binding are one atomic operation. The 201 response carries the graph, and the bound policy is readable from that moment. A create that cannot persist the binding writes no graph and returns 503 service_unavailable.

A graph is never created unprotected when a policy exists. A create on an account without the content policy entitlement, when the project revision or the create body has at least one rule, returns 403 content_policy_not_entitled and writes no graph.

The bound policy is read-only

There is no update and no delete for the bound policy of a graph. A later write to the project policy does not change a graph that already exists. When a graph needs a different policy, create a new graph.

GET /graphs/{graph_uuid}/content-policy returns the bound set as a ContentPolicy with two more fields:

{
"uuid": "…",
"revision": 3,
"categories": [ … ],
"rules": [ … ],
"created_at": "…",
"project_revision": 3,
"bound_at": "2026-09-22T10:05:00Z"
}

revision and project_revision are the number of the project revision that the bound set includes, and they are always equal. The bound set has its own uuid when the graph adds a category or a rule, and uuid equals the uuid of that project revision when the graph adds nothing. Project categories and rules keep the project identifiers. Graph additions get their own identifiers.

bound = client.graph.get_content_policy(graph_uuid="graph_uuid")
print(bound.revision, bound.bound_at)

Cloning a graph binds the source graph’s policy to the clone. The clone operation does not accept content_policy. The clone copies each source episode with its content_policy object unchanged. It does not copy pending or failed episodes.

Episode state

Every episode of a protected graph carries a content_policy object. An episode of a graph without a bound rule has no content_policy object.

{
"uuid": "…",
"graph_uuid": "…",
"processed": true,
"content_policy": {
"status": "complete",
"revision": 3,
"violated": true,
"category_keys": ["health_condition"],
"retained_count": 4,
"dropped_count": 2
}
}
FieldMeaning
statuspending, complete, or failed. failed means that the ingestion retry policy is exhausted.
revisionThe bound revision of the graph.
violatedtrue when at least one artifact matched a rule. It never returns to false under the same bound revision.
category_keysThe category keys of every match. It is empty when violated is false. It only lists keys of the bound set.
retained_countThe count of published artifacts. It is 0 while status is pending or failed, because no artifact is published before every evaluation of the episode completes.
dropped_countThe count of dropped artifacts. It grows as evaluation persists decisions, in every status.

processed keeps its existing meaning. An episode is processed only after content_policy.status reaches complete. A pending or failed evaluation leaves processed: false.

violated, category_keys, and dropped_count can grow after complete. Zep sometimes extracts one artifact from several episodes at once. When that artifact matches a rule, Zep attributes the match to every episode in the group, including an episode that is already complete. status and processed do not change in that case.

Ingestion evaluates every artifact of an episode before it publishes any of them. Retrieval never observes a protected graph state where an artifact is published before its evaluation completed.

Pending and failed episodes

An episode stays pending while ingestion evaluates it. When the classifier fails, Zep retries under the existing ingestion retry policy, and the episode stays pending. When the retry policy is exhausted, status becomes failed and processed stays false. A match that was persisted before the failure stays in violated and category_keys.

No artifact of a failed episode is published. A failure is never recorded as “no match”. A pending or failed episode is excluded from every summary and observation, and from context assembly.

Task results

The graph.add, thread.add_messages, and batch.process tasks report the policy outcome in result. A policy violation is an expected outcome, and the task reaches succeeded.

{
"uuid": "…",
"type": "graph.add",
"status": "succeeded",
"result": {
"episode_uuid": "…",
"content_policy": {
"status": "complete",
"violated": true,
"category_keys": ["health_condition"],
"retained_count": 4,
"dropped_count": 2
}
}
}
  • graph.add carries one episode_uuid and one content_policy object.
  • thread.add_messages carries episodes, an array of {episode_uuid, content_policy} with one entry per message.
  • batch.process carries the aggregate violated_count and failed_count in result.content_policy. Each batch item carries the episode content_policy object once the item completes.

The task stays processing while the episode is pending, including while Zep retries a classifier failure. A classifier failure that exhausts the retry policy is a processing failure. The task reaches failed with error.code set to content_policy_evaluation_failed. The error message names no source text, no artifact text, and no rule text. A thread.add_messages or batch.process task where some episodes completed and some failed is partial, and result carries the completed episodes.

The ingestion status recipe covers task and episode polling.

Flagged episodes in retrieval

A flagged episode is an episode where at least one artifact matched a rule. The project setting include_policy_violating_episodes on GET /project and PATCH /project controls whether four operations return flagged episodes. The default is false.

OperationSetting falseSetting true
graph.episode.listOmits flagged episodesIncludes flagged episodes
graph.search_episodesOmits flagged episodesIncludes flagged episodes
graph.episode.list_for_documentOmits flagged episodesIncludes flagged episodes
thread.list_episodesOmits flagged episodesIncludes flagged episodes

In both settings:

  • The four operations return pending and failed episodes with processed: false. Such an episode has no published artifact, so the list exposes only the source episode.
  • graph.episode.get returns a flagged episode. A direct read is an explicit request for one known identifier.
  • graph.search_episodes matches on source text.

The setting never changes the result of graph.get_context, thread.get_context, agent.get_context, summaries, or observations. Context assembly excludes flagged, pending, and failed episodes from source text and from evidence expansion. A permitted fact from a mixed episode stays searchable, and default responses do not expand it to the source episode text. A flagged episode is never an input to a thread summary, a document summary, a user summary, an entity summary, or an observation. When an episode is flagged after a summary that used it was published, Zep marks that summary unavailable and rebuilds it from eligible inputs.

Zep does not derive observations on a protected graph in this release. The observation writer does not evaluate its output against the bound policy, so it does not run.

client.project.update(include_policy_violating_episodes=True)

Filter by violation

graph.episode.list and graph.search_episodes accept content_policy_violated as an exact-match filter in filters. The filter is supported only while include_policy_violating_episodes is true. While the setting is false, the filter is rejected with 400 unsupported_filter.

Cursors

The value of include_policy_violating_episodes is part of the pagination cursor of the four operations. A cursor that was issued under one value and used under the other value returns 400 invalid_cursor. Restart the page walk after you change the setting.

Audit events

POST /graphs/{graph_uuid}/content-policy/events/list lists one event per dropped artifact and one per flagged episode. Events carry identifiers only. An event never contains source text, artifact text, spans, probabilities, questions, person names, or entity names.

{
"items": [
{
"id": "cpe_01j9z5a0",
"episode_uuid": "…",
"revision": 3,
"artifact_kind": "edge",
"category_keys": ["health_condition"],
"rule_ids": ["rule_01j9z4x9"],
"drop_reason": "match",
"decided_at": "2026-09-22T10:05:00Z"
}
],
"next_cursor": "…",
"total_size": 12
}
FieldMeaning
idThe event identifier.
episode_uuidThe episode that the artifact was derived from.
revisionThe bound revision under which Zep decided.
artifact_kindentity, entity_attribute, entity_summary, edge, thread_summary, document_summary, observation, or episode.
category_keysThe category keys of the matched rules.
rule_idsThe rule identifiers. Project rules and graph rules have different identifiers, so you can separate them.
drop_reasonmatch, indeterminate, oversize, or dependent. indeterminate is an uncertain decision. oversize is an artifact over 4000 characters. dependent is an artifact that was dropped because it connected to a dropped artifact.
decided_atThe decision time.

An episode-level event has artifact_kind: "episode", drop_reason: "match", and every matched key.

The list orders by decided_at descending and paginates with limit and cursor. It carries total_size. filters accepts episode_uuid and drop_reason as exact-match filters. Any other filter returns 400 unsupported_filter. The operation is available on an unprotected graph and returns an empty list. Events follow the retention of the audit store, so a client accepts that old events are absent.

events = client.graph.list_content_policy_events(
graph_uuid="graph_uuid",
filters={"drop_reason": "match"},
limit=50,
)
for event in events.items:
print(event.episode_uuid, event.artifact_kind, event.category_keys)

Permissions

Policy reads map to the ABAC action project.get at project scope and graph.get at graph scope. PUT /project/content-policy and the include_policy_violating_episodes field of PATCH /project map to project.update. content_policy in a create body needs graph.create or user.add, the same as the create itself. An ingestion-only API key cannot write a policy or change the retrieval setting, and receives 403 permission_denied. Policy-based access control describes how actions attach to API keys.

Entitlement and availability

Content policies need the content policy entitlement on your account. Without the entitlement:

  • A PUT /project/content-policy with at least one rule returns 403 content_policy_not_entitled.
  • A create whose bound policy, the current project revision plus the content_policy in the body, has at least one rule returns 403 content_policy_not_entitled and writes no graph. A body that adds categories only does not need the entitlement.
  • A PUT of the empty policy set is accepted. A project can always clear its policy and create graphs again.

A project that loses the entitlement keeps existing bound graphs protected. A new graph on that account is rejected until the project policy is cleared or the entitlement returns. Removing protection from an existing graph is never a side effect of a plan change.

The classifier call sends the derived text and the rule text to a language model that Zep operates through its model gateway. Zep selects the model, and you cannot change it. In this release, an account under a strict data residency policy and a BYOC deployment cannot receive the entitlement. The Security & Compliance overview describes the deployment models. Contact Zep sales for the entitlement.

Error codes

CodeStatusMeaning
invalid_request400A body limit or shape violation. param names the failing path.
unsupported_filter400The content_policy_violated filter while include_policy_violating_episodes is false, or an unsupported events filter.
invalid_cursor400A cursor issued under the other value of include_policy_violating_episodes.
permission_denied403The key lacks project.update, graph.create, or user.add.
content_policy_not_entitled403The account has no content policy entitlement.
not_found404An unknown revision or graph.
content_policy_revision_stale409project_revision in a create body is not the current project revision.
service_unavailable503The binding could not persist. No graph was written.
content_policy_evaluation_failedtask errorThe classifier failed after the ingestion retry policy was exhausted. The episode is failed.