diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts
index 64ecddb54d4..8169493b6e6 100644
--- a/packages/@n8n/api-types/src/frontend-settings.ts
+++ b/packages/@n8n/api-types/src/frontend-settings.ts
@@ -336,6 +336,17 @@ export type FrontendModuleSettings = {
autoExposeNewWorkflows: boolean;
};
+ /**
+ * Client settings for the instance-reporting module. Present only when the
+ * module is enabled on this instance.
+ */
+ 'instance-reporting'?: {
+ /** Whether a receiver is configured, i.e. whether reports are actually sent. */
+ enabled: boolean;
+ /** Minute of the UTC day the daily report fires at, as `HH:mm`. Absent when disabled. */
+ reportTime?: string;
+ };
+
/**
* Client settings for the encryption-key-manager module.
*/
diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts
index d60658a066d..6a172dbee41 100644
--- a/packages/@n8n/api-types/src/index.ts
+++ b/packages/@n8n/api-types/src/index.ts
@@ -11,6 +11,7 @@ export type * from './community-node-types';
export type * from './quick-connect';
export * from './agents/index';
export * from './instance-registry-types';
+export type * from './instance-reporting';
export type * from './worker-pools';
export * from './redaction-enforcement';
export * from './redaction-enforcement-floor';
diff --git a/packages/@n8n/api-types/src/instance-reporting.ts b/packages/@n8n/api-types/src/instance-reporting.ts
new file mode 100644
index 00000000000..98e9db707a9
--- /dev/null
+++ b/packages/@n8n/api-types/src/instance-reporting.ts
@@ -0,0 +1,7 @@
+export type InstanceReportingStatus = {
+ /**
+ * When the receiver last accepted a report, as an ISO 8601 UTC string, or
+ * `null` when it never did.
+ */
+ lastSuccessfulReport: string | null;
+};
diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs
index f3826c5b986..09304c7f98f 100644
--- a/packages/cli/eslint.config.mjs
+++ b/packages/cli/eslint.config.mjs
@@ -419,7 +419,7 @@ export default defineConfig(
'./src/license.ts',
'./src/modules/agents/integrations/n8n-checkpoint-storage.ts',
'./src/modules/instance-ai/instance-ai.service.ts',
- './src/modules/instance-reporting/instance-reporting-scheduler.service.ts',
+ './src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts',
'./src/services/pruning/executions-pruning.service.ts',
'./src/services/pruning/workflow-history-compaction.service.ts',
'./src/services/workflow-statistics-rollup.service.ts',
diff --git a/packages/cli/src/modules/instance-reporting/LOCAL_VERIFICATION.md b/packages/cli/src/modules/instance-reporting.ee/LOCAL_VERIFICATION.md
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/LOCAL_VERIFICATION.md
rename to packages/cli/src/modules/instance-reporting.ee/LOCAL_VERIFICATION.md
diff --git a/packages/cli/src/modules/instance-reporting/README.md b/packages/cli/src/modules/instance-reporting.ee/README.md
similarity index 81%
rename from packages/cli/src/modules/instance-reporting/README.md
rename to packages/cli/src/modules/instance-reporting.ee/README.md
index 514e68e55d3..3450686a8ad 100644
--- a/packages/cli/src/modules/instance-reporting/README.md
+++ b/packages/cli/src/modules/instance-reporting.ee/README.md
@@ -85,7 +85,7 @@ there.
| Env var | Default | Notes |
|---|---|---|
-| `N8N_INSTANCE_REPORTING_BASE_URL` | `''` | Base URL of the receiver. The report is POSTed to `/api/v1/instance-reports`. Left unset, the module loads but warns and never sends. |
+| `N8N_INSTANCE_REPORTING_BASE_URL` | `''` | Base URL of the receiver. The report is POSTed to `/api/v1/instance-reports`. Left unset, the module loads but warns and never sends: it starts no scheduler and claims no report time. |
| `N8N_INSTANCE_REPORTING_LABEL` | `''` | Sent as `label` in the payload, when set. |
| `N8N_INSTANCE_REPORTING_AUTH_TOKEN` | `''` | Sent as `Authorization: Bearer …`, when set. |
@@ -99,5 +99,38 @@ instance calling at the same minute. The time is always UTC and never before
first; it shifts itself later, logging a warning, if
`N8N_INSIGHTS_COMPACTION_INTERVAL_MINUTES` is raised enough to require it.
+## Client settings
+
+`GET /rest/module-settings` carries an `instance-reporting` key when the module
+is enabled on this instance:
+
+```json
+{
+ "instance-reporting": {
+ "enabled": true,
+ "reportTime": "07:42"
+ }
+}
+```
+
+`enabled` says whether a receiver is configured. Without one the key reads
+`{ "enabled": false }` and carries no `reportTime`, since no time is claimed. A
+missing key means the module is not enabled at all.
+
+These settings are built once, during module init, and served from a cache
+afterwards, so they carry only values that stay the same for the lifetime of
+the process.
+
+## Reporting status
+
+`GET /rest/instance-reporting/status` answers with the UTC instant the receiver
+last accepted a report, or `null` when it never did:
+
+```json
+{ "lastSuccessfulReport": "2026-03-25T07:42:13.000Z" }
+```
+
+The route exists whenever the module is loaded, including without a receiver — the client decides whether to ask by reading `enabled` from the client settings.
+
See [.agents/specs/central-instance-monitoring.md](../../../../../.agents/specs/central-instance-monitoring.md)
for the full design.
diff --git a/packages/cli/src/modules/instance-reporting/__tests__/instance-reporting-scheduler.service.test.ts b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting-scheduler.service.test.ts
similarity index 88%
rename from packages/cli/src/modules/instance-reporting/__tests__/instance-reporting-scheduler.service.test.ts
rename to packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting-scheduler.service.test.ts
index 579d184ca9f..2572890f56b 100644
--- a/packages/cli/src/modules/instance-reporting/__tests__/instance-reporting-scheduler.service.test.ts
+++ b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting-scheduler.service.test.ts
@@ -1,8 +1,6 @@
-import type { ModuleName, ModulesConfig } from '@n8n/backend-common';
import { mockLogger } from '@n8n/backend-test-utils';
import { Time } from '@n8n/constants';
import type { InstanceSettings } from 'n8n-core';
-import { UserError } from 'n8n-workflow';
import type { Mocked } from 'vitest';
import { mock } from 'vitest-mock-extended';
@@ -11,7 +9,6 @@ import type { EventService } from '@/events/event.service';
import type { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
import { InstanceReportingScheduler } from '../instance-reporting-scheduler.service';
import type { InstanceReportingSettingsService } from '../instance-reporting-settings.service';
-import { InstanceReportingConfig } from '../instance-reporting.config';
import type { InstanceReportingService } from '../instance-reporting.service';
const REPORT_TIME = '07:42';
@@ -28,24 +25,18 @@ interface Harness {
reportRepository: Mocked;
settingsService: Mocked;
instanceSettings: Mocked;
- modulesConfig: Mocked;
eventService: Mocked;
/** Fire the deferred `server-started` listeners (only needed when autoStart is false). */
startServer: () => void;
}
function makeHarness({
- baseUrl = 'https://example.com',
isLeader = true,
- disabledModules = [] as ModuleName[],
// The scheduler arms its first tick on `server-started`. By default the harness
// fires that listener the moment it is registered, so `init()` starts reporting
// as the tests expect. Pass false to hold it and fire it later via `startServer`.
autoStart = true,
} = {}): Harness {
- const config = new InstanceReportingConfig();
- config.instanceReportingBaseUrl = baseUrl;
-
const reportingService = mock();
// No attempt has been made yet, so nothing is holding the next one back.
reportingService.msUntilRetryAllowed.mockResolvedValue(0);
@@ -61,7 +52,6 @@ function makeHarness({
instanceRole: isLeader ? 'leader' : 'follower',
isLeader,
});
- const modulesConfig = mock({ disabledModules });
const serverStartedListeners: Array<() => void> = [];
const eventService = mock();
@@ -81,12 +71,10 @@ function makeHarness({
});
const scheduler = new InstanceReportingScheduler(
- config,
reportingService,
reportRepository,
settingsService,
instanceSettings,
- modulesConfig,
eventService,
mockLogger(),
);
@@ -97,7 +85,6 @@ function makeHarness({
reportRepository,
settingsService,
instanceSettings,
- modulesConfig,
eventService,
startServer: () => serverStartedListeners.forEach((listener) => listener()),
};
@@ -120,25 +107,10 @@ describe('InstanceReportingScheduler', () => {
});
describe('init', () => {
- test('fails when the insights module is disabled', async () => {
- const { scheduler } = makeHarness({ disabledModules: ['insights'] });
-
- await expect(scheduler.init()).rejects.toThrow(UserError);
- });
-
- test('warns and stays idle when no base URL is configured', async () => {
- const { scheduler, reportingService } = makeHarness({ baseUrl: '' });
-
- await scheduler.init();
- await vi.advanceTimersByTimeAsync(Time.days.toMilliseconds);
-
- expect(reportingService.sendReport).not.toHaveBeenCalled();
- });
-
test('does not start a timer on a follower', async () => {
const { scheduler, reportingService } = makeHarness({ isLeader: false });
- await scheduler.init();
+ scheduler.init();
await vi.advanceTimersByTimeAsync(Time.days.toMilliseconds);
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -147,7 +119,7 @@ describe('InstanceReportingScheduler', () => {
test('fires at the configured time on the leader', async () => {
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -160,7 +132,7 @@ describe('InstanceReportingScheduler', () => {
test('fires once a day thereafter', async () => {
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await vi.advanceTimersByTimeAsync(3 * Time.days.toMilliseconds);
expect(reportingService.sendReport).toHaveBeenCalledTimes(3);
@@ -172,7 +144,7 @@ describe('InstanceReportingScheduler', () => {
vi.setSystemTime(new Date(AFTER_SLOT));
const { scheduler, reportingService, startServer } = makeHarness({ autoStart: false });
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -188,7 +160,7 @@ describe('InstanceReportingScheduler', () => {
vi.setSystemTime(new Date(AFTER_SLOT));
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).toHaveBeenCalledTimes(1);
@@ -199,7 +171,7 @@ describe('InstanceReportingScheduler', () => {
const { scheduler, reportingService, reportRepository } = makeHarness();
reportRepository.hasSettledToday.mockResolvedValue(true);
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -209,7 +181,7 @@ describe('InstanceReportingScheduler', () => {
vi.setSystemTime(new Date(AFTER_SLOT));
const { scheduler, reportingService, reportRepository } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
// The catch-up delivered it, so the rest of the day has nothing to do.
reportRepository.hasSettledToday.mockResolvedValue(true);
@@ -224,7 +196,7 @@ describe('InstanceReportingScheduler', () => {
vi.setSystemTime(new Date(AFTER_SLOT));
const { scheduler, reportingService, instanceSettings } = makeHarness({ isLeader: false });
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -246,7 +218,7 @@ describe('InstanceReportingScheduler', () => {
vi.setSystemTime(new Date(AFTER_SLOT));
const { scheduler, reportingService, startServer } = makeHarness({ autoStart: false });
- await scheduler.init();
+ scheduler.init();
scheduler.stop();
startServer();
await settle();
@@ -257,7 +229,7 @@ describe('InstanceReportingScheduler', () => {
test('stops reporting when this main steps down', async () => {
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
scheduler.stop();
await vi.advanceTimersByTimeAsync(2 * Time.days.toMilliseconds);
@@ -268,7 +240,7 @@ describe('InstanceReportingScheduler', () => {
test('shutdown stops the timer and blocks a later takeover', async () => {
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
scheduler.shutdown();
scheduler.start();
@@ -284,7 +256,7 @@ describe('InstanceReportingScheduler', () => {
const { scheduler, reportingService } = makeHarness();
reportingService.sendReport.mockRejectedValueOnce(new Error('Network error'));
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).toHaveBeenCalledTimes(1);
@@ -303,7 +275,7 @@ describe('InstanceReportingScheduler', () => {
async () => reportingService.sendReport.mock.calls.length >= 3,
);
- await scheduler.init();
+ scheduler.init();
await vi.advanceTimersByTimeAsync(Time.hours.toMilliseconds);
expect(reportingService.sendReport).toHaveBeenCalledTimes(3);
@@ -316,7 +288,7 @@ describe('InstanceReportingScheduler', () => {
const { scheduler, reportingService } = makeHarness();
reportingService.msUntilRetryAllowed.mockResolvedValueOnce(2 * Time.minutes.toMilliseconds);
- await scheduler.init();
+ scheduler.init();
await settle();
expect(reportingService.sendReport).not.toHaveBeenCalled();
@@ -329,7 +301,7 @@ describe('InstanceReportingScheduler', () => {
const { scheduler, settingsService } = makeHarness();
settingsService.getReportTime.mockRejectedValueOnce(new Error('DB down'));
- await scheduler.init();
+ scheduler.init();
await settle();
expect(settingsService.getReportTime).toHaveBeenCalledTimes(1);
@@ -343,7 +315,7 @@ describe('InstanceReportingScheduler', () => {
test('a fire before the slot reports nothing and re-arms', async () => {
const { scheduler, reportingService } = makeHarness();
- await scheduler.init();
+ scheduler.init();
await settle();
// Clock jumps backwards an hour, well before the slot.
vi.setSystemTime(new Date('2026-03-26T06:41:00.000Z'));
diff --git a/packages/cli/src/modules/instance-reporting/__tests__/instance-reporting-settings.service.test.ts b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting-settings.service.test.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/__tests__/instance-reporting-settings.service.test.ts
rename to packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting-settings.service.test.ts
diff --git a/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.controller.test.ts b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.controller.test.ts
new file mode 100644
index 00000000000..6f4e873962c
--- /dev/null
+++ b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.controller.test.ts
@@ -0,0 +1,28 @@
+import { mock } from 'vitest-mock-extended';
+
+import type { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
+import { InstanceReportingController } from '../instance-reporting.controller';
+
+const LAST_DELIVERY = new Date('2026-03-25T07:42:13.000Z');
+
+describe('InstanceReportingController', () => {
+ describe('getStatus()', () => {
+ it('reports when the receiver last accepted a report', async () => {
+ const reportRepository = mock();
+ reportRepository.findLastDeliveryTime.mockResolvedValue(LAST_DELIVERY);
+
+ const status = await new InstanceReportingController(reportRepository).getStatus();
+
+ expect(status).toEqual({ lastSuccessfulReport: LAST_DELIVERY.toISOString() });
+ });
+
+ it('reports a null last delivery until the receiver accepts a report', async () => {
+ const reportRepository = mock();
+ reportRepository.findLastDeliveryTime.mockResolvedValue(null);
+
+ const status = await new InstanceReportingController(reportRepository).getStatus();
+
+ expect(status).toEqual({ lastSuccessfulReport: null });
+ });
+ });
+});
diff --git a/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.module.test.ts b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.module.test.ts
new file mode 100644
index 00000000000..5a7e271b536
--- /dev/null
+++ b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.module.test.ts
@@ -0,0 +1,115 @@
+import type { FrontendModuleSettings } from '@n8n/api-types';
+import type { ModuleName } from '@n8n/backend-common';
+import { Logger, ModulesConfig } from '@n8n/backend-common';
+import { mockLogger } from '@n8n/backend-test-utils';
+import type { Controller } from '@n8n/decorators';
+import { ControllerRegistryMetadata, ModuleMetadata } from '@n8n/decorators';
+import { Container } from '@n8n/di';
+import { UserError } from 'n8n-workflow';
+import { mock } from 'vitest-mock-extended';
+
+import { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
+import { InstanceReportingScheduler } from '../instance-reporting-scheduler.service';
+import { InstanceReportingSettingsService } from '../instance-reporting-settings.service';
+import { InstanceReportingConfig } from '../instance-reporting.config';
+import { InstanceReportingModule } from '../instance-reporting.module';
+
+// Compile-time pin: the settings this module returns are read by the frontend
+// under this exact key, so the decorator name must stay a member of
+// `FrontendModuleSettings`.
+const MODULE_NAME = 'instance-reporting' satisfies keyof FrontendModuleSettings;
+
+const REPORT_TIME = '07:42';
+const LAST_DELIVERY = new Date('2026-03-25T07:42:13.000Z');
+
+function setUpContainer({
+ baseUrl = 'https://example.com',
+ disabledModules = [] as ModuleName[],
+ lastDelivery = LAST_DELIVERY as Date | null,
+} = {}) {
+ const config = new InstanceReportingConfig();
+ config.instanceReportingBaseUrl = baseUrl;
+ Container.set(InstanceReportingConfig, config);
+
+ Container.set(ModulesConfig, mock({ disabledModules }));
+ Container.set(Logger, mockLogger());
+
+ const settingsService = mock();
+ settingsService.getReportTime.mockResolvedValue(REPORT_TIME);
+ Container.set(InstanceReportingSettingsService, settingsService);
+
+ const reportRepository = mock();
+ reportRepository.findLastDeliveryTime.mockResolvedValue(lastDelivery);
+ Container.set(InstanceMonitoringReportRepository, reportRepository);
+
+ const scheduler = mock();
+ Container.set(InstanceReportingScheduler, scheduler);
+
+ return { settingsService, reportRepository, scheduler };
+}
+
+describe('InstanceReportingModule', () => {
+ describe('settings()', () => {
+ it('reports the time the instance is due to report at', async () => {
+ setUpContainer();
+
+ const settings = await new InstanceReportingModule().settings();
+
+ expect(settings).toEqual({ enabled: true, reportTime: REPORT_TIME });
+ });
+
+ it('reports as disabled, claiming no time and reading nothing, without a receiver', async () => {
+ const { settingsService, reportRepository } = setUpContainer({ baseUrl: '' });
+
+ const settings = await new InstanceReportingModule().settings();
+
+ expect(settings).toEqual({ enabled: false });
+ expect(settingsService.getReportTime).not.toHaveBeenCalled();
+ expect(reportRepository.findLastDeliveryTime).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('init()', () => {
+ it('starts the scheduler', async () => {
+ const { scheduler } = setUpContainer();
+
+ await new InstanceReportingModule().init();
+
+ expect(scheduler.init).toHaveBeenCalled();
+ });
+
+ it('fails when the insights module is disabled', async () => {
+ setUpContainer({ disabledModules: ['insights'] });
+
+ await expect(new InstanceReportingModule().init()).rejects.toThrow(UserError);
+ });
+
+ it('leaves the scheduler alone when no receiver is configured', async () => {
+ const { scheduler } = setUpContainer({ baseUrl: '' });
+
+ await new InstanceReportingModule().init();
+
+ expect(scheduler.init).not.toHaveBeenCalled();
+ });
+
+ // The route belongs to the loaded module, not to the receiver, so a client
+ // that asks anyway gets an answer instead of a 404.
+ it('registers the status route even when no receiver is configured', async () => {
+ setUpContainer({ baseUrl: '' });
+
+ await new InstanceReportingModule().init();
+
+ const { InstanceReportingController } = await import('../instance-reporting.controller.js');
+ const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
+ InstanceReportingController as Controller,
+ );
+
+ expect(metadata.basePath).toBe('/instance-reporting');
+ expect(metadata.routes.get('getStatus')?.path).toBe('/status');
+ });
+ });
+
+ it('is registered under the module-settings key the frontend reads', () => {
+ expect(Container.get(ModuleMetadata).get(MODULE_NAME)?.class).toBe(InstanceReportingModule);
+ });
+});
diff --git a/packages/cli/src/modules/instance-reporting/__tests__/instance-reporting.service.test.ts b/packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.service.test.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/__tests__/instance-reporting.service.test.ts
rename to packages/cli/src/modules/instance-reporting.ee/__tests__/instance-reporting.service.test.ts
diff --git a/packages/cli/src/modules/instance-reporting/database/entities/instance-monitoring-report.ts b/packages/cli/src/modules/instance-reporting.ee/database/entities/instance-monitoring-report.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/database/entities/instance-monitoring-report.ts
rename to packages/cli/src/modules/instance-reporting.ee/database/entities/instance-monitoring-report.ts
diff --git a/packages/cli/src/modules/instance-reporting/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts b/packages/cli/src/modules/instance-reporting.ee/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts
similarity index 83%
rename from packages/cli/src/modules/instance-reporting/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts
rename to packages/cli/src/modules/instance-reporting.ee/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts
index 907c0ef67cf..e60a5fb55a2 100644
--- a/packages/cli/src/modules/instance-reporting/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts
+++ b/packages/cli/src/modules/instance-reporting.ee/database/repositories/__tests__/instance-monitoring-report.repository.integration.test.ts
@@ -166,6 +166,43 @@ describe('InstanceMonitoringReportRepository', () => {
});
});
+ describe('findLastDeliveryTime', () => {
+ test('is null when nothing was ever delivered', async () => {
+ await repository.createPending(DATA_POINTS);
+
+ await expect(repository.findLastDeliveryTime()).resolves.toBeNull();
+ });
+
+ test('returns the latest delivery time across delivered reports', async () => {
+ const earlier = await repository.createPending(DATA_POINTS);
+ await repository.markDelivered(earlier.id, new Date('2026-03-24T07:42:00.000Z'));
+
+ const latest = await repository.createPending(DATA_POINTS);
+ const latestDeliveredAt = new Date('2026-03-25T07:42:13.000Z');
+ await repository.markDelivered(latest.id, latestDeliveredAt);
+
+ const lastDelivery = await repository.findLastDeliveryTime();
+
+ expect(lastDelivery?.toISOString()).toBe(latestDeliveredAt.toISOString());
+ });
+
+ test('ignores reports that never reached the receiver', async () => {
+ const delivered = await repository.createPending(DATA_POINTS);
+ const deliveredAt = new Date('2026-03-24T07:42:00.000Z');
+ await repository.markDelivered(delivered.id, deliveredAt);
+
+ const failed = await repository.createPending(DATA_POINTS);
+ await repository.recordFailure(failed.id, 'Network error', new Date());
+ const skipped = await repository.createPending(DATA_POINTS);
+ await repository.markSkipped(skipped.id);
+ await repository.createPending(DATA_POINTS);
+
+ const lastDelivery = await repository.findLastDeliveryTime();
+
+ expect(lastDelivery?.toISOString()).toBe(deliveredAt.toISOString());
+ });
+ });
+
describe('markDelivered', () => {
test('stamps the delivery time, counts the attempt and clears any earlier error', async () => {
const { id } = await repository.createPending(DATA_POINTS);
diff --git a/packages/cli/src/modules/instance-reporting/database/repositories/instance-monitoring-report.repository.ts b/packages/cli/src/modules/instance-reporting.ee/database/repositories/instance-monitoring-report.repository.ts
similarity index 91%
rename from packages/cli/src/modules/instance-reporting/database/repositories/instance-monitoring-report.repository.ts
rename to packages/cli/src/modules/instance-reporting.ee/database/repositories/instance-monitoring-report.repository.ts
index 096bcccaebe..3bf6ee8fb21 100644
--- a/packages/cli/src/modules/instance-reporting/database/repositories/instance-monitoring-report.repository.ts
+++ b/packages/cli/src/modules/instance-reporting.ee/database/repositories/instance-monitoring-report.repository.ts
@@ -1,5 +1,5 @@
import { Service } from '@n8n/di';
-import { DataSource, MoreThanOrEqual, Not, Repository } from '@n8n/typeorm';
+import { DataSource, IsNull, MoreThanOrEqual, Not, Repository } from '@n8n/typeorm';
import { v4 as uuid } from 'uuid';
import type { InstanceReportDataPoint } from '../entities/instance-monitoring-report';
@@ -85,6 +85,19 @@ export class InstanceMonitoringReportRepository extends Repository (a > b ? a : b)) : null;
}
+ /**
+ * When the receiver last accepted a report, or `null` when it never did.
+ */
+ async findLastDeliveryTime(): Promise {
+ const report = await this.findOne({
+ where: { status: 'delivered', deliveredAt: Not(IsNull()) },
+ order: { deliveredAt: 'DESC' },
+ select: ['deliveredAt'],
+ });
+
+ return report?.deliveredAt ?? null;
+ }
+
async markDelivered(id: string, deliveredAt: Date): Promise {
await this.increment({ id }, 'attempts', 1);
await this.update(
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting-scheduler.service.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts
similarity index 84%
rename from packages/cli/src/modules/instance-reporting/instance-reporting-scheduler.service.ts
rename to packages/cli/src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts
index 53f0e4c5752..3950a08795d 100644
--- a/packages/cli/src/modules/instance-reporting/instance-reporting-scheduler.service.ts
+++ b/packages/cli/src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts
@@ -1,18 +1,16 @@
-import { Logger, ModulesConfig } from '@n8n/backend-common';
+import { Logger } from '@n8n/backend-common';
import { Time } from '@n8n/constants';
import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di';
import { InstanceSettings } from 'n8n-core';
-import { UserError } from 'n8n-workflow';
import { strict } from 'node:assert';
+import { EventService } from '@/events/event.service';
+
import { InstanceMonitoringReportRepository } from './database/repositories/instance-monitoring-report.repository';
import { InstanceReportingSettingsService } from './instance-reporting-settings.service';
-import { InstanceReportingConfig } from './instance-reporting.config';
import { InstanceReportingService, RETRY_DELAY_MS } from './instance-reporting.service';
-import { EventService } from '@/events/event.service';
-
const MINUTES_PER_DAY = 24 * 60;
/**
@@ -43,12 +41,10 @@ export class InstanceReportingScheduler {
private readonly onServerStarted = () => this.start();
constructor(
- private readonly config: InstanceReportingConfig,
private readonly reportingService: InstanceReportingService,
private readonly reportRepository: InstanceMonitoringReportRepository,
private readonly settingsService: InstanceReportingSettingsService,
private readonly instanceSettings: InstanceSettings,
- private readonly modulesConfig: ModulesConfig,
private readonly eventService: EventService,
private readonly logger: Logger,
) {
@@ -56,29 +52,16 @@ export class InstanceReportingScheduler {
}
/**
- * Validate what the module needs, then start reporting if this instance leads.
+ * Start reporting if this instance leads.
*
- * Runs from the module entrypoint, which the registry calls after
+ * Runs from the module entrypoint, which checks that the module has what it
+ * needs before it loads this class at all, and which the registry calls after
* `initOrchestration` has settled this instance's role, so `isLeader` is
* already meaningful here.
*/
- async init(): Promise {
+ init(): void {
strict(this.instanceSettings.instanceRole !== 'unset', 'Instance role is not set');
- // The daily figure is read from insights, so the reporter cannot run without it.
- if (this.modulesConfig.disabledModules.includes('insights')) {
- throw new UserError(
- 'The `instance-reporting` module requires the `insights` module, but it is listed in N8N_DISABLED_MODULES. Remove `insights` from N8N_DISABLED_MODULES or remove `instance-reporting` from N8N_ENABLED_MODULES.',
- );
- }
-
- if (!this.config.instanceReportingBaseUrl) {
- this.logger.warn(
- 'Instance reporting is enabled but N8N_INSTANCE_REPORTING_BASE_URL is unset, so no reports will be sent',
- );
- return;
- }
-
// Defer the first tick until the server has finished starting. A boot
// catch-up report can send right away, but log streaming module was not properly initialized
if (this.instanceSettings.isLeader) {
@@ -87,11 +70,7 @@ export class InstanceReportingScheduler {
}
get isEnabled(): boolean {
- return (
- this.instanceSettings.instanceType === 'main' &&
- this.instanceSettings.isLeader &&
- this.config.instanceReportingBaseUrl !== ''
- );
+ return this.instanceSettings.instanceType === 'main' && this.instanceSettings.isLeader;
}
/**
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting-settings.service.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting-settings.service.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/instance-reporting-settings.service.ts
rename to packages/cli/src/modules/instance-reporting.ee/instance-reporting-settings.service.ts
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting.config.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.config.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/instance-reporting.config.ts
rename to packages/cli/src/modules/instance-reporting.ee/instance-reporting.config.ts
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting.constants.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.constants.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/instance-reporting.constants.ts
rename to packages/cli/src/modules/instance-reporting.ee/instance-reporting.constants.ts
diff --git a/packages/cli/src/modules/instance-reporting.ee/instance-reporting.controller.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.controller.ts
new file mode 100644
index 00000000000..fc72daed9d3
--- /dev/null
+++ b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.controller.ts
@@ -0,0 +1,16 @@
+import type { InstanceReportingStatus } from '@n8n/api-types';
+import { Get, RestController } from '@n8n/decorators';
+
+import { InstanceMonitoringReportRepository } from './database/repositories/instance-monitoring-report.repository';
+
+@RestController('/instance-reporting')
+export class InstanceReportingController {
+ constructor(private readonly reportRepository: InstanceMonitoringReportRepository) {}
+
+ @Get('/status')
+ async getStatus(): Promise {
+ const lastDelivery = await this.reportRepository.findLastDeliveryTime();
+
+ return { lastSuccessfulReport: lastDelivery?.toISOString() ?? null };
+ }
+}
diff --git a/packages/cli/src/modules/instance-reporting.ee/instance-reporting.module.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.module.ts
new file mode 100644
index 00000000000..fe73d03a305
--- /dev/null
+++ b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.module.ts
@@ -0,0 +1,84 @@
+import { Logger, ModulesConfig } from '@n8n/backend-common';
+import type { ModuleInterface } from '@n8n/decorators';
+import { BackendModule } from '@n8n/decorators';
+import { Container } from '@n8n/di';
+import { UserError } from 'n8n-workflow';
+
+/**
+ * Reports this instance's billable execution numbers to a central monitoring
+ * receiver, once a day.
+ *
+ * Opt-in: not a default module, so it runs only when an operator lists it in
+ * `N8N_ENABLED_MODULES`. Main-only, and within a multi-main deployment only the
+ * leader holds the timer, so a cluster reports once rather than once per main.
+ *
+ * The daily figure comes from the insights module, but the receiver only sees
+ * data points, so that source is an implementation detail of
+ * `InstanceReportingService` rather than part of the reporting contract.
+ */
+@BackendModule({ name: 'instance-reporting', instanceTypes: ['main'] })
+export class InstanceReportingModule implements ModuleInterface {
+ async init() {
+ // Imported before the receiver check, so the route exists whenever the
+ // module is loaded. The client gates off `enabled` in the module settings.
+ await import('./instance-reporting.controller.js');
+
+ // The daily figure is read from insights, so the reporter cannot run without it.
+ if (Container.get(ModulesConfig).disabledModules.includes('insights')) {
+ throw new UserError(
+ 'The `instance-reporting` module requires the `insights` module, but it is listed in N8N_DISABLED_MODULES. Remove `insights` from N8N_DISABLED_MODULES or remove `instance-reporting` from N8N_ENABLED_MODULES.',
+ );
+ }
+
+ if (!(await this.isConfigured())) {
+ Container.get(Logger)
+ .scoped('instance-reporting')
+ .warn(
+ 'Instance reporting is enabled but N8N_INSTANCE_REPORTING_BASE_URL is unset, so no reports will be sent',
+ );
+ return;
+ }
+
+ const { InstanceReportingScheduler } = await import(
+ './instance-reporting-scheduler.service.js'
+ );
+
+ Container.get(InstanceReportingScheduler).init();
+ }
+
+ /**
+ * Settings exposed to the frontend under `/rest/module-settings`.
+ *
+ * Return values:
+ * { enabled: false } - module loaded but no receiver configured
+ * { enabled: true, reportTime: 'HH:mm' } - module loaded and receiver configured
+ *
+ * Built once at startup and cached for the process lifetime.
+ **/
+ async settings() {
+ if (!(await this.isConfigured())) return { enabled: false };
+
+ const { InstanceReportingSettingsService } = await import(
+ './instance-reporting-settings.service.js'
+ );
+
+ const reportTime = await Container.get(InstanceReportingSettingsService).getReportTime();
+
+ return { enabled: true, reportTime };
+ }
+
+ async entities() {
+ const { InstanceMonitoringReport } = await import(
+ './database/entities/instance-monitoring-report.js'
+ );
+
+ return [InstanceMonitoringReport];
+ }
+
+ /** Whether a receiver is configured, i.e. whether reports are actually sent. */
+ private async isConfigured(): Promise {
+ const { InstanceReportingConfig } = await import('./instance-reporting.config.js');
+
+ return Container.get(InstanceReportingConfig).instanceReportingBaseUrl !== '';
+ }
+}
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting.service.ts b/packages/cli/src/modules/instance-reporting.ee/instance-reporting.service.ts
similarity index 100%
rename from packages/cli/src/modules/instance-reporting/instance-reporting.service.ts
rename to packages/cli/src/modules/instance-reporting.ee/instance-reporting.service.ts
diff --git a/packages/cli/src/modules/instance-reporting/instance-reporting.module.ts b/packages/cli/src/modules/instance-reporting/instance-reporting.module.ts
deleted file mode 100644
index 67a65484ff9..00000000000
--- a/packages/cli/src/modules/instance-reporting/instance-reporting.module.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { ModuleInterface } from '@n8n/decorators';
-import { BackendModule } from '@n8n/decorators';
-import { Container } from '@n8n/di';
-
-/**
- * Reports this instance's billable execution numbers to a central monitoring
- * receiver, once a day.
- *
- * Opt-in: not a default module, so it runs only when an operator lists it in
- * `N8N_ENABLED_MODULES`. Main-only, and within a multi-main deployment only the
- * leader holds the timer, so a cluster reports once rather than once per main.
- *
- * The daily figure comes from the insights module, but the receiver only sees
- * data points, so that source is an implementation detail of
- * `InstanceReportingService` rather than part of the reporting contract.
- */
-@BackendModule({ name: 'instance-reporting', instanceTypes: ['main'] })
-export class InstanceReportingModule implements ModuleInterface {
- async init() {
- const { InstanceReportingScheduler } = await import(
- './instance-reporting-scheduler.service.js'
- );
-
- await Container.get(InstanceReportingScheduler).init();
- }
-
- async entities() {
- const { InstanceMonitoringReport } = await import(
- './database/entities/instance-monitoring-report.js'
- );
-
- return [InstanceMonitoringReport];
- }
-}
diff --git a/packages/frontend/@n8n/rest-api-client/src/api/index.ts b/packages/frontend/@n8n/rest-api-client/src/api/index.ts
index 8dc81622d82..e2e1a160b73 100644
--- a/packages/frontend/@n8n/rest-api-client/src/api/index.ts
+++ b/packages/frontend/@n8n/rest-api-client/src/api/index.ts
@@ -6,6 +6,7 @@ export * from './credentialResolvers';
export * from './ctas';
export * from './eventbus.ee';
export * from './instance-registry';
+export * from './instance-reporting';
export * from './instance-version-history';
export * from './events';
export * from './externalSecrets.ee';
diff --git a/packages/frontend/@n8n/rest-api-client/src/api/instance-reporting.ts b/packages/frontend/@n8n/rest-api-client/src/api/instance-reporting.ts
new file mode 100644
index 00000000000..cc498144625
--- /dev/null
+++ b/packages/frontend/@n8n/rest-api-client/src/api/instance-reporting.ts
@@ -0,0 +1,10 @@
+import type { InstanceReportingStatus } from '@n8n/api-types';
+
+import type { IRestApiContext } from '../types';
+import { makeRestApiRequest } from '../utils';
+
+export async function getInstanceReportingStatus(
+ context: IRestApiContext,
+): Promise {
+ return await makeRestApiRequest(context, 'GET', '/instance-reporting/status');
+}
diff --git a/scripts/lint-parity/samples.json b/scripts/lint-parity/samples.json
new file mode 100644
index 00000000000..004b8c9d40f
--- /dev/null
+++ b/scripts/lint-parity/samples.json
@@ -0,0 +1,398 @@
+{
+ "packages/@n8n/agents": [
+ "src/__tests__/agent-configuration.test.ts",
+ "src/__tests__/integration/agent-vector-store-pinecone.test.ts",
+ "src/evals/categorization.ts"
+ ],
+ "packages/@n8n/ai-node-sdk": ["src/index.ts"],
+ "packages/@n8n/ai-utilities": [
+ "integration-tests/openai.test.ts",
+ "src/__tests__/adapters/langchain-chat-model.test.ts",
+ "src/adapters/langchain-chat-model.ts"
+ ],
+ "packages/@n8n/ai-workflow-builder.ee": [
+ "evaluations/__tests__/argument-parser.test.ts",
+ "src/agents/planner.agent.ts",
+ "src/test/ai-workflow-builder-agent.service.test.ts"
+ ],
+ "packages/@n8n/api-types": [
+ "src/__tests__/agent-builder-interactive.test.ts",
+ "src/agent-builder-interactive.ts"
+ ],
+ "packages/@n8n/backend-common": ["src/__tests__/cli-parser.test.ts", "src/cli-parser.ts"],
+ "packages/@n8n/backend-network": [
+ "src/dns/__tests__/dns-resolver.test.ts",
+ "src/dns/dns-resolver.ts"
+ ],
+ "packages/@n8n/backend-test-utils": ["src/db/projects.ts"],
+ "packages/@n8n/benchmark": ["src/commands/list.ts"],
+ "packages/@n8n/blob-storage": [
+ "src/__tests__/azure-byte-store.test.ts",
+ "src/azure-blob/azure-blob.config.ts"
+ ],
+ "packages/@n8n/chat-hub": ["src/artifact.test.ts", "src/artifact.ts"],
+ "packages/@n8n/cli": [
+ "src/__tests__/client.test.ts",
+ "src/base-command.ts",
+ "src/client.ts",
+ "src/commands/login.ts",
+ "src/index.ts"
+ ],
+ "packages/@n8n/client-oauth2": ["src/client-oauth2-token.ts", "test/client-oauth2.test.ts"],
+ "packages/@n8n/codemirror-lang-sql": ["src/complete.ts", "test/complete.test.ts"],
+ "packages/@n8n/codemirror-lang": [
+ "src/expressions/grammar.terms.ts",
+ "test/expressions/expressions.test.ts"
+ ],
+ "packages/@n8n/computer-use": [
+ "src/__mocks__/@inquirer/prompts.ts",
+ "src/config-templates.test.ts"
+ ],
+ "packages/@n8n/config": [
+ "src/configs/__tests__/ai.config.test.ts",
+ "src/configs/activity-log.config.ts"
+ ],
+ "packages/@n8n/constants": ["src/api.ts"],
+ "packages/@n8n/crdt": [
+ "src/__tests__/helpers.ts",
+ "src/awareness/awareness.test.ts",
+ "src/awareness/index.ts"
+ ],
+ "packages/@n8n/db": [
+ "src/connection/__tests__/backoff.test.ts",
+ "src/connection/backoff.ts",
+ "src/migrations/__tests__/migration-helpers.ts"
+ ],
+ "packages/@n8n/decorators": [
+ "src/__tests__/memoized.test.ts",
+ "src/auth-handler/auth-handler-metadata.ts"
+ ],
+ "packages/@n8n/di": ["src/__tests__/circular-depedency.test.ts", "src/di.ts"],
+ "packages/@n8n/engine": [
+ "src/admittance/__tests__/allow-all-admittance.test.ts",
+ "src/admittance/admittance-rejected.error.ts"
+ ],
+ "packages/@n8n/errors": ["src/application.error.ts"],
+ "packages/@n8n/eslint-plugin-community-nodes": [
+ "src/plugin.ts",
+ "src/rules/ai-node-package-json.test.ts"
+ ],
+ "packages/@n8n/extension-sdk": ["src/backend/define.ts"],
+ "packages/@n8n/imap": ["src/connection-options.test.ts", "src/connection-options.ts"],
+ "packages/@n8n/instance-ai": [
+ "evaluations/__tests__/agent-report-rendering.test.ts",
+ "evaluations/computer-use/report-html.ts",
+ "src/__tests__/binary-checks.test.ts",
+ "src/agent/aia-model-defaults.ts",
+ "src/tools/__tests__/activity.tool.test.ts"
+ ],
+ "packages/@n8n/json-schema-to-zod": ["src/index.ts", "test/json-schema-to-zod.test.ts"],
+ "packages/@n8n/local-gateway": ["src/main/connect-origin.test.ts", "src/main/connect-origin.ts"],
+ "packages/@n8n/mcp-apps": [
+ "src/apps-manifest.ts",
+ "src/apps/workflow-preview/App.vue",
+ "src/apps/workflow-preview/composables/use-workflow-preview.test.ts"
+ ],
+ "packages/@n8n/mcp-browser-extension": [
+ "src/approvedHosts.test.ts",
+ "src/approvedHosts.ts",
+ "src/ui/App.vue"
+ ],
+ "packages/@n8n/mcp-browser": [
+ "src/__tests__/cdp-relay-attach.test.ts",
+ "src/adapters/agent-browser.ts"
+ ],
+ "packages/@n8n/node-cli": [
+ "src/commands/build.test.ts",
+ "src/commands/build.ts",
+ "src/configs/eslint.ts",
+ "src/modules.d.ts",
+ "src/test-utils/command-tester.ts"
+ ],
+ "packages/@n8n/node-engine-compatibility": [
+ "src/__tests__/acceptance-fixtures.ts",
+ "src/__tests__/engine-step-data-loader.test.ts",
+ "src/constants.ts"
+ ],
+ "packages/@n8n/nodes-langchain": [
+ "credentials/AlibabaCloudApi.credentials.ts",
+ "credentials/test/AnthropicApi.credentials.test.ts",
+ "nodes/Guardrails/Guardrails.node.ts",
+ "nodes/Guardrails/helpers/__tests__/model.test.ts",
+ "nodes/mcp/McpClient/__test__/McpClient.node.test.ts"
+ ],
+ "packages/@n8n/permissions": ["src/__tests__/schemas.test.ts", "src/constants.ee.ts"],
+ "packages/@n8n/scheduler": ["src/__tests__/dependency-purity.test.ts", "src/core/clock-skew.ts"],
+ "packages/@n8n/syslog-client": ["src/client.ts", "test/client.test.ts", "vite.config.ts"],
+ "packages/@n8n/task-runner": [
+ "src/__tests__/node-types.test.ts",
+ "src/config/base-runner-config.ts"
+ ],
+ "packages/@n8n/telemetry": ["src/__tests__/catalog.test.ts", "src/catalog.ts"],
+ "packages/@n8n/tournament": ["src/Constants.ts", "test/ASTHooks.test.ts"],
+ "packages/@n8n/utils": [
+ "src/__tests__/setup.ts",
+ "src/assert.ts",
+ "src/client-assertion.test.ts"
+ ],
+ "packages/@n8n/workflow-sdk": [
+ "src/__tests__/fixtures-download.ts",
+ "src/ast-interpreter/errors.ts",
+ "src/ast-interpreter/interpreter.test.ts",
+ "src/prompts/best-practices/guides/chatbot.ts",
+ "src/prompts/node-guidance/parameter-guides/embedding-nodes.ts"
+ ],
+ "packages/cli": [
+ "src/__tests__/abstract-server.test.ts",
+ "src/abstract-server.ts",
+ "src/active-workflow-manager.ts",
+ "src/binary-data/database.manager.ts",
+ "src/commands/db/revert.ts",
+ "src/commands/import/credentials.ts",
+ "src/commands/ldap/reset.ts",
+ "src/controllers/project.controller.ts",
+ "src/credentials-helper.ts",
+ "src/credentials/credential-connection-status-provider.interface.ts",
+ "src/credentials/credential-connection-status-proxy.ts",
+ "src/credentials/credential-dependency.service.ts",
+ "src/credentials/credentials-finder.service.ts",
+ "src/credentials/credentials.controller.ts",
+ "src/credentials/credentials.service.ee.ts",
+ "src/credentials/credentials.service.ts",
+ "src/databases/repositories/__tests__/credentials.repository.test.ts",
+ "src/evaluation.ee/evaluation-collection.service.ts",
+ "src/evaluation.ee/test-runner/test-runner.service.ee.ts",
+ "src/eventbus/message-event-bus/message-event-bus.ts",
+ "src/events/relays/telemetry.event-relay.ts",
+ "src/executions/execution-data/db-store.ts",
+ "src/executions/execution-persistence.ts",
+ "src/executions/execution-recovery.service.ts",
+ "src/executions/execution.service.ts",
+ "src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts",
+ "src/license.ts",
+ "src/metrics/prometheus/instance-role-metrics.service.ts",
+ "src/modules/agent-evals/agent-evals.module.ts",
+ "src/modules/agents/agent-knowledge.service.ts",
+ "src/modules/agents/agent-publish.service.ts",
+ "src/modules/agents/agent-task.service.ts",
+ "src/modules/agents/agents.service.ts",
+ "src/modules/agents/builder/agents-builder.service.ts",
+ "src/modules/agents/entities/agent-background-job.entity.ts",
+ "src/modules/agents/instance-ai-builder-delegate.adapter.ts",
+ "src/modules/agents/integrations/agent-channel-reconciler.service.ts",
+ "src/modules/agents/integrations/leader-channel-relay.service.ts",
+ "src/modules/agents/integrations/n8n-memory.ts",
+ "src/modules/agents/integrations/platforms/discord-integration.ts",
+ "src/modules/agents/repositories/agent-background-job.repository.ts",
+ "src/modules/agents/tools/workflow-tool-workflow-resolver.ts",
+ "src/modules/breaking-changes/breaking-changes.service.ts",
+ "src/modules/chat-hub/chat-hub-agent.service.ts",
+ "src/modules/chat-hub/chat-hub-credentials.service.ts",
+ "src/modules/chat-hub/chat-hub-title.service.ts",
+ "src/modules/chat-hub/chat-hub-tool.service.ts",
+ "src/modules/chat-hub/chat-hub-workflow.service.ts",
+ "src/modules/chat-hub/chat-hub.attachment.service.ts",
+ "src/modules/chat-hub/chat-hub.models.service.ts",
+ "src/modules/chat-hub/chat-hub.service.ts",
+ "src/modules/chat-hub/chat-hub.settings.service.ts",
+ "src/modules/data-table/data-table-ddl.service.ts",
+ "src/modules/data-table/data-table.service.ts",
+ "src/modules/data-table/utils/sql-utils.ts",
+ "src/modules/dynamic-credentials.ee/database/entities/credential-resolver.ts",
+ "src/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository.ts",
+ "src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts",
+ "src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts",
+ "src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts",
+ "src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts",
+ "src/modules/favorites/favorites.service.ts",
+ "src/modules/insights/insights-collection.service.ts",
+ "src/modules/instance-ai/__tests__/agent-preview-handoff.test.ts",
+ "src/modules/instance-ai/instance-ai.adapter.service.ts",
+ "src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts",
+ "src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts",
+ "src/modules/instance-ai/storage/typeorm-agent-memory.ts",
+ "src/modules/instance-ai/storage/typeorm-observation-log-store.ts",
+ "src/modules/instance-ai/suspended-thread-persistence.service.ts",
+ "src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts",
+ "src/modules/log-streaming.ee/log-streaming-destination.service.ts",
+ "src/modules/mcp/mcp-api-key.service.ts",
+ "src/modules/mcp/mcp.settings.service.ts",
+ "src/modules/oauth-jwe/oauth-jwe-key.service.ts",
+ "src/modules/provisioning.ee/provisioning.service.ee.ts",
+ "src/modules/provisioning.ee/role-mapping-rule.service.ee.ts",
+ "src/modules/provisioning.ee/role-resolver.service.ee.ts",
+ "src/modules/source-control.ee/source-control-context.factory.ts",
+ "src/modules/source-control.ee/source-control-export.service.ee.ts",
+ "src/modules/source-control.ee/source-control-import.service.ee.ts",
+ "src/modules/source-control.ee/source-control-scoped.service.ts",
+ "src/modules/source-control.ee/source-control-status.service.ee.ts",
+ "src/modules/token-exchange/services/trusted-key.service.ts",
+ "src/modules/workflow-index/workflow-dependency-query.service.ts",
+ "src/permissions.ee/check-access.ts",
+ "src/policy/policy-enforcement.service.ts",
+ "src/public-api/v1/controllers/__tests__/workflows.public.controller.test.ts",
+ "src/public-api/v1/handlers/audit/audit.handler.ts",
+ "src/public-api/v1/handlers/community-packages/community-packages.handler.ts",
+ "src/public-api/v1/handlers/credentials/credentials.handler.ts",
+ "src/public-api/v1/handlers/data-tables/data-tables.columns.handler.ts",
+ "src/public-api/v1/handlers/data-tables/data-tables.handler.ts",
+ "src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts",
+ "src/public-api/v1/handlers/discover/discover.handler.ts",
+ "src/public-api/v1/handlers/evaluations/evaluations.handler.ts",
+ "src/public-api/v1/handlers/folders/folders.handler.ts",
+ "src/public-api/v1/handlers/insights/insights.handler.ts",
+ "src/public-api/v1/handlers/ldap/ldap.handler.ts",
+ "src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts",
+ "src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts",
+ "src/public-api/v1/handlers/otel/otel.handler.ts",
+ "src/public-api/v1/handlers/projects/projects.handler.ts",
+ "src/public-api/v1/handlers/security-policy/security-policy.handler.ts",
+ "src/public-api/v1/handlers/sso-oidc/sso-oidc.handler.ts",
+ "src/public-api/v1/handlers/sso-saml/sso-saml.handler.ts",
+ "src/public-api/v1/handlers/tags/tags.handler.ts",
+ "src/public-api/v1/handlers/users/users.handler.ee.ts",
+ "src/public-api/v1/handlers/variables/variables.handler.ts",
+ "src/public-api/v1/handlers/workflows/workflows.handler.ts",
+ "src/scaling/scaling.service.ts",
+ "src/scheduling/durable-job-provisioner.ts",
+ "src/scheduling/durable-scheduler.ts",
+ "src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts",
+ "src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts",
+ "src/scheduling/system-tasks/system-task-runner.ts",
+ "src/security-audit/risk-reporters/credentials-risk-reporter.ts",
+ "src/security-audit/security-audit.repository.ts",
+ "src/services/export.service.ts",
+ "src/services/folder-finder.service.ts",
+ "src/services/folder.service.ts",
+ "src/services/hooks.service.ts",
+ "src/services/import.service.ts",
+ "src/services/ownership-transfer/ownership-transfer-handler.registry.ts",
+ "src/services/ownership.service.ts",
+ "src/services/project.service.ee.ts",
+ "src/services/pruning/executions-pruning.service.ts",
+ "src/services/pruning/workflow-history-compaction.service.ts",
+ "src/services/public-api-key.service.ts",
+ "src/services/role-cache.service.ts",
+ "src/services/role.service.ts",
+ "src/services/tag.service.ts",
+ "src/services/user.service.ts",
+ "src/services/workflow-statistics-rollup.service.ts",
+ "src/wait-tracker.ts",
+ "src/workflows/publication/workflow-publication-outbox-consumer.ts",
+ "src/workflows/publication/workflow-publication-reconciler.service.ts",
+ "src/workflows/workflow-creation.service.ts",
+ "src/workflows/workflow-finder.service.ts",
+ "src/workflows/workflow-history/workflow-history.service.ts",
+ "src/workflows/workflow-sharing.service.ts",
+ "src/workflows/workflow-validation.service.ts",
+ "src/workflows/workflow.service.ee.ts",
+ "src/workflows/workflow.service.ts",
+ "src/workflows/workflows.controller.ts",
+ "test/extend-expect.ts"
+ ],
+ "packages/core": [
+ "src/__tests__/credentials.test.ts",
+ "src/binary-data/binary-data.config.ts",
+ "test/helpers/constants.ts"
+ ],
+ "packages/extensions/insights": [
+ "src/backend/index.ts",
+ "src/frontend/InsightsDashboard.vue",
+ "src/frontend/index.ts"
+ ],
+ "packages/frontend/@n8n/chat": [
+ "src/App.vue",
+ "src/__stories__/App.stories.ts",
+ "src/__tests__/Chat.spec.ts",
+ "src/__tests__/plugins/chat.test.ts"
+ ],
+ "packages/frontend/@n8n/composables": [
+ "src/__tests__/packageBoundary.test.ts",
+ "src/injectionKeys.ts"
+ ],
+ "packages/frontend/@n8n/design-system": [
+ "src/__tests__/render.ts",
+ "src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts",
+ "src/components/AskAssistantAvatar/AssistantAvatar.stories.ts",
+ "src/components/AskAssistantAvatar/AssistantAvatar.vue",
+ "src/components/N8nFormInput/validators.ts"
+ ],
+ "packages/frontend/@n8n/eslint-plugin-design-system": [
+ "src/plugin.ts",
+ "src/rules/require-teleported-tooltip-in-dropdown.test.ts"
+ ],
+ "packages/frontend/@n8n/frontend-constants": ["src/urls.ts", "src/views.test.ts", "src/views.ts"],
+ "packages/frontend/@n8n/frontend-module-sdk": [
+ "src/__tests__/setup.ts",
+ "src/index.ts",
+ "src/registries/commandRegistry.test.ts"
+ ],
+ "packages/frontend/@n8n/frontend-test-utils": ["src/__tests__/render.test.ts", "src/defaults.ts"],
+ "packages/frontend/@n8n/frontend-utils": [
+ "src/__tests__/setup.ts",
+ "src/constants/sanitization.ts",
+ "src/htmlUtils.test.ts"
+ ],
+ "packages/frontend/@n8n/i18n": ["src/__tests__/setup.ts", "src/index.test.ts", "src/index.ts"],
+ "packages/frontend/@n8n/rest-api-client": [
+ "src/__tests__/setup.ts",
+ "src/api/__tests__/api-keys.test.ts",
+ "src/api/ai-usage.ts"
+ ],
+ "packages/frontend/@n8n/stores": [
+ "src/__tests__/metaTagConfig.test.ts",
+ "src/cloudPlan.store.ts"
+ ],
+ "packages/frontend/@n8n/storybook": [".storybook/ThemedDocsContainer.ts"],
+ "packages/frontend/editor-ui": [
+ "src/Interface.ts",
+ "src/__tests__/data/users.ts",
+ "src/app/App.vue",
+ "src/app/__tests__/modal-definition.test.ts",
+ "src/app/constants/modals.ts",
+ "src/app/stores/defaults/modals.ts",
+ "src/app/stores/workflowDocument/types.ts",
+ "src/app/stores/workflows.store.ts",
+ "src/features/agents/__tests__/AgentAdvancedPanel.test.ts",
+ "src/features/agents/channels/AgentChannelStandardEditView.vue",
+ "src/features/ai/chatHub/__test__/data.ts",
+ "src/features/settings/environments.ee/completions/variables.completions.test.ts",
+ "src/features/shared/editors/components/CodeNodeEditor/CodeNodeEditor.vue",
+ "src/features/shared/editors/plugins/codemirror/completions/__tests__/mock.ts",
+ "src/features/shared/toolsConnection/ToolsConnectionModal.stories.ts"
+ ],
+ "packages/modules/insights/frontend": [
+ "src/__tests__/setup.ts",
+ "src/chartjs.utils.ts",
+ "src/components/InsightsDashboard.test.ts",
+ "src/components/InsightsDashboard.vue"
+ ],
+ "packages/modules/instance-registry/frontend": [
+ "src/__tests__/setup.ts",
+ "src/index.ts",
+ "src/instance-registry.store.test.ts"
+ ],
+ "packages/modules/otel/frontend": [
+ "src/OtelSettingsRow.vue",
+ "src/SettingsOpenTelemetryView.test.ts",
+ "src/__tests__/setup.ts",
+ "src/index.ts"
+ ],
+ "packages/node-dev": ["src/Build.ts"],
+ "packages/nodes-base": [
+ "credentials/ActionNetworkApi.credentials.ts",
+ "credentials/common/aws/s3-path-signing.test.ts",
+ "credentials/test/AtlassianOAuth2Api.credentials.test.ts",
+ "nodes/ActionNetwork/ActionNetwork.node.ts",
+ "nodes/Aws/__tests__/AwsLambda.node.test.ts"
+ ],
+ "packages/testing/code-health": ["src/cli.ts", "src/cli/arg-parser.test.ts"],
+ "packages/testing/janitor": ["src/cli.test.ts", "src/cli.ts"],
+ "packages/testing/playwright": ["coverage-options.test.ts"],
+ "packages/testing/rules-engine": ["src/ast/ast-rule.ts", "src/ast/imports.test.ts"],
+ "packages/testing/test-impact": ["src/changes.test.ts", "src/changes.ts"],
+ "packages/workflow": [
+ "src/augment-object.ts",
+ "test/ExpressionExtensions/array-extensions.test.ts"
+ ]
+}