diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py
index 126ce503f81..367dd2e6d42 100644
--- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py
+++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py
@@ -836,7 +836,12 @@ def _knowledge_metadata_filtering_config(
return DifyKnowledgeMetadataFilteringConfig(
mode=metadata_filtering.mode,
model_config=_knowledge_model_config(metadata_filtering.metadata_model_config),
- conditions=cast(Any, metadata_filtering.conditions.model_dump(mode="json"))
+ conditions=cast(
+ Any,
+ metadata_filtering.conditions.model_dump(
+ mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}}
+ ),
+ )
if metadata_filtering.conditions is not None
else None,
)
diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py
index a6a0cd544a2..b919eaed602 100644
--- a/api/models/agent_config_entities.py
+++ b/api/models/agent_config_entities.py
@@ -420,8 +420,19 @@ class AgentKnowledgeRetrievalConfig(BaseModel):
class AgentKnowledgeMetadataCondition(BaseModel):
+ """One manual metadata filter clause.
+
+ ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
+ every save (a stable row key and a reference to the selected metadata
+ field). They are persisted here for round-tripping the composer's draft
+ state but are stripped before building the Agent runtime request, whose
+ DTO only accepts ``name``/``comparison_operator``/``value``.
+ """
+
model_config = ConfigDict(extra="forbid")
+ id: str | None = None
+ metadata_id: str | None = None
name: str = Field(min_length=1, max_length=255)
comparison_operator: SupportedComparisonOperator
value: ConditionValue = None
diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md
index ef1a80c5ce2..2745eacd15f 100644
--- a/api/openapi/markdown/console-openapi.md
+++ b/api/openapi/markdown/console-openapi.md
@@ -14302,9 +14302,19 @@ the current roster/workflow APIs scoped to Dify Agent.
#### AgentKnowledgeMetadataCondition
+One manual metadata filter clause.
+
+``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
+every save (a stable row key and a reference to the selected metadata
+field). They are persisted here for round-tripping the composer's draft
+state but are stripped before building the Agent runtime request, whose
+DTO only accepts ``name``/``comparison_operator``/``value``.
+
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| comparison_operator | string,
**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes |
+| id | string | | No |
+| metadata_id | string | | No |
| name | string | | Yes |
| value | string
[ string ]
number | | No |
diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py
index 36a4e9a5ee9..c969b296efb 100644
--- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py
+++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py
@@ -744,7 +744,13 @@ def test_build_maps_agent_soul_knowledge_to_knowledge_layer_config():
"conditions": {
"logical_operator": "and",
"conditions": [
- {"name": "category", "comparison_operator": "contains", "value": "auth"}
+ {
+ "id": "cond-1",
+ "metadata_id": "meta-1",
+ "name": "category",
+ "comparison_operator": "contains",
+ "value": "auth",
+ }
],
},
},
diff --git a/api/tests/unit_tests/services/agent/test_agent_composer_entities.py b/api/tests/unit_tests/services/agent/test_agent_composer_entities.py
index 4aaae11b7dc..efc29ffb602 100644
--- a/api/tests/unit_tests/services/agent/test_agent_composer_entities.py
+++ b/api/tests/unit_tests/services/agent/test_agent_composer_entities.py
@@ -365,6 +365,51 @@ def test_knowledge_runtime_requirements_block_publish_but_not_draft_save(knowled
ComposerConfigValidator.validate_publish_payload(publish_payload)
+def test_manual_metadata_filtering_condition_accepts_composer_ui_identifiers():
+ """The composer's condition editor always sends ``id`` (list row key) and
+ ``metadata_id`` (selected metadata field reference) alongside every
+ condition. Rejecting them as unknown fields broke every save of a manual
+ metadata filter (see GH issue #40169)."""
+ payload = ComposerSavePayload.model_validate(
+ {
+ "variant": ComposerVariant.AGENT_APP,
+ "save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
+ "agent_soul": {
+ "knowledge": {
+ "sets": [
+ {
+ "id": "support",
+ "name": "Support KB",
+ "datasets": [{"id": "dataset-1"}],
+ "query": {"mode": "generated_query"},
+ "retrieval": {"mode": "multiple", "top_k": 4},
+ "metadata_filtering": {
+ "mode": "manual",
+ "conditions": {
+ "logical_operator": "and",
+ "conditions": [
+ {
+ "id": "b149eceb-191a-40a2-9f11-61cf21ebd147",
+ "metadata_id": "ad6cf326-eadf-46e8-a2d5-9cb892d2cc84",
+ "name": "category",
+ "comparison_operator": "is",
+ "value": "auth",
+ }
+ ],
+ },
+ },
+ },
+ ]
+ }
+ },
+ }
+ )
+
+ condition = payload.agent_soul.knowledge.sets[0].metadata_filtering.conditions.conditions[0]
+ assert condition.id == "b149eceb-191a-40a2-9f11-61cf21ebd147"
+ assert condition.metadata_id == "ad6cf326-eadf-46e8-a2d5-9cb892d2cc84"
+
+
def test_agent_soul_model_config_is_first_class_without_credentials():
config = AgentSoulConfig(
model=AgentSoulModelConfig(
diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts
index cfabaa87f32..fa33c2698b3 100644
--- a/packages/contracts/generated/api/console/agent/types.gen.ts
+++ b/packages/contracts/generated/api/console/agent/types.gen.ts
@@ -1882,6 +1882,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
+ id?: string | null
+ metadata_id?: string | null
name: string
value?: string | Array | number | null
}
diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts
index 9ad871a9582..9b9c8878a2a 100644
--- a/packages/contracts/generated/api/console/agent/zod.gen.ts
+++ b/packages/contracts/generated/api/console/agent/zod.gen.ts
@@ -2357,6 +2357,14 @@ export const zAgentKnowledgeRetrievalConfig = z.object({
/**
* AgentKnowledgeMetadataCondition
+ *
+ * One manual metadata filter clause.
+ *
+ * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
+ * every save (a stable row key and a reference to the selected metadata
+ * field). They are persisted here for round-tripping the composer's draft
+ * state but are stripped before building the Agent runtime request, whose
+ * DTO only accepts ``name``/``comparison_operator``/``value``.
*/
export const zAgentKnowledgeMetadataCondition = z.object({
comparison_operator: z.enum([
@@ -2379,6 +2387,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({
'≤',
'≥',
]),
+ id: z.string().nullish(),
+ metadata_id: z.string().nullish(),
name: z.string().min(1).max(255),
value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(),
})
diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts
index ff09e5e48f0..1c5202c86ee 100644
--- a/packages/contracts/generated/api/console/apps/types.gen.ts
+++ b/packages/contracts/generated/api/console/apps/types.gen.ts
@@ -3097,6 +3097,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
+ id?: string | null
+ metadata_id?: string | null
name: string
value?: string | Array | number | null
}
diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts
index 0693ecd3dbe..41ce64d22be 100644
--- a/packages/contracts/generated/api/console/apps/zod.gen.ts
+++ b/packages/contracts/generated/api/console/apps/zod.gen.ts
@@ -4048,6 +4048,14 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({
/**
* AgentKnowledgeMetadataCondition
+ *
+ * One manual metadata filter clause.
+ *
+ * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
+ * every save (a stable row key and a reference to the selected metadata
+ * field). They are persisted here for round-tripping the composer's draft
+ * state but are stripped before building the Agent runtime request, whose
+ * DTO only accepts ``name``/``comparison_operator``/``value``.
*/
export const zAgentKnowledgeMetadataCondition = z.object({
comparison_operator: z.enum([
@@ -4070,6 +4078,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({
'≤',
'≥',
]),
+ id: z.string().nullish(),
+ metadata_id: z.string().nullish(),
name: z.string().min(1).max(255),
value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(),
})
diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts
index d5b6a62cfc4..28b48d12ceb 100644
--- a/packages/contracts/generated/api/console/snippets/types.gen.ts
+++ b/packages/contracts/generated/api/console/snippets/types.gen.ts
@@ -1130,6 +1130,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
+ id?: string | null
+ metadata_id?: string | null
name: string
value?: string | Array | number | null
}
diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts
index 4474c465df9..31c5ee2e3d1 100644
--- a/packages/contracts/generated/api/console/snippets/zod.gen.ts
+++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts
@@ -1439,6 +1439,14 @@ export const zAgentKnowledgeRetrievalConfig = z.object({
/**
* AgentKnowledgeMetadataCondition
+ *
+ * One manual metadata filter clause.
+ *
+ * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
+ * every save (a stable row key and a reference to the selected metadata
+ * field). They are persisted here for round-tripping the composer's draft
+ * state but are stripped before building the Agent runtime request, whose
+ * DTO only accepts ``name``/``comparison_operator``/``value``.
*/
export const zAgentKnowledgeMetadataCondition = z.object({
comparison_operator: z.enum([
@@ -1461,6 +1469,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({
'≤',
'≥',
]),
+ id: z.string().nullish(),
+ metadata_id: z.string().nullish(),
name: z.string().min(1).max(255),
value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(),
})