feat(core): Add the scope column to the agents table (no-changelog) (#41165)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jaakko Husso 2026-10-16 09:52:52 +00:00 committed by GitHub
parent aadbb7c498
commit aba5258930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 512 additions and 12 deletions

View File

@ -36,7 +36,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.agent_thread_grants](public.agent_thread_grants.md) | 4 | | BASE TABLE |
| [public.agent_version_skill_dependency](public.agent_version_skill_dependency.md) | 4 | | BASE TABLE |
| [public.agent_workflow_dependency](public.agent_workflow_dependency.md) | 3 | | BASE TABLE |
| [public.agents](public.agents.md) | 14 | | BASE TABLE |
| [public.agents](public.agents.md) | 15 | | BASE TABLE |
| [public.agents_memory_entries](public.agents_memory_entries.md) | 13 | | BASE TABLE |
| [public.agents_memory_entry_candidates](public.agents_memory_entry_candidates.md) | 14 | | BASE TABLE |
| [public.agents_memory_entry_locks](public.agents_memory_entry_locks.md) | 6 | | BASE TABLE |
@ -240,7 +240,7 @@ erDiagram
"public.agent_version_skill_dependency" }o--|| "public.skill_version" : "FOREIGN KEY (#quot;skillVersionId#quot;) REFERENCES skill_version(id)"
"public.agent_workflow_dependency" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.agent_workflow_dependency" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agents" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" }o--o| "public.agent_history" : "FOREIGN KEY (#quot;activeVersionId#quot;) REFERENCES agent_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.agents_memory_entries" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agents_memory_entries" }o--|| "public.agents_resources" : "FOREIGN KEY (#quot;resourceId#quot;) REFERENCES agents_resources(id) ON DELETE CASCADE"
@ -758,6 +758,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -95,6 +95,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -70,6 +70,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -75,6 +75,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -56,6 +56,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -58,6 +58,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -49,6 +49,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -69,6 +69,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -102,6 +102,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -70,6 +70,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -67,6 +67,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -53,6 +53,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -61,6 +61,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -55,6 +55,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -49,6 +49,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -10,9 +10,10 @@
| id | varchar(36) | | false | [public.agent_background_job](public.agent_background_job.md) [public.agent_channel_status](public.agent_channel_status.md) [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_skill_dependency](public.agent_skill_dependency.md) [public.agent_task_definition](public.agent_task_definition.md) [public.agent_task_run_lock](public.agent_task_run_lock.md) [public.agent_workflow_dependency](public.agent_workflow_dependency.md) [public.agents_memory_entries](public.agents_memory_entries.md) [public.agents_memory_entry_candidates](public.agents_memory_entry_candidates.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) [public.skill](public.skill.md) | | |
| integrations | json | '[]'::json | false | | | |
| name | varchar(128) | | false | | | |
| projectId | varchar(255) | | false | | [public.project](public.project.md) | |
| projectId | varchar(255) | | true | | [public.project](public.project.md) | |
| revision | integer | 0 | false | | | |
| schema | json | | true | | | |
| scope | varchar(16) | 'project'::character varying | false | | | project: belongs to projectId; instance: code-defined, belongs to no project (projectId is null) |
| setupCompletedAt | timestamp(3) with time zone | | true | | | When this agent first reached a complete, publishable setup |
| skills | json | '{}'::json | false | | | |
| tools | json | '{}'::json | false | | | |
@ -23,6 +24,8 @@
| Name | Type | Definition |
| ---- | ---- | ---------- |
| CHK_agents_scope | CHECK | CHECK (((scope)::text = ANY ((ARRAY['project'::character varying, 'instance'::character varying])::text[]))) |
| CHK_agents_scope_project | CHECK | CHECK ((((scope)::text = 'instance'::text) = ("projectId" IS NULL))) |
| FK_940597dfe9753375309ce6aeea0 | FOREIGN KEY | FOREIGN KEY ("activeVersionId") REFERENCES agent_history("versionId") ON DELETE SET NULL |
| FK_a30d560207c4071d98aa03c179c | FOREIGN KEY | FOREIGN KEY ("projectId") REFERENCES project(id) ON DELETE CASCADE |
| PK_9c653f28ae19c5884d5baf6a1d9 | PRIMARY KEY | PRIMARY KEY (id) |
@ -31,8 +34,8 @@
| agents_id_not_null | n | NOT NULL id |
| agents_integrations_not_null | n | NOT NULL integrations |
| agents_name_not_null | n | NOT NULL name |
| agents_projectId_not_null | n | NOT NULL "projectId" |
| agents_revision_not_null | n | NOT NULL revision |
| agents_scope_not_null | n | NOT NULL scope |
| agents_skills_not_null | n | NOT NULL skills |
| agents_tools_not_null | n | NOT NULL tools |
| agents_updatedAt_not_null | n | NOT NULL "updatedAt" |
@ -73,7 +76,7 @@ erDiagram
"public.agents_observation_locks" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agents_observations" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.skill" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE"
"public.agents" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" {
varchar_36_ activeVersionId FK
@ -85,6 +88,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -82,6 +82,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -91,6 +91,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -58,6 +58,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -80,6 +80,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -58,6 +58,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -62,6 +62,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -81,6 +81,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -42,7 +42,7 @@ erDiagram
"public.activity_event" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agent_chat_attachments" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agent_execution_threads" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.agents" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.ai_preference" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.data_table" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.folder" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
@ -148,6 +148,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -65,6 +65,7 @@ erDiagram
varchar_255_ projectId FK
integer revision
json schema
varchar_16_ scope
timestamp_3__with_time_zone setupCompletedAt
json skills
json tools

View File

@ -36,7 +36,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [agent_thread_grants](agent_thread_grants.md) | 4 | | table |
| [agent_version_skill_dependency](agent_version_skill_dependency.md) | 4 | | table |
| [agent_workflow_dependency](agent_workflow_dependency.md) | 3 | | table |
| [agents](agents.md) | 14 | | table |
| [agents](agents.md) | 15 | | table |
| [agents_memory_entries](agents_memory_entries.md) | 13 | | table |
| [agents_memory_entry_candidates](agents_memory_entry_candidates.md) | 14 | | table |
| [agents_memory_entry_locks](agents_memory_entry_locks.md) | 6 | | table |
@ -222,7 +222,7 @@ erDiagram
"agent_version_skill_dependency" |o--|| "agent_history" : "FOREIGN KEY (agentVersionId) REFERENCES agent_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_workflow_dependency" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_workflow_dependency" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--o| "agent_history" : "FOREIGN KEY (activeVersionId) REFERENCES agent_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"agents_memory_entries" }o--o| "agents_memory_entries" : "FOREIGN KEY (supersededBy) REFERENCES agents_memory_entries (id) ON UPDATE NO ACTION ON DELETE NO ACTION MATCH NONE"
"agents_memory_entries" }o--|| "agents_resources" : "FOREIGN KEY (resourceId) REFERENCES agents_resources (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -744,6 +744,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -97,6 +97,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -77,6 +77,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -77,6 +77,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -65,6 +65,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -66,6 +66,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -59,6 +59,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -74,6 +74,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -102,6 +102,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -73,6 +73,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -74,6 +74,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -63,6 +63,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -66,6 +66,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -62,6 +62,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -59,6 +59,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -6,7 +6,7 @@
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "agents" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128) NOT NULL, "projectId" varchar(255) NOT NULL, "integrations" text NOT NULL DEFAULT ('[]'), "schema" text, "tools" text NOT NULL DEFAULT ('{}'), "skills" text NOT NULL DEFAULT ('{}'), "versionId" varchar(36), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "activeVersionId" varchar(36), "availableInMCP" boolean NOT NULL DEFAULT (false), "setupCompletedAt" datetime(3), "revision" integer NOT NULL DEFAULT 0, CONSTRAINT "FK_940597dfe9753375309ce6aeea0" FOREIGN KEY ("activeVersionId") REFERENCES "agent_history" ("versionId") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_a30d560207c4071d98aa03c179c" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)
CREATE TABLE "agents" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128) NOT NULL, "projectId" varchar(255), "integrations" text NOT NULL DEFAULT ('[]'), "schema" text, "tools" text NOT NULL DEFAULT ('{}'), "skills" text NOT NULL DEFAULT ('{}'), "versionId" varchar(36), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "activeVersionId" varchar(36), "availableInMCP" boolean NOT NULL DEFAULT (false), "setupCompletedAt" datetime(3), "revision" integer NOT NULL DEFAULT (0), "scope" varchar(16) NOT NULL DEFAULT ('project'), CONSTRAINT "CHK_agents_scope" CHECK (("scope" IN ('project', 'instance'))), CONSTRAINT "CHK_agents_scope_project" CHECK (("scope" = 'instance') = ("projectId" IS NULL)), CONSTRAINT "FK_940597dfe9753375309ce6aeea0" FOREIGN KEY ("activeVersionId") REFERENCES "agent_history" ("versionId") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_a30d560207c4071d98aa03c179c" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)
```
</details>
@ -21,9 +21,10 @@ CREATE TABLE "agents" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128
| id | varchar(36) | | false | [agent_background_job](agent_background_job.md) [agent_channel_status](agent_channel_status.md) [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_skill_dependency](agent_skill_dependency.md) [agent_task_definition](agent_task_definition.md) [agent_task_run_lock](agent_task_run_lock.md) [agent_workflow_dependency](agent_workflow_dependency.md) [agents_memory_entries](agents_memory_entries.md) [agents_memory_entry_candidates](agents_memory_entry_candidates.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) [skill](skill.md) | | |
| integrations | TEXT | '[]' | false | | | |
| name | varchar(128) | | false | | | |
| projectId | varchar(255) | | false | | [project](project.md) | |
| projectId | varchar(255) | | true | | [project](project.md) | |
| revision | INTEGER | 0 | false | | | |
| schema | TEXT | | true | | | |
| scope | varchar(16) | 'project' | false | | | |
| setupCompletedAt | datetime(3) | | true | | | |
| skills | TEXT | '{}' | false | | | |
| tools | TEXT | '{}' | false | | | |
@ -34,6 +35,8 @@ CREATE TABLE "agents" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - | CHECK | CHECK (("scope" IN ('project', 'instance'))) |
| - | CHECK | CHECK (("scope" = 'instance') = ("projectId" IS NULL)) |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (activeVersionId) REFERENCES agent_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| id | PRIMARY KEY | PRIMARY KEY (id) |
@ -75,7 +78,7 @@ erDiagram
"agents_observation_locks" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents_observations" }o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"skill" }o--o| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" {
varchar_36_ activeVersionId FK
@ -87,6 +90,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -85,6 +85,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -90,6 +90,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -65,6 +65,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -84,6 +84,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -65,6 +65,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -69,6 +69,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -84,6 +84,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -48,7 +48,7 @@ erDiagram
"activity_event" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_chat_attachments" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agent_execution_threads" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"agents" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"ai_preference" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"data_table" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"folder" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -154,6 +154,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -73,6 +73,7 @@ erDiagram
varchar_255_ projectId FK
INTEGER revision
TEXT schema
varchar_16_ scope
datetime_3_ setupCompletedAt
TEXT skills
TEXT tools

View File

@ -0,0 +1,74 @@
import { TableCheck } from '@n8n/typeorm';
import type { MigrationContext, ReversibleMigration } from '../migration-types';
const table = 'agents';
const scopeCheck = 'agents_scope_project';
/**
* Adds instance-scoped agents. An instance agent belongs to no project, so
* its `projectId` is null. Its runtime comes from code, and each of its
* threads carries its own working project. A CHECK constraint makes sure that
* `projectId` is null exactly for instance agents.
*/
export class AddInstanceScopeToAgents1792051765225 implements ReversibleMigration {
async up(context: MigrationContext) {
const {
schemaBuilder: { addColumns, column, dropNotNull },
} = context;
await addColumns(
table,
[
column('scope')
.varchar(16)
.notNull.default("'project'")
.withEnumCheck(['project', 'instance'])
.comment(
'project: belongs to projectId; instance: code-defined, belongs to no project (projectId is null)',
),
],
{ recreatesOnSqlite: true },
);
await dropNotNull(table, 'projectId', { recreatesOnSqlite: true });
await this.addScopeCheck(context);
}
async down(context: MigrationContext) {
const {
schemaBuilder: { addNotNull, dropColumns, dropEnumCheck },
} = context;
// The old schema cannot store an agent without a project.
// `DeleteInstanceAgentsOnRevert` already deleted these agents and their
// dependent rows with foreign keys on. This delete is a fallback.
await this.deleteInstanceAgents(context);
await this.dropScopeCheck(context);
await addNotNull(table, 'projectId', { recreatesOnSqlite: true });
await dropEnumCheck(table, 'scope', { recreatesOnSqlite: true });
await dropColumns(table, ['scope'], { recreatesOnSqlite: true });
}
private async addScopeCheck({ escape, queryRunner, tablePrefix }: MigrationContext) {
const scope = escape.columnName('scope');
const projectId = escape.columnName('projectId');
await queryRunner.createCheckConstraint(
`${tablePrefix}${table}`,
new TableCheck({
name: `CHK_${tablePrefix}${scopeCheck}`,
expression: `(${scope} = 'instance') = (${projectId} IS NULL)`,
}),
);
}
private async dropScopeCheck({ queryRunner, tablePrefix }: MigrationContext) {
await queryRunner.dropCheckConstraint(
`${tablePrefix}${table}`,
`CHK_${tablePrefix}${scopeCheck}`,
);
}
private async deleteInstanceAgents({ escape, runQuery }: MigrationContext) {
await runQuery(
`DELETE FROM ${escape.tableName(table)} WHERE ${escape.columnName('projectId')} IS NULL`,
);
}
}

View File

@ -0,0 +1,19 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
/**
* Runs before `AddInstanceScopeToAgents` on revert. That migration recreates
* `agents` with foreign keys off on SQLite, so deleting instance agents there
* does not cascade. This migration keeps foreign keys on and deletes them
* first, so the cascades also delete the rows that depend on them.
*/
export class DeleteInstanceAgentsOnRevert1792139680416 implements ReversibleMigration {
async up() {
// Nothing to change on upgrade. This migration only does work on revert.
}
async down({ escape, runQuery }: MigrationContext) {
await runQuery(
`DELETE FROM ${escape.tableName('agents')} WHERE ${escape.columnName('scope')} = 'instance'`,
);
}
}

View File

@ -0,0 +1,11 @@
import { AddInstanceScopeToAgents1792051765225 as BaseMigration } from '../common/1792051765225-AddInstanceScopeToAgents';
/**
* `agents` has incoming cascading foreign keys. Recreating it must not delete
* their rows, so foreign keys are off for this migration. On revert,
* `DeleteInstanceAgentsOnRevert` deletes instance agents and their dependent
* rows first, with foreign keys on.
*/
export class AddInstanceScopeToAgents1792051765225 extends BaseMigration {
withFKsDisabled = true as const;
}

View File

@ -0,0 +1,202 @@
import {
createTestMigrationContext,
initDbUpToMigration,
runSingleMigration,
undoLastSingleMigration,
type TestMigrationContext,
} from '@n8n/backend-test-utils';
import { DbConnection } from '@n8n/db';
import { Container } from '@n8n/di';
import { DataSource } from '@n8n/typeorm';
import { randomUUID } from 'node:crypto';
const migrationName = 'AddInstanceScopeToAgents1792051765225';
describe('AddInstanceScopeToAgents migration', () => {
let dataSource: DataSource;
beforeAll(async () => {
await Container.get(DbConnection).init();
dataSource = Container.get(DataSource);
});
beforeEach(async () => {
await withContext(async ({ queryRunner }) => await queryRunner.clearDatabase());
await initDbUpToMigration(migrationName);
});
afterAll(async () => {
await Container.get(DbConnection).close();
});
async function withContext<T>(run: (context: TestMigrationContext) => Promise<T>): Promise<T> {
const context = createTestMigrationContext(dataSource);
try {
return await run(context);
} finally {
await context.queryRunner.release();
}
}
function columns(context: TestMigrationContext, names: string[]) {
return names.map((name) => context.escape.columnName(name)).join(', ');
}
async function insertProject(context: TestMigrationContext) {
const id = randomUUID();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('project')} (${columns(context, ['id', 'name', 'type', 'createdAt', 'updatedAt'])})
VALUES (:id, 'Agents project', 'team', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ id },
);
return id;
}
async function insertAgent(
context: TestMigrationContext,
projectId: string | null,
scope?: string,
) {
const id = randomUUID();
const names = ['id', 'name', 'projectId', 'createdAt', 'updatedAt'];
const values = [':id', "'Agent'", ':projectId', 'CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP'];
if (scope !== undefined) {
names.push('scope');
values.push(':scope');
}
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agents')} (${columns(context, names)}) VALUES (${values.join(', ')})`,
{ id, projectId, scope },
);
return id;
}
/** A thread with one execution: rows that depend on the agent at two levels. */
async function insertThreadWithExecution(
context: TestMigrationContext,
agentId: string,
projectId: string,
) {
const threadId = randomUUID();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agent_execution_threads')}
(${columns(context, ['id', 'agentId', 'agentName', 'projectId', 'createdAt', 'updatedAt'])})
VALUES (:threadId, :agentId, 'Agent', :projectId, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ threadId, agentId, projectId },
);
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agent_execution')}
(${columns(context, ['id', 'threadId', 'status', 'createdAt', 'updatedAt'])})
VALUES (:id, :threadId, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ id: randomUUID(), threadId },
);
return threadId;
}
async function readIds(context: TestMigrationContext, table: string) {
const rows = await context.runQuery<Array<{ id: string }>>(
`SELECT ${context.escape.columnName('id')} AS id FROM ${context.escape.tableName(table)}`,
);
return rows.map((row) => row.id).sort();
}
async function readAgent(context: TestMigrationContext, id: string) {
const [row] = await context.runQuery<Array<{ scope: string; projectId: string | null }>>(
`SELECT ${columns(context, ['scope', 'projectId'])} FROM ${context.escape.tableName('agents')} WHERE ${context.escape.columnName('id')} = :id`,
{ id },
);
return row;
}
it('marks existing agents as project agents and keeps their dependent rows', async () => {
const { agentId, threadId } = await withContext(async (context) => {
const projectId = await insertProject(context);
const agentId = await insertAgent(context, projectId);
const threadId = await insertThreadWithExecution(context, agentId, projectId);
return { agentId, threadId };
});
await runSingleMigration(migrationName);
await withContext(async (context) => {
expect(await readAgent(context, agentId)).toMatchObject({ scope: 'project' });
expect(await readIds(context, 'agent_execution_threads')).toEqual([threadId]);
expect(await readIds(context, 'agent_execution')).toHaveLength(1);
});
});
it('stores an instance agent without a project and rejects an unknown scope', async () => {
await runSingleMigration(migrationName);
await withContext(async (context) => {
const agentId = await insertAgent(context, null, 'instance');
expect(await readAgent(context, agentId)).toEqual({ scope: 'instance', projectId: null });
const projectId = await insertProject(context);
await expect(insertAgent(context, projectId, 'global')).rejects.toThrow();
});
});
it('rejects a scope that does not match the project', async () => {
await runSingleMigration(migrationName);
await withContext(async (context) => {
const projectId = await insertProject(context);
// A project agent needs a project.
await expect(insertAgent(context, null)).rejects.toThrow();
await expect(insertAgent(context, null, 'project')).rejects.toThrow();
// An instance agent belongs to no project.
await expect(insertAgent(context, projectId, 'instance')).rejects.toThrow();
expect(await readIds(context, 'agents')).toEqual([]);
});
});
it('rejects an agent without a project before the migration', async () => {
await withContext(async (context) => {
await expect(insertAgent(context, null)).rejects.toThrow();
});
});
it('deletes instance agents on revert, and keeps project agents with their rows', async () => {
await runSingleMigration(migrationName);
const kept = await withContext(async (context) => {
const projectId = await insertProject(context);
const projectAgentId = await insertAgent(context, projectId);
const projectThreadId = await insertThreadWithExecution(context, projectAgentId, projectId);
// DeleteInstanceAgentsOnRevert deletes the rows that depend on instance agents first.
await insertAgent(context, null, 'instance');
return { projectAgentId, projectThreadId };
});
await undoLastSingleMigration();
await withContext(async (context) => {
expect(await readIds(context, 'agents')).toEqual([kept.projectAgentId]);
expect(await readIds(context, 'agent_execution_threads')).toEqual([kept.projectThreadId]);
expect(await readIds(context, 'agent_execution')).toHaveLength(1);
if (context.isSqlite) {
expect(await context.runQuery('PRAGMA foreign_key_check')).toEqual([]);
}
await expect(insertAgent(context, null)).rejects.toThrow();
// The foreign keys still hold after the table was recreated.
await expect(
insertThreadWithExecution(context, randomUUID(), await insertProject(context)),
).rejects.toThrow();
});
});
it('can apply again after a revert', async () => {
const agentId = await withContext(async (context) => {
return await insertAgent(context, await insertProject(context));
});
await runSingleMigration(migrationName);
await undoLastSingleMigration();
await runSingleMigration(migrationName);
await withContext(async (context) => {
expect(await readAgent(context, agentId)).toMatchObject({ scope: 'project' });
await expect(insertAgent(context, null, 'instance')).resolves.toBeDefined();
});
});
});

View File

@ -0,0 +1,138 @@
import {
createTestMigrationContext,
initDbUpToMigration,
runSingleMigration,
undoLastSingleMigration,
type TestMigrationContext,
} from '@n8n/backend-test-utils';
import { DbConnection } from '@n8n/db';
import { Container } from '@n8n/di';
import { DataSource } from '@n8n/typeorm';
import { randomUUID } from 'node:crypto';
const migrationName = 'DeleteInstanceAgentsOnRevert1792139680416';
describe('DeleteInstanceAgentsOnRevert migration', () => {
let dataSource: DataSource;
beforeAll(async () => {
await Container.get(DbConnection).init();
dataSource = Container.get(DataSource);
});
beforeEach(async () => {
await withContext(async ({ queryRunner }) => await queryRunner.clearDatabase());
await initDbUpToMigration(migrationName);
});
afterAll(async () => {
await Container.get(DbConnection).close();
});
async function withContext<T>(run: (context: TestMigrationContext) => Promise<T>): Promise<T> {
const context = createTestMigrationContext(dataSource);
try {
return await run(context);
} finally {
await context.queryRunner.release();
}
}
function columns(context: TestMigrationContext, names: string[]) {
return names.map((name) => context.escape.columnName(name)).join(', ');
}
async function insertProject(context: TestMigrationContext) {
const id = randomUUID();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('project')} (${columns(context, ['id', 'name', 'type', 'createdAt', 'updatedAt'])})
VALUES (:id, 'Agents project', 'team', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ id },
);
return id;
}
async function insertAgent(context: TestMigrationContext, projectId: string | null) {
const id = randomUUID();
const scope = projectId === null ? 'instance' : 'project';
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agents')} (${columns(context, ['id', 'name', 'projectId', 'scope', 'createdAt', 'updatedAt'])})
VALUES (:id, 'Agent', :projectId, :scope, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ id, projectId, scope },
);
return id;
}
/** A thread with one execution: rows that depend on the agent at two levels. */
async function insertThreadWithExecution(
context: TestMigrationContext,
agentId: string,
projectId: string,
) {
const threadId = randomUUID();
const executionId = randomUUID();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agent_execution_threads')}
(${columns(context, ['id', 'agentId', 'agentName', 'projectId', 'createdAt', 'updatedAt'])})
VALUES (:threadId, :agentId, 'Agent', :projectId, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ threadId, agentId, projectId },
);
await context.runQuery(
`INSERT INTO ${context.escape.tableName('agent_execution')}
(${columns(context, ['id', 'threadId', 'status', 'createdAt', 'updatedAt'])})
VALUES (:executionId, :threadId, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ executionId, threadId },
);
return { threadId, executionId };
}
async function readIds(context: TestMigrationContext, table: string) {
const rows = await context.runQuery<Array<{ id: string }>>(
`SELECT ${context.escape.columnName('id')} AS id FROM ${context.escape.tableName(table)}`,
);
return rows.map((row) => row.id).sort();
}
it('deletes instance agents and their dependent rows on revert', async () => {
await runSingleMigration(migrationName);
const kept = await withContext(async (context) => {
const projectId = await insertProject(context);
const projectAgentId = await insertAgent(context, projectId);
const projectRows = await insertThreadWithExecution(context, projectAgentId, projectId);
const instanceAgentId = await insertAgent(context, null);
await insertThreadWithExecution(context, instanceAgentId, projectId);
return { projectAgentId, ...projectRows };
});
await undoLastSingleMigration();
await withContext(async (context) => {
expect(await readIds(context, 'agents')).toEqual([kept.projectAgentId]);
expect(await readIds(context, 'agent_execution_threads')).toEqual([kept.threadId]);
expect(await readIds(context, 'agent_execution')).toEqual([kept.executionId]);
if (context.isSqlite) {
expect(await context.runQuery('PRAGMA foreign_key_check')).toEqual([]);
}
});
});
it('leaves no orphan rows after both migrations are reverted', async () => {
await runSingleMigration(migrationName);
await withContext(async (context) => {
const projectId = await insertProject(context);
await insertThreadWithExecution(context, await insertAgent(context, null), projectId);
});
await undoLastSingleMigration();
await undoLastSingleMigration();
await withContext(async (context) => {
expect(await readIds(context, 'agents')).toEqual([]);
expect(await readIds(context, 'agent_execution_threads')).toEqual([]);
expect(await readIds(context, 'agent_execution')).toEqual([]);
if (context.isSqlite) {
expect(await context.runQuery('PRAGMA foreign_key_check')).toEqual([]);
}
});
});
});