diff --git a/docs/generated/postgres-schema/README.md b/docs/generated/postgres-schema/README.md
index 13aa56c7d7c..63e6a6a6e82 100644
--- a/docs/generated/postgres-schema/README.md
+++ b/docs/generated/postgres-schema/README.md
@@ -11,6 +11,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.agent_chat_attachments](public.agent_chat_attachments.md) | 12 | | BASE TABLE |
| [public.agent_chat_subscriptions](public.agent_chat_subscriptions.md) | 6 | | BASE TABLE |
| [public.agent_checkpoints](public.agent_checkpoints.md) | 6 | | BASE TABLE |
+| [public.agent_credential_dependency](public.agent_credential_dependency.md) | 3 | | BASE TABLE |
| [public.agent_eval_dataset](public.agent_eval_dataset.md) | 10 | | BASE TABLE |
| [public.agent_eval_rating](public.agent_eval_rating.md) | 8 | | BASE TABLE |
| [public.agent_eval_result](public.agent_eval_result.md) | 15 | | BASE TABLE |
@@ -158,6 +159,8 @@ erDiagram
"public.agent_chat_attachments" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_chat_subscriptions" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_checkpoints" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
+"public.agent_credential_dependency" }o--|| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE CASCADE"
+"public.agent_credential_dependency" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_eval_dataset" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.agent_eval_dataset" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_eval_rating" }o--o| "public.user" : "FOREIGN KEY (#quot;ratedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
@@ -361,6 +364,11 @@ erDiagram
text state
timestamp_3__with_time_zone updatedAt
}
+"public.agent_credential_dependency" {
+ varchar_36_ agentId FK
+ timestamp_3__with_time_zone createdAt
+ varchar_36_ credentialId FK
+}
"public.agent_eval_dataset" {
varchar_36_ agentId FK
json columnMapping
diff --git a/docs/generated/postgres-schema/public.agent_credential_dependency.md b/docs/generated/postgres-schema/public.agent_credential_dependency.md
new file mode 100644
index 00000000000..35482087357
--- /dev/null
+++ b/docs/generated/postgres-schema/public.agent_credential_dependency.md
@@ -0,0 +1,75 @@
+# public.agent_credential_dependency
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| agentId | varchar(36) | | false | | [public.agents](public.agents.md) | |
+| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
+| credentialId | varchar(36) | | false | | [public.credentials_entity](public.credentials_entity.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| FK_6a6948884969cb4204a1975578b | FOREIGN KEY | FOREIGN KEY ("agentId") REFERENCES agents(id) ON DELETE CASCADE |
+| FK_fec7ee37062350d6a4a2979327d | FOREIGN KEY | FOREIGN KEY ("credentialId") REFERENCES credentials_entity(id) ON DELETE CASCADE |
+| PK_546f5d9cfaf5ea78efcc6f5d0a2 | PRIMARY KEY | PRIMARY KEY ("agentId", "credentialId") |
+| agent_credential_dependency_agentId_not_null | n | NOT NULL "agentId" |
+| agent_credential_dependency_createdAt_not_null | n | NOT NULL "createdAt" |
+| agent_credential_dependency_credentialId_not_null | n | NOT NULL "credentialId" |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| IDX_fec7ee37062350d6a4a2979327 | CREATE INDEX "IDX_fec7ee37062350d6a4a2979327" ON public.agent_credential_dependency USING btree ("credentialId") |
+| PK_546f5d9cfaf5ea78efcc6f5d0a2 | CREATE UNIQUE INDEX "PK_546f5d9cfaf5ea78efcc6f5d0a2" ON public.agent_credential_dependency USING btree ("agentId", "credentialId") |
+
+## Relations
+
+```mermaid
+erDiagram
+
+"public.agent_credential_dependency" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
+"public.agent_credential_dependency" }o--|| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE CASCADE"
+
+"public.agent_credential_dependency" {
+ varchar_36_ agentId FK
+ timestamp_3__with_time_zone createdAt
+ varchar_36_ credentialId FK
+}
+"public.agents" {
+ varchar_36_ activeVersionId FK
+ boolean availableInMCP
+ timestamp_3__with_time_zone createdAt
+ varchar_36_ id
+ json integrations
+ varchar_128_ name
+ varchar_255_ projectId FK
+ json schema
+ timestamp_3__with_time_zone setupCompletedAt
+ json skills
+ json tools
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId
+}
+"public.credentials_entity" {
+ timestamp_3__with_time_zone createdAt
+ text data
+ varchar_36_ id
+ boolean isGlobal
+ boolean isManaged
+ boolean isResolvable
+ varchar_128_ name
+ boolean resolvableAllowFallback
+ varchar_16_ resolverId FK
+ varchar_128_ type
+ timestamp_3__with_time_zone updatedAt
+ varchar_16_ usageScope
+}
+```
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/generated/postgres-schema/public.agents.md b/docs/generated/postgres-schema/public.agents.md
index fa4ed6f9570..1e7807e49a7 100644
--- a/docs/generated/postgres-schema/public.agents.md
+++ b/docs/generated/postgres-schema/public.agents.md
@@ -7,7 +7,7 @@
| activeVersionId | varchar(36) | | true | | [public.agent_history](public.agent_history.md) | |
| availableInMCP | boolean | false | false | | | Whether MCP clients granted agent scopes may operate on this agent |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
-| id | varchar(36) | | false | [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_chat_subscriptions](public.agent_chat_subscriptions.md) [public.agent_checkpoints](public.agent_checkpoints.md) [public.agent_eval_dataset](public.agent_eval_dataset.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agent_files](public.agent_files.md) [public.agent_history](public.agent_history.md) [public.agent_task_definition](public.agent_task_definition.md) [public.agent_task_run_lock](public.agent_task_run_lock.md) [public.agents_memory_entries](public.agents_memory_entries.md) [public.agents_memory_entry_cursors](public.agents_memory_entry_cursors.md) [public.agents_memory_entry_locks](public.agents_memory_entry_locks.md) [public.agents_memory_entry_sources](public.agents_memory_entry_sources.md) [public.agents_observation_cursors](public.agents_observation_cursors.md) [public.agents_observation_locks](public.agents_observation_locks.md) [public.agents_observations](public.agents_observations.md) | | |
+| id | varchar(36) | | false | [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_chat_subscriptions](public.agent_chat_subscriptions.md) [public.agent_checkpoints](public.agent_checkpoints.md) [public.agent_credential_dependency](public.agent_credential_dependency.md) [public.agent_eval_dataset](public.agent_eval_dataset.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agent_files](public.agent_files.md) [public.agent_history](public.agent_history.md) [public.agent_task_definition](public.agent_task_definition.md) [public.agent_task_run_lock](public.agent_task_run_lock.md) [public.agents_memory_entries](public.agents_memory_entries.md) [public.agents_memory_entry_cursors](public.agents_memory_entry_cursors.md) [public.agents_memory_entry_locks](public.agents_memory_entry_locks.md) [public.agents_memory_entry_sources](public.agents_memory_entry_sources.md) [public.agents_observation_cursors](public.agents_observation_cursors.md) [public.agents_observation_locks](public.agents_observation_locks.md) [public.agents_observations](public.agents_observations.md) | | |
| integrations | json | '[]'::json | false | | | |
| name | varchar(128) | | false | | | |
| projectId | varchar(255) | | false | | [public.project](public.project.md) | |
@@ -52,6 +52,7 @@ erDiagram
"public.agent_chat_attachments" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_chat_subscriptions" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_checkpoints" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
+"public.agent_credential_dependency" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_eval_dataset" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_execution_threads" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agent_files" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
@@ -123,6 +124,11 @@ erDiagram
text state
timestamp_3__with_time_zone updatedAt
}
+"public.agent_credential_dependency" {
+ varchar_36_ agentId FK
+ timestamp_3__with_time_zone createdAt
+ varchar_36_ credentialId FK
+}
"public.agent_eval_dataset" {
varchar_36_ agentId FK
json columnMapping
diff --git a/docs/generated/postgres-schema/public.credentials_entity.md b/docs/generated/postgres-schema/public.credentials_entity.md
index 2f86bb6fb02..b7f144f087c 100644
--- a/docs/generated/postgres-schema/public.credentials_entity.md
+++ b/docs/generated/postgres-schema/public.credentials_entity.md
@@ -6,7 +6,7 @@
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| data | text | | false | | | |
-| id | varchar(36) | | false | [public.chat_hub_agents](public.chat_hub_agents.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.credential_dependency](public.credential_dependency.md) [public.dynamic_credential_entry](public.dynamic_credential_entry.md) [public.dynamic_credential_user_entry](public.dynamic_credential_user_entry.md) [public.instance_ai_mcp_registry_connections](public.instance_ai_mcp_registry_connections.md) [public.instance_credential_assignment](public.instance_credential_assignment.md) [public.shared_credentials](public.shared_credentials.md) | | |
+| id | varchar(36) | | false | [public.agent_credential_dependency](public.agent_credential_dependency.md) [public.chat_hub_agents](public.chat_hub_agents.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.credential_dependency](public.credential_dependency.md) [public.dynamic_credential_entry](public.dynamic_credential_entry.md) [public.dynamic_credential_user_entry](public.dynamic_credential_user_entry.md) [public.instance_ai_mcp_registry_connections](public.instance_ai_mcp_registry_connections.md) [public.instance_credential_assignment](public.instance_credential_assignment.md) [public.shared_credentials](public.shared_credentials.md) | | |
| isGlobal | boolean | false | false | | | |
| isManaged | boolean | false | false | | | |
| isResolvable | boolean | false | false | | | |
@@ -50,6 +50,7 @@
```mermaid
erDiagram
+"public.agent_credential_dependency" }o--|| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE CASCADE"
"public.chat_hub_agents" }o--o| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE SET NULL"
"public.chat_hub_sessions" }o--o| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE SET NULL"
"public.credential_dependency" }o--|| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE CASCADE"
@@ -74,6 +75,11 @@ erDiagram
timestamp_3__with_time_zone updatedAt
varchar_16_ usageScope
}
+"public.agent_credential_dependency" {
+ varchar_36_ agentId FK
+ timestamp_3__with_time_zone createdAt
+ varchar_36_ credentialId FK
+}
"public.chat_hub_agents" {
timestamp_3__with_time_zone createdAt
varchar_36_ credentialId FK
diff --git a/docs/generated/sqlite-schema/README.md b/docs/generated/sqlite-schema/README.md
index 7182604a07c..dad2bace939 100644
--- a/docs/generated/sqlite-schema/README.md
+++ b/docs/generated/sqlite-schema/README.md
@@ -11,6 +11,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [agent_chat_attachments](agent_chat_attachments.md) | 12 | | table |
| [agent_chat_subscriptions](agent_chat_subscriptions.md) | 6 | | table |
| [agent_checkpoints](agent_checkpoints.md) | 6 | | table |
+| [agent_credential_dependency](agent_credential_dependency.md) | 3 | | table |
| [agent_eval_dataset](agent_eval_dataset.md) | 10 | | table |
| [agent_eval_rating](agent_eval_rating.md) | 8 | | table |
| [agent_eval_result](agent_eval_result.md) | 15 | | table |
@@ -141,6 +142,8 @@ erDiagram
"agent_chat_attachments" }o--o| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_chat_subscriptions" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_checkpoints" }o--o| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"agent_credential_dependency" |o--|| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"agent_credential_dependency" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_eval_dataset" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"agent_eval_dataset" }o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_eval_rating" }o--o| "user" : "FOREIGN KEY (ratedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
@@ -348,6 +351,11 @@ erDiagram
TEXT state
datetime_3_ updatedAt
}
+"agent_credential_dependency" {
+ varchar_36_ agentId PK
+ datetime_3_ createdAt
+ varchar_36_ credentialId PK
+}
"agent_eval_dataset" {
varchar_36_ agentId FK
TEXT columnMapping
diff --git a/docs/generated/sqlite-schema/agent_credential_dependency.md b/docs/generated/sqlite-schema/agent_credential_dependency.md
new file mode 100644
index 00000000000..8c5026566cb
--- /dev/null
+++ b/docs/generated/sqlite-schema/agent_credential_dependency.md
@@ -0,0 +1,85 @@
+# agent_credential_dependency
+
+## Description
+
+
+Table Definition
+
+```sql
+CREATE TABLE "agent_credential_dependency" ("agentId" varchar(36) NOT NULL, "credentialId" varchar(36) NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "FK_6a6948884969cb4204a1975578b" FOREIGN KEY ("agentId") REFERENCES "agents" ("id") ON DELETE CASCADE, CONSTRAINT "FK_fec7ee37062350d6a4a2979327d" FOREIGN KEY ("credentialId") REFERENCES "credentials_entity" ("id") ON DELETE CASCADE, PRIMARY KEY ("agentId", "credentialId"))
+```
+
+
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| agentId | varchar(36) | | false | | [agents](agents.md) | |
+| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
+| credentialId | varchar(36) | | false | | [credentials_entity](credentials_entity.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
+| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
+| agentId | PRIMARY KEY | PRIMARY KEY (agentId) |
+| credentialId | PRIMARY KEY | PRIMARY KEY (credentialId) |
+| sqlite_autoindex_agent_credential_dependency_1 | PRIMARY KEY | PRIMARY KEY (agentId, credentialId) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| IDX_fec7ee37062350d6a4a2979327 | CREATE INDEX "IDX_fec7ee37062350d6a4a2979327" ON "agent_credential_dependency" ("credentialId") |
+| sqlite_autoindex_agent_credential_dependency_1 | PRIMARY KEY (agentId, credentialId) |
+
+## Relations
+
+```mermaid
+erDiagram
+
+"agent_credential_dependency" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"agent_credential_dependency" |o--|| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+
+"agent_credential_dependency" {
+ varchar_36_ agentId PK
+ datetime_3_ createdAt
+ varchar_36_ credentialId PK
+}
+"agents" {
+ varchar_36_ activeVersionId FK
+ boolean availableInMCP
+ datetime_3_ createdAt
+ varchar_36_ id PK
+ TEXT integrations
+ varchar_128_ name
+ varchar_255_ projectId FK
+ TEXT schema
+ datetime_3_ setupCompletedAt
+ TEXT skills
+ TEXT tools
+ datetime_3_ updatedAt
+ varchar_36_ versionId
+}
+"credentials_entity" {
+ datetime_3_ createdAt
+ TEXT data
+ varchar_36_ id PK
+ boolean isGlobal
+ boolean isManaged
+ boolean isResolvable
+ varchar_128_ name
+ boolean resolvableAllowFallback
+ varchar_16_ resolverId FK
+ varchar_32_ type
+ datetime_3_ updatedAt
+ VARCHAR_16_ usageScope
+}
+```
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/generated/sqlite-schema/agents.md b/docs/generated/sqlite-schema/agents.md
index 3934c965d7d..9fe1d9018a5 100644
--- a/docs/generated/sqlite-schema/agents.md
+++ b/docs/generated/sqlite-schema/agents.md
@@ -18,7 +18,7 @@ CREATE TABLE "agents" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128
| activeVersionId | varchar(36) | | true | | [agent_history](agent_history.md) | |
| availableInMCP | boolean | false | false | | | |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
-| id | varchar(36) | | false | [agent_chat_attachments](agent_chat_attachments.md) [agent_chat_subscriptions](agent_chat_subscriptions.md) [agent_checkpoints](agent_checkpoints.md) [agent_eval_dataset](agent_eval_dataset.md) [agent_execution_threads](agent_execution_threads.md) [agent_files](agent_files.md) [agent_history](agent_history.md) [agent_task_definition](agent_task_definition.md) [agent_task_run_lock](agent_task_run_lock.md) [agents_memory_entries](agents_memory_entries.md) [agents_memory_entry_cursors](agents_memory_entry_cursors.md) [agents_memory_entry_locks](agents_memory_entry_locks.md) [agents_memory_entry_sources](agents_memory_entry_sources.md) [agents_observation_cursors](agents_observation_cursors.md) [agents_observation_locks](agents_observation_locks.md) [agents_observations](agents_observations.md) | | |
+| id | varchar(36) | | false | [agent_chat_attachments](agent_chat_attachments.md) [agent_chat_subscriptions](agent_chat_subscriptions.md) [agent_checkpoints](agent_checkpoints.md) [agent_credential_dependency](agent_credential_dependency.md) [agent_eval_dataset](agent_eval_dataset.md) [agent_execution_threads](agent_execution_threads.md) [agent_files](agent_files.md) [agent_history](agent_history.md) [agent_task_definition](agent_task_definition.md) [agent_task_run_lock](agent_task_run_lock.md) [agents_memory_entries](agents_memory_entries.md) [agents_memory_entry_cursors](agents_memory_entry_cursors.md) [agents_memory_entry_locks](agents_memory_entry_locks.md) [agents_memory_entry_sources](agents_memory_entry_sources.md) [agents_observation_cursors](agents_observation_cursors.md) [agents_observation_locks](agents_observation_locks.md) [agents_observations](agents_observations.md) | | |
| integrations | TEXT | '[]' | false | | | |
| name | varchar(128) | | false | | | |
| projectId | varchar(255) | | false | | [project](project.md) | |
@@ -55,6 +55,7 @@ erDiagram
"agent_chat_attachments" }o--o| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_chat_subscriptions" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_checkpoints" }o--o| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"agent_credential_dependency" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_eval_dataset" }o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_execution_threads" }o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_files" }o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@@ -126,6 +127,11 @@ erDiagram
TEXT state
datetime_3_ updatedAt
}
+"agent_credential_dependency" {
+ varchar_36_ agentId PK
+ datetime_3_ createdAt
+ varchar_36_ credentialId PK
+}
"agent_eval_dataset" {
varchar_36_ agentId FK
TEXT columnMapping
diff --git a/docs/generated/sqlite-schema/credentials_entity.md b/docs/generated/sqlite-schema/credentials_entity.md
index 51be82bd4d3..5fa537d3546 100644
--- a/docs/generated/sqlite-schema/credentials_entity.md
+++ b/docs/generated/sqlite-schema/credentials_entity.md
@@ -17,7 +17,7 @@ CREATE TABLE "credentials_entity" ("id" varchar(36) PRIMARY KEY NOT NULL, "name"
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| data | TEXT | | false | | | |
-| id | varchar(36) | | false | [chat_hub_agents](chat_hub_agents.md) [chat_hub_sessions](chat_hub_sessions.md) [credential_dependency](credential_dependency.md) [dynamic_credential_entry](dynamic_credential_entry.md) [dynamic_credential_user_entry](dynamic_credential_user_entry.md) [instance_ai_mcp_registry_connections](instance_ai_mcp_registry_connections.md) [instance_credential_assignment](instance_credential_assignment.md) [shared_credentials](shared_credentials.md) | | |
+| id | varchar(36) | | false | [agent_credential_dependency](agent_credential_dependency.md) [chat_hub_agents](chat_hub_agents.md) [chat_hub_sessions](chat_hub_sessions.md) [credential_dependency](credential_dependency.md) [dynamic_credential_entry](dynamic_credential_entry.md) [dynamic_credential_user_entry](dynamic_credential_user_entry.md) [instance_ai_mcp_registry_connections](instance_ai_mcp_registry_connections.md) [instance_credential_assignment](instance_credential_assignment.md) [shared_credentials](shared_credentials.md) | | |
| isGlobal | boolean | 0 | false | | | |
| isManaged | boolean | 0 | false | | | |
| isResolvable | boolean | false | false | | | |
@@ -50,6 +50,7 @@ CREATE TABLE "credentials_entity" ("id" varchar(36) PRIMARY KEY NOT NULL, "name"
```mermaid
erDiagram
+"agent_credential_dependency" |o--|| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"chat_hub_agents" }o--o| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"chat_hub_sessions" }o--o| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"credential_dependency" }o--|| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@@ -74,6 +75,11 @@ erDiagram
datetime_3_ updatedAt
VARCHAR_16_ usageScope
}
+"agent_credential_dependency" {
+ varchar_36_ agentId PK
+ datetime_3_ createdAt
+ varchar_36_ credentialId PK
+}
"chat_hub_agents" {
datetime_3_ createdAt
varchar_36_ credentialId FK
diff --git a/packages/@n8n/api-types/src/schemas/dependency-counts.schema.ts b/packages/@n8n/api-types/src/schemas/dependency-counts.schema.ts
index a8320f8f0b3..2e0337c945d 100644
--- a/packages/@n8n/api-types/src/schemas/dependency-counts.schema.ts
+++ b/packages/@n8n/api-types/src/schemas/dependency-counts.schema.ts
@@ -1,4 +1,5 @@
export interface DependencyTypeCounts {
+ agentUsage: number;
credentialId: number;
dataTableId: number;
errorWorkflow: number;
diff --git a/packages/@n8n/api-types/src/schemas/dependency.schema.ts b/packages/@n8n/api-types/src/schemas/dependency.schema.ts
index 20d0c9ef1a7..2715f80d0fd 100644
--- a/packages/@n8n/api-types/src/schemas/dependency.schema.ts
+++ b/packages/@n8n/api-types/src/schemas/dependency.schema.ts
@@ -1,4 +1,5 @@
export type DependencyType =
+ | 'agentUsage'
| 'credentialId'
| 'dataTableId'
| 'errorWorkflow'
@@ -12,7 +13,7 @@ export interface ResolvedDependency {
type: DependencyType;
id: string;
name: string;
- /** Project ID — included for data tables so the frontend can build a direct link */
+ /** Project ID — included for agents and data tables so the frontend can build a direct link */
projectId?: string;
}
diff --git a/packages/@n8n/db/src/migrations/common/1785828155092-CreateAgentCredentialDependencyTable.ts b/packages/@n8n/db/src/migrations/common/1785828155092-CreateAgentCredentialDependencyTable.ts
new file mode 100644
index 00000000000..d77c705f283
--- /dev/null
+++ b/packages/@n8n/db/src/migrations/common/1785828155092-CreateAgentCredentialDependencyTable.ts
@@ -0,0 +1,26 @@
+import type { MigrationContext, ReversibleMigration } from '../migration-types';
+
+export class CreateAgentCredentialDependencyTable1785828155092 implements ReversibleMigration {
+ async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
+ await createTable('agent_credential_dependency')
+ .withColumns(
+ column('agentId').varchar(36).primary,
+ column('credentialId').varchar(36).primary,
+ )
+ .withForeignKey('agentId', {
+ tableName: 'agents',
+ columnName: 'id',
+ onDelete: 'CASCADE',
+ })
+ .withForeignKey('credentialId', {
+ tableName: 'credentials_entity',
+ columnName: 'id',
+ onDelete: 'CASCADE',
+ })
+ .withIndexOn(['credentialId']).withCreatedAt;
+ }
+
+ async down({ schemaBuilder: { dropTable } }: MigrationContext) {
+ await dropTable('agent_credential_dependency');
+ }
+}
diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts
index 9fcafac14c1..fc763e69d84 100644
--- a/packages/@n8n/db/src/migrations/postgresdb/index.ts
+++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts
@@ -238,6 +238,7 @@ import { AddMisfirePolicyToScheduler1785247194307 } from '../common/178524719430
import { CreateAgentChatAttachmentsTable1785255306000 } from '../common/1785255306000-CreateAgentChatAttachmentsTable';
import { AddSetupCompletedAtToAgents1785500832626 } from '../common/1785500832626-AddSetupCompletedAtToAgents';
import { AddAgentExecutionRuntimeState1785828155091 } from '../common/1785828155091-AddAgentExecutionRuntimeState';
+import { CreateAgentCredentialDependencyTable1785828155092 } from '../common/1785828155092-CreateAgentCredentialDependencyTable';
import type { Migration } from '../migration-types';
export const postgresMigrations: Migration[] = [
@@ -481,4 +482,5 @@ export const postgresMigrations: Migration[] = [
CreateAgentChatAttachmentsTable1785255306000,
AddSetupCompletedAtToAgents1785500832626,
AddAgentExecutionRuntimeState1785828155091,
+ CreateAgentCredentialDependencyTable1785828155092,
];
diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts
index e8fffaa8759..221ba629c25 100644
--- a/packages/@n8n/db/src/migrations/sqlite/index.ts
+++ b/packages/@n8n/db/src/migrations/sqlite/index.ts
@@ -230,6 +230,7 @@ import { AddAgentFileStorageColumns1785186578138 } from '../common/1785186578138
import { CrashStaleEnqueuedExecutions1785247194306 } from '../common/1785247194306-CrashStaleEnqueuedExecutions';
import { CreateAgentChatAttachmentsTable1785255306000 } from '../common/1785255306000-CreateAgentChatAttachmentsTable';
import { AddAgentExecutionRuntimeState1785828155091 } from '../common/1785828155091-AddAgentExecutionRuntimeState';
+import { CreateAgentCredentialDependencyTable1785828155092 } from '../common/1785828155092-CreateAgentCredentialDependencyTable';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@@ -463,6 +464,7 @@ const sqliteMigrations: Migration[] = [
CreateAgentChatAttachmentsTable1785255306000,
AddSetupCompletedAtToAgents1785500832626,
AddAgentExecutionRuntimeState1785828155091,
+ CreateAgentCredentialDependencyTable1785828155092,
];
export { sqliteMigrations };
diff --git a/packages/cli/src/events/maps/relay.event-map.ts b/packages/cli/src/events/maps/relay.event-map.ts
index a1df0e05569..7220680e64a 100644
--- a/packages/cli/src/events/maps/relay.event-map.ts
+++ b/packages/cli/src/events/maps/relay.event-map.ts
@@ -1057,6 +1057,10 @@ export type RelayEventMap = {
// #endregion
// region Agents
+ 'agent-saved': {
+ agentId: string;
+ };
+
'agent-deleted': {
agentId: string;
projectId: string;
diff --git a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts
index c716d06a2dc..91b08404221 100644
--- a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts
+++ b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts
@@ -6,6 +6,7 @@ import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { mock } from 'vitest-mock-extended';
import type { CredentialsService } from '@/credentials/credentials.service';
+import type { EventService } from '@/events/event.service';
import type { Telemetry } from '@/telemetry';
@@ -58,6 +59,7 @@ function makeService() {
const runtimeCacheService = mock();
const credentialsService = mock();
const workflowRepository = mock();
+ const eventService = mock();
const agentValidationService = mock();
const telemetry = mock();
@@ -87,6 +89,7 @@ function makeService() {
runtimeCacheService,
credentialsService,
workflowRepository,
+ eventService,
new AgentSetupCompletionService(agentValidationService, telemetry, agentRepository),
new AgentModificationTelemetryService(telemetry),
);
@@ -99,6 +102,7 @@ function makeService() {
runtimeCacheService,
credentialsService,
workflowRepository,
+ eventService,
agentValidationService,
telemetry,
};
@@ -215,7 +219,7 @@ describe('AgentConfigService', () => {
it('persists an explicit web-search disable and clears native provider tools', async () => {
// Regression: previously the disable was stripped on write and resurrected
// on read, so the config hash never changed and the builder looped.
- const { service, agentRepository } = makeService();
+ const { service, agentRepository, eventService } = makeService();
const agent = makeAgent({
schema: {
...baseConfig,
@@ -244,6 +248,7 @@ describe('AgentConfigService', () => {
// layer's freshness hash actually changes.
expect(result.config?.config?.webSearch).toEqual({ enabled: false });
expect(result.config?.providerTools).toEqual({});
+ expect(eventService.emit).toHaveBeenCalledWith('agent-saved', { agentId });
});
it('preserves omitted stored fields but clears explicitly empty integrations', async () => {
diff --git a/packages/cli/src/modules/agents/__tests__/agent-credential-dependency.repository.test.ts b/packages/cli/src/modules/agents/__tests__/agent-credential-dependency.repository.test.ts
new file mode 100644
index 00000000000..3f302c856ad
--- /dev/null
+++ b/packages/cli/src/modules/agents/__tests__/agent-credential-dependency.repository.test.ts
@@ -0,0 +1,171 @@
+/* eslint-disable @typescript-eslint/unbound-method */
+import type { CredentialsEntity, OperationContext, TransactionRunner } from '@n8n/db';
+import type { DataSource, SelectQueryBuilder } from '@n8n/typeorm';
+import { mock, type MockProxy } from 'vitest-mock-extended';
+
+import { mockEntityManager } from '@test/mocking';
+
+import { AgentCredentialDependency } from '../entities/agent-credential-dependency.entity';
+import type { AgentHistory } from '../entities/agent-history.entity';
+import { Agent } from '../entities/agent.entity';
+import { AgentCredentialDependencyRepository } from '../repositories/agent-credential-dependency.repository';
+
+const entityManager = mockEntityManager(AgentCredentialDependency);
+const mockDataSource = { manager: entityManager };
+
+const makeAgent = (): Agent =>
+ ({
+ id: 'agent-1',
+ schema: {
+ name: 'Support Agent',
+ model: 'openai/gpt-4.1-mini',
+ instructions: 'Help the user',
+ credential: 'credential-draft',
+ memory: {
+ enabled: true,
+ storage: 'n8n',
+ episodicMemory: { enabled: true, credential: 'credential-shared' },
+ },
+ },
+ integrations: [{ type: 'slack', credentialId: 'credential-integration' }],
+ activeVersionId: 'published-version-1',
+ }) as Agent;
+
+const makePublishedVersion = (): AgentHistory =>
+ ({
+ versionId: 'published-version-1',
+ agentId: 'agent-1',
+ schema: {
+ name: 'Support Agent',
+ model: 'openai/gpt-4.1-mini',
+ instructions: 'Help the user',
+ credential: 'credential-published',
+ tools: [
+ {
+ type: 'node',
+ name: 'OpenAI tool',
+ node: {
+ nodeType: 'n8n-nodes-base.openAiTool',
+ nodeTypeVersion: 1,
+ nodeParameters: {},
+ credentials: {
+ openAiApi: { id: 'credential-shared', name: 'OpenAI account' },
+ },
+ },
+ },
+ ],
+ },
+ }) as unknown as AgentHistory;
+
+describe('AgentCredentialDependencyRepository', () => {
+ let repository: AgentCredentialDependencyRepository;
+ let txRunner: MockProxy;
+ let credentialQuery: MockProxy>;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(entityManager.connection, { options: { type: 'sqlite' } });
+ txRunner = mock();
+ txRunner.run.mockImplementation(
+ async (ctx: OperationContext, fn: (ctx: OperationContext) => Promise) => await fn(ctx),
+ );
+ credentialQuery = mock>();
+ credentialQuery.select.mockReturnValue(credentialQuery);
+ credentialQuery.where.mockReturnValue(credentialQuery);
+ entityManager.createQueryBuilder.mockReturnValue(credentialQuery);
+ repository = new AgentCredentialDependencyRepository(
+ mockDataSource as unknown as DataSource,
+ txRunner,
+ );
+ });
+
+ it('atomically rebuilds the union of draft and published credential references', async () => {
+ const agent = makeAgent();
+ const publishedVersion = makePublishedVersion();
+ entityManager.findOne.mockImplementation(async (entity) => {
+ if (entity === Agent) return agent;
+ return publishedVersion;
+ });
+ credentialQuery.getMany.mockResolvedValue([
+ mock({ id: 'credential-draft' }),
+ mock({ id: 'credential-shared' }),
+ mock({ id: 'credential-integration' }),
+ mock({ id: 'credential-published' }),
+ ]);
+
+ await repository.refreshForAgent(agent.id);
+
+ expect(entityManager.delete).toHaveBeenCalledWith(AgentCredentialDependency, {
+ agentId: agent.id,
+ });
+ expect(entityManager.insert).toHaveBeenCalledWith(AgentCredentialDependency, [
+ {
+ agentId: agent.id,
+ credentialId: 'credential-draft',
+ },
+ {
+ agentId: agent.id,
+ credentialId: 'credential-shared',
+ },
+ {
+ agentId: agent.id,
+ credentialId: 'credential-integration',
+ },
+ {
+ agentId: agent.id,
+ credentialId: 'credential-published',
+ },
+ ]);
+ });
+
+ it('filters stale credential ids without failing the replacement', async () => {
+ const agent = makeAgent();
+ agent.activeVersionId = null;
+ entityManager.findOne.mockResolvedValue(agent);
+ credentialQuery.getMany.mockResolvedValue([
+ mock({ id: 'credential-draft' }),
+ ]);
+
+ await repository.refreshForAgent(agent.id);
+
+ expect(entityManager.insert).toHaveBeenCalledWith(AgentCredentialDependency, [
+ {
+ agentId: agent.id,
+ credentialId: 'credential-draft',
+ },
+ ]);
+ });
+
+ it('leaves the replacement empty when the agent no longer exists', async () => {
+ entityManager.findOne.mockResolvedValue(null);
+
+ await repository.refreshForAgent('deleted-agent');
+
+ expect(entityManager.delete).toHaveBeenCalledWith(AgentCredentialDependency, {
+ agentId: 'deleted-agent',
+ });
+ expect(entityManager.createQueryBuilder).not.toHaveBeenCalled();
+ expect(entityManager.insert).not.toHaveBeenCalled();
+ });
+
+ it('locks the current agent and referenced credentials before replacing rows on Postgres', async () => {
+ Object.assign(entityManager.connection, { options: { type: 'postgres' } });
+ const agent = makeAgent();
+ agent.activeVersionId = null;
+ entityManager.findOne.mockResolvedValue(agent);
+ credentialQuery.getMany.mockResolvedValue([
+ mock({ id: 'credential-draft' }),
+ ]);
+
+ await repository.refreshForAgent(agent.id);
+
+ expect(entityManager.findOne).toHaveBeenCalledWith(Agent, {
+ where: { id: agent.id },
+ lock: { mode: 'pessimistic_write' },
+ });
+ expect(credentialQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
+ expect(credentialQuery.getMany.mock.invocationCallOrder[0]).toBeLessThan(
+ entityManager.delete.mock.invocationCallOrder[0],
+ );
+ });
+});
diff --git a/packages/cli/src/modules/agents/__tests__/agent-credential-index.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-credential-index.service.test.ts
new file mode 100644
index 00000000000..63487b2e13d
--- /dev/null
+++ b/packages/cli/src/modules/agents/__tests__/agent-credential-index.service.test.ts
@@ -0,0 +1,67 @@
+/* eslint-disable @typescript-eslint/unbound-method */
+import type { Logger } from '@n8n/backend-common';
+import type { WorkflowsConfig } from '@n8n/config';
+import { mock } from 'vitest-mock-extended';
+
+import { AgentCredentialIndexService } from '../agent-credential-index.service';
+import type { AgentCredentialDependencyRepository } from '../repositories/agent-credential-dependency.repository';
+import type { AgentRepository } from '../repositories/agent.repository';
+
+function makeService(batchSize = 2) {
+ const dependencyRepository = mock();
+ const agentRepository = mock();
+ const workflowsConfig = mock({ indexingBatchSize: batchSize });
+ const logger = mock();
+ logger.scoped.mockReturnValue(logger);
+ const service = new AgentCredentialIndexService(
+ dependencyRepository,
+ agentRepository,
+ logger,
+ workflowsConfig,
+ );
+
+ return { service, dependencyRepository, agentRepository, logger };
+}
+
+describe('AgentCredentialIndexService', () => {
+ it('refreshes both sources from current persisted Agent state', async () => {
+ const { service, dependencyRepository } = makeService();
+
+ await service.refresh('agent-1');
+
+ expect(dependencyRepository.refreshForAgent).toHaveBeenCalledWith('agent-1');
+ });
+
+ it('removes all rows as an idempotent fallback when an agent is deleted', async () => {
+ const { service, dependencyRepository } = makeService();
+
+ await service.remove('agent-1');
+
+ expect(dependencyRepository.removeForAgent).toHaveBeenCalledWith('agent-1');
+ });
+
+ it('rebuilds every agent in batches and continues after an individual failure', async () => {
+ const { service, dependencyRepository, agentRepository, logger } = makeService(2);
+ agentRepository.findCredentialIndexAgentIdsBatch
+ .mockResolvedValueOnce([{ id: 'agent-a' }, { id: 'agent-b' }])
+ .mockResolvedValueOnce([{ id: 'agent-c' }]);
+ dependencyRepository.refreshForAgent.mockImplementation(async (agentId) => {
+ if (agentId === 'agent-b') throw new Error('transient failure');
+ });
+
+ await service.buildIndex();
+
+ expect(agentRepository.findCredentialIndexAgentIdsBatch).toHaveBeenNthCalledWith(1, null, 2);
+ expect(agentRepository.findCredentialIndexAgentIdsBatch).toHaveBeenNthCalledWith(
+ 2,
+ 'agent-b',
+ 2,
+ );
+ expect(dependencyRepository.refreshForAgent).toHaveBeenCalledTimes(3);
+ expect(dependencyRepository.refreshForAgent).toHaveBeenCalledWith('agent-c');
+ expect(logger.error).toHaveBeenCalledWith('Failed to index agent credential dependencies', {
+ agentId: 'agent-b',
+ error: expect.any(Error),
+ });
+ });
+});
diff --git a/packages/cli/src/modules/agents/__tests__/agent-integration-persistence.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-integration-persistence.service.test.ts
index 1ccbd4dd6a7..058dc107fdb 100644
--- a/packages/cli/src/modules/agents/__tests__/agent-integration-persistence.service.test.ts
+++ b/packages/cli/src/modules/agents/__tests__/agent-integration-persistence.service.test.ts
@@ -5,6 +5,7 @@ import { UserError } from 'n8n-workflow';
import { mock } from 'vitest-mock-extended';
import type { CredentialsService } from '@/credentials/credentials.service';
+import type { EventService } from '@/events/event.service';
import type { Telemetry } from '@/telemetry';
import { AgentIntegrationPersistenceService } from '../agent-integration-persistence.service';
@@ -52,6 +53,7 @@ function makeService() {
const chatIntegrationService = mock();
const runtimeCacheService = mock();
const chatIntegrationRegistry = mock();
+ const eventService = mock();
const telemetry = mock();
const credentialsService = mock();
const setupCompletionService = mock();
@@ -65,6 +67,7 @@ function makeService() {
chatIntegrationService,
runtimeCacheService,
chatIntegrationRegistry,
+ eventService,
new AgentModificationTelemetryService(telemetry),
credentialsService,
setupCompletionService,
@@ -73,6 +76,7 @@ function makeService() {
chatIntegrationService,
runtimeCacheService,
chatIntegrationRegistry,
+ eventService,
telemetry,
credentialsService,
setupCompletionService,
@@ -121,7 +125,8 @@ describe('AgentIntegrationPersistenceService', () => {
});
it('appends a credential integration to an empty list and invalidates the runtime cache', async () => {
- const { service, agentRepository, chatIntegrationService, runtimeCacheService } = makeService();
+ const { service, agentRepository, chatIntegrationService, runtimeCacheService, eventService } =
+ makeService();
const agent = makeAgent();
await service.saveCredentialIntegration(
@@ -134,6 +139,7 @@ describe('AgentIntegrationPersistenceService', () => {
expect(agent.versionId).not.toBe(agent.activeVersionId);
expect(runtimeCacheService.clearRuntimes).toHaveBeenCalledWith(agentId);
expect(agentRepository.save).toHaveBeenCalledWith(agent);
+ expect(eventService.emit).toHaveBeenCalledWith('agent-saved', { agentId });
expect(chatIntegrationService.broadcastIntegrationChange).toHaveBeenCalledWith(
agentId,
{ type: 'slack', credentialId: 'slack-1' },
diff --git a/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts
index 57b8ba2ebff..04089aa8b03 100644
--- a/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts
+++ b/packages/cli/src/modules/agents/__tests__/agent-publish.service.test.ts
@@ -6,6 +6,7 @@ import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { mock } from 'vitest-mock-extended';
import type { CredentialsService } from '@/credentials/credentials.service';
+import type { EventService } from '@/events/event.service';
import type { Telemetry } from '@/telemetry';
import type { AgentCustomToolsService } from '../agent-custom-tools.service';
@@ -113,6 +114,7 @@ function makeService() {
const agentValidationService = mock();
const credentialsService = mock();
const telemetry = mock();
+ const eventService = mock();
const { trx, taskRepo, transaction } = makeTransaction();
Object.defineProperty(agentRepository, 'manager', {
@@ -152,6 +154,7 @@ function makeService() {
agentValidationService,
credentialsService,
telemetry,
+ eventService,
new AgentSetupCompletionService(agentValidationService, telemetry, agentRepository),
new AgentModificationTelemetryService(telemetry),
);
@@ -170,6 +173,7 @@ function makeService() {
agentValidationService,
credentialsService,
telemetry,
+ eventService,
trx,
taskRepo,
};
@@ -262,6 +266,7 @@ describe('AgentPublishService', () => {
chatIntegrationService,
agentValidationService,
telemetry,
+ eventService,
trx,
} = makeService();
const configuredTools = { tool: { descriptor: { name: 'tool' } } };
@@ -328,6 +333,7 @@ describe('AgentPublishService', () => {
);
expect(agent.activeVersionId).toBe(versionId);
expect(runtimeCacheService.clearRuntimes).toHaveBeenCalledWith(agentId);
+ expect(eventService.emit).toHaveBeenCalledWith('agent-saved', { agentId });
expect(chatIntegrationService.syncToConfig).toHaveBeenCalledWith(agent, [], integrations);
expect(telemetry.track).toHaveBeenCalledWith(
TELEMETRY_EVENT.AGENTS.BUILDER_PUBLISHED_AGENT,
diff --git a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts
index fc96e09793f..54ed75a2737 100644
--- a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts
+++ b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts
@@ -254,6 +254,7 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
runtimeCacheService,
credentialsService,
mock(),
+ mock(),
mock(),
modificationTelemetry,
);
@@ -278,6 +279,7 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
chatIntegrationService,
runtimeCacheService,
chatIntegrationRegistry,
+ mock(),
modificationTelemetry,
credentialsService,
mock(),
@@ -303,6 +305,7 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
agentValidationService,
credentialsService,
telemetry,
+ mock(),
mock(),
mock(),
);
diff --git a/packages/cli/src/modules/agents/agent-config.service.ts b/packages/cli/src/modules/agents/agent-config.service.ts
index c3a198264ac..06d104b4302 100644
--- a/packages/cli/src/modules/agents/agent-config.service.ts
+++ b/packages/cli/src/modules/agents/agent-config.service.ts
@@ -14,6 +14,7 @@ import { UserError } from 'n8n-workflow';
import { CredentialsService } from '@/credentials/credentials.service';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
+import { EventService } from '@/events/event.service';
import {
AgentModificationTelemetryService,
@@ -46,6 +47,7 @@ export class AgentConfigService {
private readonly runtimeCacheService: AgentRuntimeCacheService,
private readonly credentialsService: CredentialsService,
private readonly workflowRepository: WorkflowRepository,
+ private readonly eventService: EventService,
private readonly setupCompletionService: AgentSetupCompletionService,
private readonly modificationTelemetry: AgentModificationTelemetryService,
) {}
@@ -264,6 +266,7 @@ export class AgentConfigService {
);
const saved = await this.agentRepository.save(entity);
+ this.eventService.emit('agent-saved', { agentId });
this.logger.debug('Updated agent JSON config', { agentId, projectId });
this.modificationTelemetry.record({
diff --git a/packages/cli/src/modules/agents/agent-credential-index.listener.ts b/packages/cli/src/modules/agents/agent-credential-index.listener.ts
new file mode 100644
index 00000000000..71c2ff31809
--- /dev/null
+++ b/packages/cli/src/modules/agents/agent-credential-index.listener.ts
@@ -0,0 +1,43 @@
+import { Logger } from '@n8n/backend-common';
+import { Service } from '@n8n/di';
+
+import { EventService } from '@/events/event.service';
+
+import { AgentCredentialIndexService } from './agent-credential-index.service';
+
+@Service()
+export class AgentCredentialIndexListener {
+ constructor(
+ private readonly logger: Logger,
+ private readonly eventService: EventService,
+ private readonly indexService: AgentCredentialIndexService,
+ ) {
+ this.logger = this.logger.scoped('agents');
+ }
+
+ init(): void {
+ this.eventService.on('server-started', async () => {
+ await this.run('build credential dependency index', async () => {
+ await this.indexService.buildIndex();
+ });
+ });
+ this.eventService.on('agent-saved', async ({ agentId }) => {
+ await this.run('refresh credential dependency index', async () => {
+ await this.indexService.refresh(agentId);
+ });
+ });
+ this.eventService.on('agent-deleted', async ({ agentId }) => {
+ await this.run('remove credential dependency index entries', async () => {
+ await this.indexService.remove(agentId);
+ });
+ });
+ }
+
+ private async run(action: string, operation: () => Promise): Promise {
+ try {
+ await operation();
+ } catch (error) {
+ this.logger.error(`Failed to ${action}`, { error });
+ }
+ }
+}
diff --git a/packages/cli/src/modules/agents/agent-credential-index.service.ts b/packages/cli/src/modules/agents/agent-credential-index.service.ts
new file mode 100644
index 00000000000..4b88ca8b5ac
--- /dev/null
+++ b/packages/cli/src/modules/agents/agent-credential-index.service.ts
@@ -0,0 +1,55 @@
+import { Logger } from '@n8n/backend-common';
+import { WorkflowsConfig } from '@n8n/config';
+import { Service } from '@n8n/di';
+
+import { AgentCredentialDependencyRepository } from './repositories/agent-credential-dependency.repository';
+import { AgentRepository } from './repositories/agent.repository';
+
+@Service()
+export class AgentCredentialIndexService {
+ private readonly batchSize: number;
+
+ private readonly logger: Logger;
+
+ constructor(
+ private readonly dependencyRepository: AgentCredentialDependencyRepository,
+ private readonly agentRepository: AgentRepository,
+ logger: Logger,
+ workflowsConfig: WorkflowsConfig,
+ ) {
+ this.logger = logger.scoped('agents');
+ this.batchSize = workflowsConfig.indexingBatchSize;
+ }
+
+ async remove(agentId: string): Promise {
+ await this.dependencyRepository.removeForAgent(agentId);
+ }
+
+ async refresh(agentId: string): Promise {
+ await this.dependencyRepository.refreshForAgent(agentId);
+ }
+
+ async buildIndex(): Promise {
+ let afterId: string | null = null;
+
+ while (true) {
+ const agents = await this.agentRepository.findCredentialIndexAgentIdsBatch(
+ afterId,
+ this.batchSize,
+ );
+
+ for (const { id: agentId } of agents) {
+ try {
+ await this.refresh(agentId);
+ } catch (error) {
+ this.logger.error('Failed to index agent credential dependencies', { agentId, error });
+ }
+ }
+
+ if (agents.length < this.batchSize) return;
+ const lastAgent = agents[agents.length - 1];
+ if (lastAgent === undefined) return;
+ afterId = lastAgent.id;
+ }
+ }
+}
diff --git a/packages/cli/src/modules/agents/agent-integration-persistence.service.ts b/packages/cli/src/modules/agents/agent-integration-persistence.service.ts
index 8d065d17f84..f4abeda3fb6 100644
--- a/packages/cli/src/modules/agents/agent-integration-persistence.service.ts
+++ b/packages/cli/src/modules/agents/agent-integration-persistence.service.ts
@@ -9,6 +9,7 @@ import { Service } from '@n8n/di';
import { UserError } from 'n8n-workflow';
import { CredentialsService } from '@/credentials/credentials.service';
+import { EventService } from '@/events/event.service';
import {
AgentModificationTelemetryService,
@@ -38,6 +39,7 @@ export class AgentIntegrationPersistenceService {
private readonly chatIntegrationService: ChatIntegrationService,
private readonly runtimeCacheService: AgentRuntimeCacheService,
private readonly chatIntegrationRegistry: ChatIntegrationRegistry,
+ private readonly eventService: EventService,
private readonly modificationTelemetry: AgentModificationTelemetryService,
private readonly credentialsService: CredentialsService,
private readonly setupCompletionService: AgentSetupCompletionService,
@@ -117,6 +119,7 @@ export class AgentIntegrationPersistenceService {
context.user,
);
const result = await this.agentRepository.save(agent);
+ this.eventService.emit('agent-saved', { agentId: agent.id });
await emitSetupCompleted?.();
await this.recordIntegrationMutation(
result,
@@ -159,6 +162,7 @@ export class AgentIntegrationPersistenceService {
markAgentDraftDirty(agent);
this.runtimeCacheService.clearRuntimes(agent.id);
const result = await this.agentRepository.save(agent);
+ this.eventService.emit('agent-saved', { agentId: agent.id });
await this.recordIntegrationMutation(
result,
previousSchema,
diff --git a/packages/cli/src/modules/agents/agent-publish.service.ts b/packages/cli/src/modules/agents/agent-publish.service.ts
index e93ef05d87f..eb3a9730124 100644
--- a/packages/cli/src/modules/agents/agent-publish.service.ts
+++ b/packages/cli/src/modules/agents/agent-publish.service.ts
@@ -16,6 +16,7 @@ import { v4 as uuid } from 'uuid';
import { CredentialsService } from '@/credentials/credentials.service';
import { ConflictError } from '@/errors/response-errors/conflict.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
+import { EventService } from '@/events/event.service';
import { getMissingSkillIds } from '@/modules/agents/utils/agent-missing-skill-ids';
import { Telemetry } from '@/telemetry';
@@ -99,6 +100,7 @@ export class AgentPublishService {
private readonly agentValidationService: AgentValidationService,
private readonly credentialsService: CredentialsService,
private readonly telemetry: Telemetry,
+ private readonly eventService: EventService,
private readonly setupCompletionService: AgentSetupCompletionService,
private readonly modificationTelemetry: AgentModificationTelemetryService,
) {}
@@ -174,6 +176,7 @@ export class AgentPublishService {
await trx.save(agent);
});
+ this.eventService.emit('agent-saved', { agentId });
this.runtimeCacheService.clearRuntimes(agentId);
@@ -262,6 +265,7 @@ export class AgentPublishService {
await trx.save(agent);
});
+ this.eventService.emit('agent-saved', { agentId });
this.runtimeCacheService.clearRuntimes(agentId);
@@ -414,6 +418,7 @@ export class AgentPublishService {
await trx.save(agent);
tasksChanged = await this.restoreTasksFromSnapshot(trx, agentId, activeVersion.versionId);
});
+ this.eventService.emit('agent-saved', { agentId });
this.runtimeCacheService.clearRuntimes(agentId);
await this.recordRevert(agent, projectId, user, modifiedBy, previousSchema, {
@@ -465,6 +470,7 @@ export class AgentPublishService {
await trx.save(agent);
tasksChanged = await this.restoreTasksFromSnapshot(trx, agentId, target.versionId);
});
+ this.eventService.emit('agent-saved', { agentId });
this.runtimeCacheService.clearRuntimes(agentId);
await this.recordRevert(agent, projectId, user, modifiedBy, previousSchema, {
diff --git a/packages/cli/src/modules/agents/agents.module.ts b/packages/cli/src/modules/agents/agents.module.ts
index 409cf74c6ea..34caf87f065 100644
--- a/packages/cli/src/modules/agents/agents.module.ts
+++ b/packages/cli/src/modules/agents/agents.module.ts
@@ -29,6 +29,9 @@ export class AgentsModule implements ModuleInterface {
const { AgentsService } = await import('./agents.service.js');
Container.get(AgentsService);
+ const { AgentCredentialIndexListener } = await import('./agent-credential-index.listener.js');
+ Container.get(AgentCredentialIndexListener).init();
+
const { AgentsBuilderSettingsService } = await import(
'./builder/agents-builder-settings.service.js'
);
@@ -165,6 +168,9 @@ export class AgentsModule implements ModuleInterface {
const { AgentExecutionThread } = await import('./entities/agent-execution-thread.entity.js');
const { AgentExecution } = await import('./entities/agent-execution.entity.js');
const { AgentHistory } = await import('./entities/agent-history.entity.js');
+ const { AgentCredentialDependency } = await import(
+ './entities/agent-credential-dependency.entity.js'
+ );
const { AgentTask } = await import('./entities/agent-task.entity.js');
const { AgentTaskRunLock } = await import('./entities/agent-task-run-lock.entity.js');
const { AgentTaskSnapshot } = await import('./entities/agent-task-snapshot.entity.js');
@@ -198,6 +204,7 @@ export class AgentsModule implements ModuleInterface {
AgentExecutionThread,
AgentExecution,
AgentHistory,
+ AgentCredentialDependency,
AgentTask,
AgentTaskRunLock,
AgentTaskSnapshot,
diff --git a/packages/cli/src/modules/agents/entities/agent-credential-dependency.entity.ts b/packages/cli/src/modules/agents/entities/agent-credential-dependency.entity.ts
new file mode 100644
index 00000000000..28da9964a4d
--- /dev/null
+++ b/packages/cli/src/modules/agents/entities/agent-credential-dependency.entity.ts
@@ -0,0 +1,22 @@
+import { type CredentialsEntity, WithCreatedAt } from '@n8n/db';
+import { Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, type Relation } from '@n8n/typeorm';
+
+import type { Agent } from './agent.entity';
+
+@Entity({ name: 'agent_credential_dependency' })
+export class AgentCredentialDependency extends WithCreatedAt {
+ @PrimaryColumn({ type: 'varchar', length: 36 })
+ agentId: string;
+
+ @ManyToOne('Agent', { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'agentId' })
+ agent: Relation;
+
+ @Index()
+ @PrimaryColumn({ type: 'varchar', length: 36 })
+ credentialId: string;
+
+ @ManyToOne('CredentialsEntity', { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'credentialId' })
+ credential: Relation;
+}
diff --git a/packages/cli/src/modules/agents/repositories/agent-credential-dependency.repository.ts b/packages/cli/src/modules/agents/repositories/agent-credential-dependency.repository.ts
new file mode 100644
index 00000000000..8bac460a333
--- /dev/null
+++ b/packages/cli/src/modules/agents/repositories/agent-credential-dependency.repository.ts
@@ -0,0 +1,107 @@
+import { BaseRepository, CredentialsEntity, TransactionRunner } from '@n8n/db';
+import { Service } from '@n8n/di';
+import { DataSource, In, type EntityManager } from '@n8n/typeorm';
+
+import { AgentCredentialDependency } from '../entities/agent-credential-dependency.entity';
+import { AgentHistory } from '../entities/agent-history.entity';
+import { Agent } from '../entities/agent.entity';
+import { extractAgentCredentialIds } from '../utils/extract-agent-credential-ids';
+
+export type AgentCredentialDependencyReference = Pick<
+ AgentCredentialDependency,
+ 'agentId' | 'credentialId'
+>;
+
+@Service()
+export class AgentCredentialDependencyRepository extends BaseRepository {
+ constructor(
+ dataSource: DataSource,
+ private readonly txRunner: TransactionRunner,
+ ) {
+ super(AgentCredentialDependency, dataSource.manager);
+ }
+
+ async refreshForAgent(agentId: string): Promise {
+ await this.txRunner.run({}, async (ctx) => {
+ const manager = this.managerFor(ctx);
+ const isPostgres = manager.connection.options.type === 'postgres';
+
+ // Starting with a write serializes the authoritative re-read on SQLite.
+ if (!isPostgres) {
+ await manager.delete(AgentCredentialDependency, { agentId });
+ }
+
+ const agent = isPostgres
+ ? await manager.findOne(Agent, {
+ where: { id: agentId },
+ lock: { mode: 'pessimistic_write' },
+ })
+ : await manager.findOne(Agent, { where: { id: agentId } });
+
+ if (agent === null) {
+ if (isPostgres) await manager.delete(AgentCredentialDependency, { agentId });
+ return;
+ }
+
+ const publishedVersion =
+ agent.activeVersionId === null
+ ? null
+ : await manager.findOne(AgentHistory, {
+ where: { versionId: agent.activeVersionId, agentId },
+ });
+ const draftCredentialIds = extractAgentCredentialIds([agent.schema, agent.integrations]);
+ const publishedCredentialIds = extractAgentCredentialIds(publishedVersion?.schema);
+ const referencedCredentialIds = new Set([...draftCredentialIds, ...publishedCredentialIds]);
+
+ const existingCredentialIds = await this.findExistingCredentialIds(
+ manager,
+ referencedCredentialIds,
+ isPostgres,
+ );
+
+ if (isPostgres) {
+ await manager.delete(AgentCredentialDependency, { agentId });
+ }
+
+ const rows = [...referencedCredentialIds]
+ .filter((credentialId) => existingCredentialIds.has(credentialId))
+ .map((credentialId) => ({ agentId, credentialId }));
+
+ if (rows.length > 0) {
+ await manager.insert(AgentCredentialDependency, rows);
+ }
+ });
+ }
+
+ async removeForAgent(agentId: string): Promise {
+ await this.managerFor({}).delete(AgentCredentialDependency, { agentId });
+ }
+
+ async findByCredentialIds(
+ credentialIds: string[],
+ ): Promise {
+ if (credentialIds.length === 0) return [];
+
+ return await this.find({
+ select: ['agentId', 'credentialId'],
+ where: { credentialId: In(credentialIds) },
+ });
+ }
+
+ private async findExistingCredentialIds(
+ manager: EntityManager,
+ credentialIds: ReadonlySet,
+ lockForUpdate: boolean,
+ ): Promise> {
+ if (credentialIds.size === 0) return new Set();
+
+ const query = manager
+ .createQueryBuilder(CredentialsEntity, 'credential')
+ .select(['credential.id'])
+ .where('credential.id IN (:...credentialIds)', { credentialIds: [...credentialIds] });
+ query.orderBy('credential.id', 'ASC');
+ if (lockForUpdate) query.setLock('pessimistic_write');
+
+ return new Set((await query.getMany()).map(({ id }) => id));
+ }
+}
diff --git a/packages/cli/src/modules/agents/repositories/agent.repository.ts b/packages/cli/src/modules/agents/repositories/agent.repository.ts
index 8c82723ba31..883904706d9 100644
--- a/packages/cli/src/modules/agents/repositories/agent.repository.ts
+++ b/packages/cli/src/modules/agents/repositories/agent.repository.ts
@@ -159,6 +159,33 @@ export class AgentRepository extends Repository {
});
}
+ async findCredentialIndexAgentIdsBatch(
+ afterId: string | null,
+ batchSize: number,
+ ): Promise>> {
+ const query = this.createQueryBuilder('agent')
+ .select(['agent.id'])
+ .orderBy('agent.id', 'ASC')
+ .take(batchSize);
+
+ if (afterId !== null) {
+ query.where('agent.id > :afterId', { afterId });
+ }
+
+ return await query.getMany();
+ }
+
+ async findSummariesByIds(
+ ids: string[],
+ ): Promise>> {
+ if (ids.length === 0) return [];
+
+ return await this.find({
+ select: ['id', 'name', 'projectId'],
+ where: { id: In(ids) },
+ });
+ }
+
async findByIdInProjects(id: string, projectIds: string[]): Promise {
if (projectIds.length === 0) return null;
return await this.findOne({
diff --git a/packages/cli/src/modules/agents/utils/__tests__/extract-agent-credential-ids.test.ts b/packages/cli/src/modules/agents/utils/__tests__/extract-agent-credential-ids.test.ts
new file mode 100644
index 00000000000..532f77abb6e
--- /dev/null
+++ b/packages/cli/src/modules/agents/utils/__tests__/extract-agent-credential-ids.test.ts
@@ -0,0 +1,105 @@
+import { AI_GATEWAY_MANAGED_TAG, MANAGED_CREDENTIAL_TOKEN } from '@n8n/api-types';
+
+import { extractAgentCredentialIds } from '../extract-agent-credential-ids';
+
+describe('extractAgentCredentialIds', () => {
+ it('recursively extracts credential, credentialId, and credentials.*.id references', () => {
+ const result = extractAgentCredentialIds({
+ credential: 'main-model-credential',
+ memory: {
+ worker: { credential: 'memory-worker-credential' },
+ },
+ integrations: [{ credentialId: 'integration-credential' }],
+ tools: [
+ {
+ node: {
+ credentials: {
+ openAiApi: { id: 'node-tool-credential', name: 'OpenAI account' },
+ },
+ },
+ },
+ ],
+ unrelated: { id: 'not-a-credential-reference' },
+ });
+
+ expect(result).toEqual(
+ new Set([
+ 'main-model-credential',
+ 'memory-worker-credential',
+ 'integration-credential',
+ 'node-tool-credential',
+ ]),
+ );
+ });
+
+ it('deduplicates the same credential found through multiple reference forms', () => {
+ const result = extractAgentCredentialIds({
+ credential: 'shared-credential',
+ integrations: [{ credentialId: 'shared-credential' }],
+ tools: [
+ {
+ node: {
+ credentials: {
+ openAiApi: { id: 'shared-credential', name: 'OpenAI account' },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(result).toEqual(new Set(['shared-credential']));
+ });
+
+ it('ignores empty and managed credential references', () => {
+ const result = extractAgentCredentialIds({
+ credential: '',
+ memory: {
+ episodicMemory: { credential: MANAGED_CREDENTIAL_TOKEN },
+ worker: { credential: AI_GATEWAY_MANAGED_TAG },
+ },
+ integrations: [
+ { credentialId: '' },
+ { credentialId: MANAGED_CREDENTIAL_TOKEN },
+ { credentialId: 'real-credential' },
+ ],
+ tools: [
+ {
+ node: {
+ credentials: {
+ empty: { id: '', name: 'Empty' },
+ managed: { id: MANAGED_CREDENTIAL_TOKEN, name: 'Managed' },
+ gateway: { id: AI_GATEWAY_MANAGED_TAG, name: 'n8n Connect' },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(result).toEqual(new Set(['real-credential']));
+ });
+
+ it('lets callers extract draft and published snapshots separately and union them', () => {
+ const draftIds = extractAgentCredentialIds({
+ credential: 'draft-credential',
+ integrations: [{ credentialId: 'shared-credential' }],
+ });
+ const publishedIds = extractAgentCredentialIds({
+ credential: 'published-credential',
+ tools: [
+ {
+ node: {
+ credentials: {
+ openAiApi: { id: 'shared-credential', name: 'OpenAI account' },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(draftIds).toEqual(new Set(['draft-credential', 'shared-credential']));
+ expect(publishedIds).toEqual(new Set(['published-credential', 'shared-credential']));
+ expect(new Set([...draftIds, ...publishedIds])).toEqual(
+ new Set(['draft-credential', 'shared-credential', 'published-credential']),
+ );
+ });
+});
diff --git a/packages/cli/src/modules/agents/utils/extract-agent-credential-ids.ts b/packages/cli/src/modules/agents/utils/extract-agent-credential-ids.ts
new file mode 100644
index 00000000000..160d6f5cafe
--- /dev/null
+++ b/packages/cli/src/modules/agents/utils/extract-agent-credential-ids.ts
@@ -0,0 +1,42 @@
+import { AI_GATEWAY_MANAGED_TAG, MANAGED_CREDENTIAL_TOKEN } from '@n8n/api-types';
+
+const MANAGED_CREDENTIAL_IDS = new Set([MANAGED_CREDENTIAL_TOKEN, AI_GATEWAY_MANAGED_TAG]);
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function addCredentialId(value: unknown, credentialIds: Set): void {
+ if (typeof value === 'string' && value !== '' && !MANAGED_CREDENTIAL_IDS.has(value)) {
+ credentialIds.add(value);
+ }
+}
+
+function collectCredentialIds(value: unknown, credentialIds: Set): void {
+ if (Array.isArray(value)) {
+ for (const entry of value) collectCredentialIds(entry, credentialIds);
+ return;
+ }
+
+ if (!isRecord(value)) return;
+
+ for (const [key, entry] of Object.entries(value)) {
+ if (key === 'credential' || key === 'credentialId') {
+ addCredentialId(entry, credentialIds);
+ } else if (key === 'credentials' && isRecord(entry)) {
+ for (const credentialReference of Object.values(entry)) {
+ if (isRecord(credentialReference)) {
+ addCredentialId(credentialReference.id, credentialIds);
+ }
+ }
+ }
+
+ collectCredentialIds(entry, credentialIds);
+ }
+}
+
+export function extractAgentCredentialIds(value: unknown): Set {
+ const credentialIds = new Set();
+ collectCredentialIds(value, credentialIds);
+ return credentialIds;
+}
diff --git a/packages/cli/src/modules/mcp/__tests__/agent-tools.service.test.ts b/packages/cli/src/modules/mcp/__tests__/agent-tools.service.test.ts
index 650f5d60b9c..01b7df913f6 100644
--- a/packages/cli/src/modules/mcp/__tests__/agent-tools.service.test.ts
+++ b/packages/cli/src/modules/mcp/__tests__/agent-tools.service.test.ts
@@ -25,6 +25,7 @@ vi.mock('@/modules/agents/json-config/mcp-client-factory', () => ({
}));
import { CredentialsService } from '@/credentials/credentials.service';
+import type { EventService } from '@/events/event.service';
import { AgentConfigService } from '@/modules/agents/agent-config.service';
import { AgentCustomToolsService } from '@/modules/agents/agent-custom-tools.service';
import { AgentIntegrationPersistenceService } from '@/modules/agents/agent-integration-persistence.service';
@@ -203,6 +204,7 @@ describe('McpAgentToolsService', () => {
runtimeCacheService,
localCredentialsService,
workflowRepository,
+ mock(),
mock(),
modificationTelemetry,
);
diff --git a/packages/cli/src/modules/workflow-index/workflow-dependency-query.service.ts b/packages/cli/src/modules/workflow-index/workflow-dependency-query.service.ts
index cef7ff23b3a..6662c4eec84 100644
--- a/packages/cli/src/modules/workflow-index/workflow-dependency-query.service.ts
+++ b/packages/cli/src/modules/workflow-index/workflow-dependency-query.service.ts
@@ -4,6 +4,7 @@ import type {
DependencyResourceType,
ResolvedDependency,
} from '@n8n/api-types';
+import { ModuleRegistry } from '@n8n/backend-common';
import {
CredentialsRepository,
ProjectRelationRepository,
@@ -16,11 +17,14 @@ import { hasGlobalScope } from '@n8n/permissions';
import { In } from '@n8n/typeorm';
import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
+import { AgentCredentialDependencyRepository } from '@/modules/agents/repositories/agent-credential-dependency.repository';
+import { AgentRepository } from '@/modules/agents/repositories/agent.repository';
import { DataTableRepository } from '@/modules/data-table/data-table.repository';
import { RoleService } from '@/services/role.service';
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
interface RawDepMaps {
+ agentUsageMap: Map>;
credMap: Map>;
dtMap: Map>;
subMap: Map>;
@@ -28,6 +32,7 @@ interface RawDepMaps {
errorWfMap: Map>;
errorWfParentMap: Map>;
allCredIds: Set;
+ allAgentIds: Set;
allWfIds: Set;
allDtIds: Set;
}
@@ -43,6 +48,9 @@ export class WorkflowDependencyQueryService {
private readonly credentialsFinderService: CredentialsFinderService,
private readonly projectRelationRepository: ProjectRelationRepository,
private readonly roleService: RoleService,
+ private readonly agentDependencyRepository: AgentCredentialDependencyRepository,
+ private readonly agentRepository: AgentRepository,
+ private readonly moduleRegistry: ModuleRegistry,
) {}
async getDependencyCounts(
@@ -58,6 +66,7 @@ export class WorkflowDependencyQueryService {
const result: DependencyCountsBatchResponse = {};
for (const id of accessibleInputIds) {
result[id] = {
+ agentUsage: maps.agentUsageMap.get(id)?.size ?? 0,
credentialId: maps.credMap.get(id)?.size ?? 0,
dataTableId: maps.dtMap.get(id)?.size ?? 0,
errorWorkflow: maps.errorWfMap.get(id)?.size ?? 0,
@@ -81,16 +90,18 @@ export class WorkflowDependencyQueryService {
const { accessibleInputIds, maps } = loaded;
// Check user access for each dependency type
- const [accessibleWfIds, accessibleCredIds, accessibleDtIds] = await Promise.all([
- this.filterByAccess([...maps.allWfIds], 'workflow', user),
- this.filterByAccess([...maps.allCredIds], 'credential', user),
- this.filterByAccess([...maps.allDtIds], 'dataTable', user),
- ]);
+ const [accessibleWfIds, accessibleCredIds, accessibleDtIds, accessibleAgentProjectIds] =
+ await Promise.all([
+ this.filterByAccess([...maps.allWfIds], 'workflow', user),
+ this.filterByAccess([...maps.allCredIds], 'credential', user),
+ this.filterByAccess([...maps.allDtIds], 'dataTable', user),
+ maps.allAgentIds.size > 0 ? this.getAccessibleAgentProjectIds(user) : new Set(),
+ ]);
// Load all referenced resources (not just accessible ones) so that ids whose
// resource has been deleted — the index may still reference them — can be
// dropped instead of being reported as inaccessible.
- const [credentials, workflows, dataTables] = await Promise.all([
+ const [credentials, workflows, dataTables, agents] = await Promise.all([
maps.allCredIds.size > 0
? this.credentialsRepository.find({
where: { id: In([...maps.allCredIds]), usageScope: 'project' },
@@ -109,19 +120,34 @@ export class WorkflowDependencyQueryService {
select: ['id', 'name', 'projectId'],
})
: [],
+ maps.allAgentIds.size > 0
+ ? this.agentRepository.findSummariesByIds([...maps.allAgentIds])
+ : [],
]);
const accessibleWfIdSet = new Set(accessibleWfIds);
const accessibleCredIdSet = new Set(accessibleCredIds);
const accessibleDtIdSet = new Set(accessibleDtIds);
+ const agentNames = new Map();
const wfNames = new Map();
const credNames = new Map();
const dtNames = new Map();
+ const existingAgentIds = new Set();
const existingWfIds = new Set();
const existingCredIds = new Set();
const existingDtIds = new Set();
+ for (const agent of agents) {
+ existingAgentIds.add(agent.id);
+ // `null` means global `agent:read`; otherwise access is limited to the listed projects.
+ if (accessibleAgentProjectIds === null || accessibleAgentProjectIds.has(agent.projectId)) {
+ agentNames.set(agent.id, {
+ name: agent.name ?? agent.id,
+ projectId: agent.projectId,
+ });
+ }
+ }
for (const c of credentials) {
existingCredIds.add(c.id);
if (accessibleCredIdSet.has(c.id)) credNames.set(c.id, c.name ?? c.id);
@@ -140,11 +166,12 @@ export class WorkflowDependencyQueryService {
accessibleInputIds,
maps,
{
+ agentNames,
wfNames,
credNames,
dtNames,
},
- { existingWfIds, existingCredIds, existingDtIds },
+ { existingAgentIds, existingWfIds, existingCredIds, existingDtIds },
);
}
@@ -156,25 +183,32 @@ export class WorkflowDependencyQueryService {
const accessibleInputIds = await this.filterByAccess(resourceIds, resourceType, user);
if (accessibleInputIds.length === 0) return null;
- const rawDeps = await this.dependencyRepository.find({
- where: [
- {
- workflowId: In(accessibleInputIds),
- dependencyType: In(['credentialId', 'dataTableId', 'errorWorkflow', 'workflowCall']),
- },
- { dependencyKey: In(accessibleInputIds) },
- ],
- select: ['workflowId', 'dependencyType', 'dependencyKey'],
- });
+ const [rawDeps, agentDeps] = await Promise.all([
+ this.dependencyRepository.find({
+ where: [
+ {
+ workflowId: In(accessibleInputIds),
+ dependencyType: In(['credentialId', 'dataTableId', 'errorWorkflow', 'workflowCall']),
+ },
+ { dependencyKey: In(accessibleInputIds) },
+ ],
+ select: ['workflowId', 'dependencyType', 'dependencyKey'],
+ }),
+ resourceType === 'credential' && this.moduleRegistry.isActive('agents')
+ ? this.agentDependencyRepository.findByCredentialIds(accessibleInputIds)
+ : [],
+ ]);
- if (rawDeps.length === 0) return null;
+ if (rawDeps.length === 0 && agentDeps.length === 0) return null;
- return { accessibleInputIds, maps: this.buildDepMaps(rawDeps) };
+ return { accessibleInputIds, maps: this.buildDepMaps(rawDeps, agentDeps) };
}
private buildDepMaps(
rawDeps: Array<{ workflowId: string; dependencyType: string; dependencyKey: string }>,
+ agentDeps: Array<{ agentId: string; credentialId: string }>,
): RawDepMaps {
+ const agentUsageMap = new Map>();
const credMap = new Map>();
const dtMap = new Map>();
const subMap = new Map>();
@@ -182,6 +216,7 @@ export class WorkflowDependencyQueryService {
const errorWfMap = new Map>();
const errorWfParentMap = new Map>();
const allCredIds = new Set();
+ const allAgentIds = new Set();
const allWfIds = new Set();
const allDtIds = new Set();
@@ -211,7 +246,13 @@ export class WorkflowDependencyQueryService {
}
}
+ for (const dep of agentDeps) {
+ addToSet(agentUsageMap, dep.credentialId, dep.agentId);
+ allAgentIds.add(dep.agentId);
+ }
+
return {
+ agentUsageMap,
credMap,
dtMap,
subMap,
@@ -219,6 +260,7 @@ export class WorkflowDependencyQueryService {
errorWfMap,
errorWfParentMap,
allCredIds,
+ allAgentIds,
allWfIds,
allDtIds,
};
@@ -232,11 +274,13 @@ export class WorkflowDependencyQueryService {
resourceIds: string[],
maps: RawDepMaps,
accessMaps: {
+ agentNames: Map;
wfNames: Map;
credNames: Map;
dtNames: Map;
},
existing: {
+ existingAgentIds: Set;
existingWfIds: Set;
existingCredIds: Set;
existingDtIds: Set;
@@ -248,6 +292,21 @@ export class WorkflowDependencyQueryService {
const dependencies: ResolvedDependency[] = [];
let inaccessibleCount = 0;
+ for (const id of maps.agentUsageMap.get(resourceId) ?? []) {
+ if (!existing.existingAgentIds.has(id)) continue;
+ const agent = accessMaps.agentNames.get(id);
+ if (agent) {
+ dependencies.push({
+ id,
+ name: agent.name,
+ type: 'agentUsage',
+ projectId: agent.projectId,
+ });
+ } else {
+ inaccessibleCount++;
+ }
+ }
+
const resolve = (
ids: Set | undefined,
nameMap: Map,
@@ -360,6 +419,15 @@ export class WorkflowDependencyQueryService {
return dataTables.filter((dt) => accessibleProjectIds.has(dt.projectId)).map((dt) => dt.id);
}
+
+ private async getAccessibleAgentProjectIds(user: User): Promise | null> {
+ if (hasGlobalScope(user, 'agent:read')) return null;
+
+ const roles = await this.roleService.rolesWithScope('project', ['agent:read']);
+ return new Set(
+ await this.projectRelationRepository.getAccessibleProjectsByRoles(user.id, roles),
+ );
+ }
}
function addToSet(map: Map>, key: string, val: string) {
diff --git a/packages/cli/test/integration/workflows/workflow-dependency.controller.test.ts b/packages/cli/test/integration/workflows/workflow-dependency.controller.test.ts
index 6d8ffadd4cd..8eae6f6bb16 100644
--- a/packages/cli/test/integration/workflows/workflow-dependency.controller.test.ts
+++ b/packages/cli/test/integration/workflows/workflow-dependency.controller.test.ts
@@ -1,17 +1,37 @@
+import { randomUUID } from 'node:crypto';
+
import {
createWorkflow,
randomCredentialPayload,
shareWorkflowWithUsers,
} from '@n8n/backend-test-utils';
-import { WorkflowDependencyRepository } from '@n8n/db';
+import { ModuleRegistry } from '@n8n/backend-common';
+import { ProjectRepository, WorkflowDependencyRepository, type User } from '@n8n/db';
import { Container } from '@n8n/di';
+import { AgentCredentialDependency } from '@/modules/agents/entities/agent-credential-dependency.entity';
+import { AgentHistory } from '@/modules/agents/entities/agent-history.entity';
+import { Agent } from '@/modules/agents/entities/agent.entity';
+import { AgentCredentialDependencyRepository } from '@/modules/agents/repositories/agent-credential-dependency.repository';
+import { AgentHistoryRepository } from '@/modules/agents/repositories/agent-history.repository';
+import { AgentRepository } from '@/modules/agents/repositories/agent.repository';
+
import { saveCredential } from '../shared/db/credentials';
import { createMember, createOwner } from '../shared/db/users';
import * as utils from '../shared/utils';
let testServer: ReturnType;
let depRepo: WorkflowDependencyRepository;
+let agentDepRepo: AgentCredentialDependencyRepository;
+let agentHistoryRepo: AgentHistoryRepository;
+let agentRepo: AgentRepository;
+let projectRepo: ProjectRepository;
+
+beforeAll(() => {
+ const moduleRegistry = Container.get(ModuleRegistry);
+ moduleRegistry.entities.push(Agent, AgentHistory, AgentCredentialDependency);
+ moduleRegistry.getActiveModules().push('agents');
+});
testServer = utils.setupTestServer({
endpointGroups: ['workflowDependencies'],
@@ -20,6 +40,10 @@ testServer = utils.setupTestServer({
beforeAll(() => {
depRepo = Container.get(WorkflowDependencyRepository);
+ agentDepRepo = Container.get(AgentCredentialDependencyRepository);
+ agentHistoryRepo = Container.get(AgentHistoryRepository);
+ agentRepo = Container.get(AgentRepository);
+ projectRepo = Container.get(ProjectRepository);
});
/** Seed a workflow_dependency row (draft). */
@@ -37,6 +61,46 @@ async function seedDep(workflowId: string, dependencyType: string, dependencyKey
);
}
+async function seedAgentCredentialDependencies(
+ user: User,
+ credentialId: string,
+ name = 'Support Agent',
+) {
+ const project = await projectRepo.getPersonalProjectForUserOrFail(user.id);
+ const agent = agentRepo.create({
+ name,
+ projectId: project.id,
+ schema: {
+ name,
+ model: 'openai/gpt-4.1-mini',
+ instructions: 'Help the user',
+ credential: credentialId,
+ tools: [],
+ skills: [],
+ },
+ integrations: [],
+ tools: {},
+ skills: {},
+ versionId: 'draft-version-1',
+ });
+ await agentRepo.save(agent);
+
+ const publishedVersionId = randomUUID();
+ await agentHistoryRepo.saveVersion({
+ versionId: publishedVersionId,
+ agentId: agent.id,
+ schema: agent.schema,
+ tools: agent.tools,
+ skills: agent.skills,
+ publishedBy: user,
+ });
+ agent.activeVersionId = publishedVersionId;
+ await agentRepo.save(agent);
+ await agentDepRepo.refreshForAgent(agent.id);
+
+ return agent;
+}
+
describe('POST /workflow-dependencies/counts', () => {
it('should return counts only for workflows the user owns', async () => {
const owner = await createOwner();
@@ -97,6 +161,23 @@ describe('POST /workflow-dependencies/counts', () => {
expect(resp.body.data[memberCred.id].workflowParent).toBe(1);
});
+ it('should count an agent once when draft and published versions use the credential', async () => {
+ const owner = await createOwner();
+ const credential = await saveCredential(randomCredentialPayload(), {
+ user: owner,
+ role: 'credential:owner',
+ });
+ await seedAgentCredentialDependencies(owner, credential.id);
+
+ const resp = await testServer
+ .authAgentFor(owner)
+ .post('/workflow-dependencies/counts')
+ .send({ resourceIds: [credential.id], resourceType: 'credential' });
+
+ expect(resp.statusCode).toBe(200);
+ expect(resp.body.data[credential.id].agentUsage).toBe(1);
+ });
+
it('should include counts for dependencies the user cannot access', async () => {
const owner = await createOwner();
const member = await createMember();
@@ -301,6 +382,54 @@ describe('POST /workflow-dependencies/details', () => {
});
});
+ it('should resolve an accessible agent using a credential', async () => {
+ const owner = await createOwner();
+ const credential = await saveCredential(randomCredentialPayload(), {
+ user: owner,
+ role: 'credential:owner',
+ });
+ const agent = await seedAgentCredentialDependencies(owner, credential.id, 'Support Agent');
+
+ const resp = await testServer
+ .authAgentFor(owner)
+ .post('/workflow-dependencies/details')
+ .send({ resourceIds: [credential.id], resourceType: 'credential' });
+
+ expect(resp.statusCode).toBe(200);
+ expect(resp.body.data[credential.id]).toEqual({
+ dependencies: [
+ {
+ id: agent.id,
+ name: 'Support Agent',
+ type: 'agentUsage',
+ projectId: agent.projectId,
+ },
+ ],
+ inaccessibleCount: 0,
+ });
+ });
+
+ it('should report an inaccessible agent without exposing its details', async () => {
+ const owner = await createOwner();
+ const member = await createMember();
+ const credential = await saveCredential(randomCredentialPayload(), {
+ user: member,
+ role: 'credential:owner',
+ });
+ await seedAgentCredentialDependencies(owner, credential.id, 'Private Agent');
+
+ const resp = await testServer
+ .authAgentFor(member)
+ .post('/workflow-dependencies/details')
+ .send({ resourceIds: [credential.id], resourceType: 'credential' });
+
+ expect(resp.statusCode).toBe(200);
+ expect(resp.body.data[credential.id]).toEqual({
+ dependencies: [],
+ inaccessibleCount: 1,
+ });
+ });
+
it('should exclude inaccessible deps and report inaccessibleCount', async () => {
const owner = await createOwner();
const member = await createMember();
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index c032281d6bc..64b859aaca1 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -4799,6 +4799,7 @@
"workflows.dependencies.type.errorWorkflowParent": "Error handler for",
"workflows.dependencies.type.subWorkflows": "Sub-workflows",
"workflows.dependencies.type.parentWorkflows": "Used by workflows",
+ "workflows.dependencies.type.agents": "Used by agents",
"workflows.dependencies.search.placeholder": "Search...",
"workflows.dependencies.search.empty": "No matching dependencies",
"workflows.dependencies.hiddenNotice": "+1 not accessible to you | +{count} not accessible to you",
diff --git a/packages/frontend/editor-ui/src/app/components/DependencyPill.test.ts b/packages/frontend/editor-ui/src/app/components/DependencyPill.test.ts
index 59fe1b71a7f..eb4402a9df5 100644
--- a/packages/frontend/editor-ui/src/app/components/DependencyPill.test.ts
+++ b/packages/frontend/editor-ui/src/app/components/DependencyPill.test.ts
@@ -5,6 +5,8 @@ import { useUIStore } from '@/app/stores/ui.store';
import { mockedStore } from '@/__tests__/utils';
import { VIEWS } from '@/app/constants';
import { DATA_TABLE_DETAILS } from '@/features/core/dataTable/constants';
+import { AGENT_BUILDER_VIEW } from '@/features/agents/constants';
+import { useI18n } from '@n8n/i18n';
import * as vueRouter from 'vue-router';
import type { MockInstance } from 'vitest';
@@ -156,6 +158,26 @@ describe('DependencyPill', () => {
expect(items[7].id).toBe('workflowParent:wf-2');
});
+ it('should group agent usages under the agents i18n label', () => {
+ const baseTextSpy = vi.spyOn(useI18n(), 'baseText');
+ mockDepsResult = {
+ dependencies: [
+ { type: 'agentUsage', id: 'agent-1', name: 'Support Agent', projectId: 'proj-1' },
+ ],
+ inaccessibleCount: 0,
+ };
+ renderComponent({ props: defaultProps });
+
+ const items = capturedItems as Array<{ id: string; label: string; disabled?: boolean }>;
+
+ expect(items).toHaveLength(2);
+ expect(items[0]).toMatchObject({ id: 'header-agentUsage', disabled: true });
+ expect(baseTextSpy).toHaveBeenCalledWith('workflows.dependencies.type.agents');
+ expect(items[1].id).toBe('agentUsage:agent-1');
+
+ baseTextSpy.mockRestore();
+ });
+
it('should open credential on select', () => {
mockDepsResult = {
dependencies: [{ type: 'credentialId', id: 'cred-1', name: 'My Key' }],
@@ -229,6 +251,24 @@ describe('DependencyPill', () => {
expect(windowOpenSpy).not.toHaveBeenCalled();
});
+ it('should open an agent using the credential in a new tab on select', () => {
+ mockDepsResult = {
+ dependencies: [
+ { type: 'agentUsage', id: 'agent-1', name: 'Support Agent', projectId: 'proj-1' },
+ ],
+ inaccessibleCount: 0,
+ };
+ renderComponent({ props: defaultProps });
+
+ capturedSelectHandler?.('agentUsage:agent-1');
+
+ expect(router.resolve).toHaveBeenCalledWith({
+ name: AGENT_BUILDER_VIEW,
+ params: { projectId: 'proj-1', agentId: 'agent-1' },
+ });
+ expect(windowOpenSpy).toHaveBeenCalledWith('/mock-href', '_blank');
+ });
+
it('should ignore select with invalid value', () => {
mockDepsResult = createDepsResult();
renderComponent({ props: defaultProps });
diff --git a/packages/frontend/editor-ui/src/app/components/DependencyPill.vue b/packages/frontend/editor-ui/src/app/components/DependencyPill.vue
index 1067a4b29e7..de3fefb2330 100644
--- a/packages/frontend/editor-ui/src/app/components/DependencyPill.vue
+++ b/packages/frontend/editor-ui/src/app/components/DependencyPill.vue
@@ -11,6 +11,7 @@ import { useUIStore } from '@/app/stores/ui.store';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import type { DependencyType, ResolvedDependency } from '@n8n/api-types';
import { useDependencies } from '@/app/composables/useDependencies';
+import { AGENT_BUILDER_VIEW } from '@/features/agents/constants';
import { DATA_TABLE_DETAILS } from '@/features/core/dataTable/constants';
const MIN_ITEMS_FOR_SEARCH = 6;
@@ -63,6 +64,10 @@ const typeConfig: Record {
const groups: Record = {
credentialId: [],
dataTableId: [],
+ agentUsage: [],
errorWorkflow: [],
errorWorkflowParent: [],
workflowCall: [],
@@ -171,6 +178,15 @@ function onSelect(value: string) {
window.open(href, '_blank');
}
break;
+ case 'agentUsage':
+ if (dep.projectId) {
+ const href = router.resolve({
+ name: AGENT_BUILDER_VIEW,
+ params: { projectId: dep.projectId, agentId: dep.id },
+ }).href;
+ window.open(href, '_blank');
+ }
+ break;
}
}