n8n
Version:
n8n Workflow Automation Tool
327 lines • 13.9 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentScheduleService = void 0;
const api_types_1 = require("@n8n/api-types");
const db_1 = require("@n8n/db");
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const decorators_1 = require("@n8n/decorators");
const di_1 = require("@n8n/di");
const cron_1 = require("cron");
const crypto_1 = require("crypto");
const luxon_1 = require("luxon");
const bad_request_error_1 = require("../../../errors/response-errors/bad-request.error");
const conflict_error_1 = require("../../../errors/response-errors/conflict.error");
const agents_service_1 = require("../agents.service");
const agent_repository_1 = require("../repositories/agent.repository");
const cron_validation_1 = require("./cron-validation");
let AgentScheduleService = class AgentScheduleService {
constructor(logger, globalConfig, agentRepository, agentsService, projectRelationRepository) {
this.logger = logger;
this.globalConfig = globalConfig;
this.agentRepository = agentRepository;
this.agentsService = agentsService;
this.projectRelationRepository = projectRelationRepository;
this.jobs = new Map();
}
getConfig(agent) {
const integration = this.getScheduleIntegration(agent);
return {
active: integration?.active ?? false,
cronExpression: integration?.cronExpression ?? '',
wakeUpPrompt: integration?.wakeUpPrompt ?? api_types_1.DEFAULT_AGENT_SCHEDULE_WAKE_UP_PROMPT,
};
}
async saveConfig(agent, cronExpression, wakeUpPrompt) {
const existing = this.getScheduleIntegration(agent);
const normalizedCronExpression = cronExpression.trim();
const nextWakeUpPrompt = wakeUpPrompt ?? existing?.wakeUpPrompt ?? api_types_1.DEFAULT_AGENT_SCHEDULE_WAKE_UP_PROMPT;
if (normalizedCronExpression !== '') {
this.assertCronExpressionIsValid(normalizedCronExpression);
}
else if (existing?.active) {
throw new bad_request_error_1.BadRequestError('Cron expression is required while the schedule is active');
}
const saved = await this.saveScheduleIntegration(agent, {
type: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE,
active: existing?.active ?? false,
cronExpression: normalizedCronExpression,
wakeUpPrompt: nextWakeUpPrompt,
});
if (this.getScheduleIntegration(saved)?.active) {
await this.registerOrRefresh(saved);
}
this.logger.debug('[AgentScheduleService] Saved schedule config', {
agentId: agent.id,
projectId: agent.projectId,
active: saved.integrations.find(api_types_1.isAgentScheduleIntegration)?.active ?? false,
cronExpression: normalizedCronExpression || null,
});
return this.getConfig(saved);
}
async activate(agent) {
if (!agent.publishedVersion) {
throw new conflict_error_1.ConflictError(`Agent "${agent.id}" must be published before activating the schedule trigger`);
}
const existing = this.getScheduleIntegration(agent);
const cronExpression = existing?.cronExpression.trim() ?? '';
if (cronExpression === '') {
throw new bad_request_error_1.BadRequestError('Cron expression is required before activation');
}
this.assertCronExpressionIsValid(cronExpression);
const saved = await this.saveScheduleIntegration(agent, {
type: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE,
active: true,
cronExpression,
wakeUpPrompt: existing?.wakeUpPrompt ?? api_types_1.DEFAULT_AGENT_SCHEDULE_WAKE_UP_PROMPT,
});
await this.registerOrRefresh(saved);
this.logger.info('[AgentScheduleService] Activated schedule trigger', {
agentId: agent.id,
projectId: agent.projectId,
cronExpression,
});
return this.getConfig(saved);
}
async deactivate(agent) {
const existing = this.getScheduleIntegration(agent);
if (!existing) {
this.deregister(agent.id);
return this.getConfig(agent);
}
const saved = await this.saveScheduleIntegration(agent, {
...existing,
active: false,
});
this.deregister(agent.id);
this.logger.info('[AgentScheduleService] Deactivated schedule trigger', {
agentId: agent.id,
projectId: agent.projectId,
});
return this.getConfig(saved);
}
async reconnectAll() {
const agents = await this.agentRepository.findPublished();
this.logger.debug('[AgentScheduleService] Reconnecting active schedules', {
publishedAgentCount: agents.length,
});
for (const agent of agents) {
const schedule = this.getScheduleIntegration(agent);
if (!schedule?.active) {
continue;
}
try {
await this.registerOrRefresh(agent);
}
catch (error) {
this.logger.error('[AgentScheduleService] Failed to reconnect agent schedule', {
agentId: agent.id,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
async applyConfig(agent) {
const schedule = this.getScheduleIntegration(agent);
if (schedule?.active) {
await this.registerOrRefresh(agent);
}
else {
this.deregister(agent.id);
}
}
async registerOrRefresh(agent) {
const schedule = this.getScheduleIntegration(agent);
if (!schedule?.active) {
this.logger.debug('[AgentScheduleService] Skipping schedule registration for inactive agent', {
agentId: agent.id,
projectId: agent.projectId,
});
this.deregister(agent.id);
return;
}
if (!agent.publishedVersion) {
this.logger.warn('[AgentScheduleService] Skipping schedule registration for unpublished agent', {
agentId: agent.id,
projectId: agent.projectId,
});
this.deregister(agent.id);
return;
}
this.assertCronExpressionIsValid(schedule.cronExpression);
this.deregister(agent.id);
const timezone = this.globalConfig.generic.timezone;
const job = new cron_1.CronJob(schedule.cronExpression, () => {
void this.runScheduled(agent.id);
}, null, false, timezone);
job.start();
this.jobs.set(agent.id, job);
this.logger.info('[AgentScheduleService] Registered schedule trigger', {
agentId: agent.id,
projectId: agent.projectId,
cronExpression: schedule.cronExpression,
timezone,
});
}
deregister(agentId) {
const existing = this.jobs.get(agentId);
if (!existing) {
return;
}
void existing.stop();
this.jobs.delete(agentId);
this.logger.info('[AgentScheduleService] Deregistered schedule trigger', { agentId });
}
stopAll() {
for (const [agentId] of this.jobs) {
this.deregister(agentId);
}
}
getScheduleIntegration(agent) {
return (agent.integrations ?? []).find(api_types_1.isAgentScheduleIntegration);
}
async saveScheduleIntegration(agent, schedule) {
agent.integrations = [
...(agent.integrations ?? []).filter((integration) => !(0, api_types_1.isAgentScheduleIntegration)(integration)),
schedule,
];
return await this.agentRepository.save(agent);
}
assertCronExpressionIsValid(cronExpression) {
if (!(0, cron_validation_1.isValidCronExpression)(cronExpression)) {
throw new bad_request_error_1.BadRequestError('Invalid cron expression');
}
}
async runScheduled(agentId) {
let projectId;
let threadId;
let cronExpression;
const startedAt = Date.now();
try {
const agent = await this.agentRepository.findOne({
where: { id: agentId },
relations: { publishedVersion: true },
});
if (!agent) {
this.logger.warn('[AgentScheduleService] Scheduled trigger fired for missing agent', {
agentId,
});
this.deregister(agentId);
return;
}
projectId = agent.projectId;
const schedule = this.getScheduleIntegration(agent);
cronExpression = schedule?.cronExpression;
if (!agent.publishedVersion) {
this.logger.warn('[AgentScheduleService] Scheduled trigger fired for unpublished agent', {
agentId,
projectId,
});
this.deregister(agentId);
return;
}
if (!schedule?.active) {
this.logger.warn('[AgentScheduleService] Scheduled trigger fired for inactive agent', {
agentId,
projectId,
});
this.deregister(agentId);
return;
}
const executionUserId = await this.resolveExecutionUserId(agent);
if (!executionUserId) {
this.logger.warn('[AgentScheduleService] No project member available for scheduled run', {
agentId,
projectId: agent.projectId,
});
return;
}
threadId = `schedule-${agentId}-${(0, crypto_1.randomUUID)()}`;
const timezone = this.globalConfig.generic.timezone;
const timestamp = luxon_1.DateTime.now().setZone(timezone).toISO() ?? new Date().toISOString();
const message = `${schedule.wakeUpPrompt}\n\nCurrent date and time: ${timestamp} (timezone: ${timezone})`;
this.logger.info('[AgentScheduleService] Scheduled trigger fired', {
agentId,
projectId,
threadId,
cronExpression: schedule.cronExpression,
timezone,
});
this.logger.debug('[AgentScheduleService] Starting scheduled agent run', {
agentId,
projectId,
threadId,
messageLength: message.length,
});
let chunkCount = 0;
for await (const _chunk of this.agentsService.executeForSchedulePublished({
agentId: agent.id,
projectId: agent.projectId,
message,
memory: { threadId, resourceId: executionUserId },
})) {
chunkCount += 1;
}
this.logger.info('[AgentScheduleService] Scheduled agent run completed', {
agentId,
projectId,
threadId,
chunkCount,
durationMs: Date.now() - startedAt,
});
}
catch (error) {
this.logger.error('[AgentScheduleService] Scheduled agent run failed', {
agentId,
projectId,
threadId,
cronExpression,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
}
}
async resolveExecutionUserId(agent) {
const userIds = await this.projectRelationRepository.findUserIdsByProjectId(agent.projectId);
if (userIds.length === 0) {
return undefined;
}
const publishedById = agent.publishedVersion?.publishedById;
if (publishedById && userIds.includes(publishedById)) {
return publishedById;
}
return undefined;
}
};
exports.AgentScheduleService = AgentScheduleService;
__decorate([
(0, decorators_1.OnLeaderTakeover)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AgentScheduleService.prototype, "reconnectAll", null);
__decorate([
(0, decorators_1.OnLeaderStepdown)(),
(0, decorators_1.OnShutdown)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], AgentScheduleService.prototype, "stopAll", null);
exports.AgentScheduleService = AgentScheduleService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
config_1.GlobalConfig,
agent_repository_1.AgentRepository,
agents_service_1.AgentsService,
db_1.ProjectRelationRepository])
], AgentScheduleService);
//# sourceMappingURL=agent-schedule.service.js.map