@mastra/core
Version:
515 lines (512 loc) • 20.1 kB
JavaScript
import { MastraError, ErrorCategory, ErrorDomain } from './chunk-M7RBQNFP.js';
import { z } from 'zod/v4';
import { randomUUID } from 'crypto';
import slugify from '@sindresorhus/slugify';
import { Cron } from 'croner';
var AGENT_SCHEDULE_PREFIX = "agent_";
var WORKFLOW_SCHEDULE_PREFIX = "schedule_";
var ScheduleAttributesSchema = z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]));
var ScheduleStreamOptionsSchema = z.object({
requestContext: z.record(z.string(), z.unknown()).optional()
});
var ScheduleIfActiveSchema = z.object({
behavior: z.enum(["deliver", "persist", "discard"]).optional(),
attributes: ScheduleAttributesSchema.optional()
});
var ScheduleIfIdleSchema = z.object({
behavior: z.enum(["wake", "persist", "discard"]).optional(),
attributes: ScheduleAttributesSchema.optional(),
streamOptions: ScheduleStreamOptionsSchema.optional()
});
var ScheduleInputSchema = z.object({
scheduleId: z.string(),
agentId: z.string(),
prompt: z.string(),
threadId: z.string().optional(),
resourceId: z.string().optional(),
signalType: z.enum(["user", "state", "reactive", "notification", "user-message", "system-reminder"]).optional(),
/**
* XML tag name the signal renders as. Defaults to `schedule`, so a fire
* surfaces to the agent as `<schedule>…</schedule>`. Override to render a
* different tag.
*/
tagName: z.string().optional(),
/** Attributes rendered onto the signal's XML tag. */
attributes: ScheduleAttributesSchema.optional(),
/**
* Provider options merged into the schedule signal payload on every fire.
* Stored as a plain JSON object (`MastraProviderMetadata` is JSON-safe) and
* applied regardless of `ifActive` / `ifIdle`.
*/
providerOptions: z.record(z.string(), z.unknown()).optional(),
ifActive: ScheduleIfActiveSchema.optional(),
ifIdle: ScheduleIfIdleSchema.optional()
});
var ScheduleOutputSchema = z.object({
status: z.enum([
"fired",
"signal-accepted",
"skipped-thread-blocked",
"thread-missing",
"agent-missing",
"invalid-input"
]),
reason: z.string().optional()
});
function validateCron(cron, timezone) {
if (typeof cron !== "string" || cron.trim() === "") {
throw new Error(
`Invalid cron expression: expected a non-empty cron string (e.g. "0 * * * *"), but received ${cron === void 0 ? "undefined" : JSON.stringify(cron)}.`
);
}
let job;
try {
job = new Cron(cron);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid cron expression "${cron}": ${reason}`);
}
if (timezone !== void 0) {
try {
new Cron(cron, { timezone }).nextRun();
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid timezone "${timezone}": ${reason}`);
}
} else {
job.nextRun();
}
}
function computeNextFireAt(cron, options) {
const job = new Cron(cron, { timezone: options?.timezone });
const reference = options?.after !== void 0 ? new Date(options.after) : /* @__PURE__ */ new Date();
const next = job.nextRun(reference);
if (!next) {
throw new Error(`Cron expression "${cron}" has no future occurrence after ${reference.toISOString()}`);
}
return next.getTime();
}
// src/schedules/schedules.ts
var TOPIC_WORKFLOWS = "workflows";
function canonicalizeScheduleId(rawId, prefix) {
const trimmed = rawId.trim();
const withoutPrefix = trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed;
const slug = slugify(withoutPrefix);
if (!slug) return "";
return `${prefix}${slug}`;
}
function normalizeScheduleId(rawId, prefix) {
const canonical = canonicalizeScheduleId(rawId, prefix);
if (!canonical) {
throw new MastraError({
id: "SCHEDULES_INVALID_ID",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `schedules.create: id "${rawId}" is empty after normalization. Provide an id with at least one alphanumeric character.`
});
}
return canonical;
}
var Schedules = class {
#mastra;
constructor(mastra) {
this.#mastra = mastra;
}
async #getStore() {
const storage = this.#mastra.getStorage();
const store = await storage?.getStore("schedules");
if (!store) {
throw new MastraError({
id: "SCHEDULES_NO_SCHEDULES_STORAGE",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: "Schedules require a storage adapter that implements the schedules domain."
});
}
return store;
}
/**
* Resolve a caller-supplied id to a stored row. An id is first looked up
* verbatim (covering `agent_`, `schedule_`, `wf_`, and legacy `hb_` ids);
* when that misses, a bare caller id is canonicalized to `agent_<slug>` to
* match what agent-schedule `create` persisted.
*/
async #load(id) {
const store = await this.#getStore();
const trimmed = id.trim();
const exact = trimmed ? await store.getSchedule(trimmed) : null;
if (exact) return exact;
const canonical = canonicalizeScheduleId(trimmed, AGENT_SCHEDULE_PREFIX);
if (!canonical || canonical === trimmed) return null;
return store.getSchedule(canonical);
}
async create(input) {
if ("workflowId" in input && input.workflowId) {
return this.#createWorkflowSchedule(input);
}
return this.#createAgentSchedule(input);
}
async #createAgentSchedule(input) {
validateCron(input.cron, input.timezone);
if (!input.agentId) {
throw new MastraError({
id: "SCHEDULES_MISSING_TARGET_ID",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: "schedules.create requires `agentId` or `workflowId`."
});
}
if (input.threadId && !input.resourceId) {
throw new MastraError({
id: "SCHEDULES_MISSING_RESOURCE_ID",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: "schedules.create requires `resourceId` when `threadId` is set."
});
}
if (!input.threadId) {
const offenders = [];
if (input.signalType !== void 0) offenders.push("signalType");
if (input.ifActive !== void 0) offenders.push("ifActive");
if (input.ifIdle !== void 0) offenders.push("ifIdle");
if (input.resourceId !== void 0) offenders.push("resourceId");
if (offenders.length > 0) {
throw new MastraError({
id: "SCHEDULES_THREADLESS_OPTIONS",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `schedules.create: ${offenders.join(", ")} require a threadId.`
});
}
}
const store = await this.#getStore();
await this.#mastra.__ensureScheduleRuntimeReady();
const id = input.id !== void 0 ? normalizeScheduleId(input.id, AGENT_SCHEDULE_PREFIX) : `${AGENT_SCHEDULE_PREFIX}${randomUUID()}`;
await this.#assertIdAvailable(store, id, input.id !== void 0);
const now = Date.now();
const nextFireAt = computeNextFireAt(input.cron, { timezone: input.timezone, after: now });
const target = {
type: "agent",
agentId: input.agentId,
prompt: input.prompt,
...input.name !== void 0 ? { name: input.name } : {},
...input.threadId ? { threadId: input.threadId } : {},
...input.resourceId ? { resourceId: input.resourceId } : {},
...input.signalType ? { signalType: input.signalType } : {},
...input.tagName ? { tagName: input.tagName } : {},
...input.attributes ? { attributes: input.attributes } : {},
...input.providerOptions ? { providerOptions: input.providerOptions } : {},
...input.ifActive ? { ifActive: input.ifActive } : {},
...input.ifIdle ? { ifIdle: input.ifIdle } : {}
};
const schedule = {
id,
target,
cron: input.cron,
timezone: input.timezone,
status: input.status ?? "active",
nextFireAt,
createdAt: now,
updatedAt: now,
ownerType: "agent",
ownerId: input.agentId,
...input.metadata ? { metadata: input.metadata } : {}
};
const created = await store.createSchedule(schedule);
return toAgentSchedule(created);
}
async #createWorkflowSchedule(input) {
validateCron(input.cron, input.timezone);
const store = await this.#getStore();
await this.#mastra.__ensureScheduleRuntimeReady();
const id = input.id !== void 0 ? normalizeScheduleId(input.id, WORKFLOW_SCHEDULE_PREFIX) : `${WORKFLOW_SCHEDULE_PREFIX}${randomUUID()}`;
await this.#assertIdAvailable(store, id, input.id !== void 0);
const now = Date.now();
const nextFireAt = computeNextFireAt(input.cron, { timezone: input.timezone, after: now });
const target = {
type: "workflow",
workflowId: input.workflowId,
...input.inputData !== void 0 ? { inputData: input.inputData } : {},
...input.initialState !== void 0 ? { initialState: input.initialState } : {},
...input.requestContext !== void 0 ? { requestContext: input.requestContext } : {}
};
const schedule = {
id,
target,
cron: input.cron,
timezone: input.timezone,
status: input.status ?? "active",
nextFireAt,
createdAt: now,
updatedAt: now,
...input.metadata ? { metadata: input.metadata } : {}
};
const created = await store.createSchedule(schedule);
return toWorkflowSchedule(created);
}
async #assertIdAvailable(store, id, callerProvided) {
if (!callerProvided) return;
const existing = await store.getSchedule(id);
if (existing) {
throw new MastraError({
id: "SCHEDULES_ID_EXISTS",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `schedules.create: a schedule with id "${id}" already exists. Use update() to modify it or choose a different id.`
});
}
}
async get(id) {
const schedule = await this.#load(id);
if (!schedule) return null;
return toScheduleView(schedule);
}
async list(filter) {
const store = await this.#getStore();
const schedules = await store.listSchedules({
...filter?.agentId ? { ownerType: "agent", ownerId: filter.agentId } : {},
...filter?.workflowId ? { workflowId: filter.workflowId } : {},
...filter?.status ? { status: filter.status } : {}
});
const views = schedules.map(toScheduleView).filter((s) => s !== null).filter((s) => filter?.agentId ? s.agentId !== void 0 : true);
const agentOnly = filter?.threadId !== void 0 || filter?.resourceId !== void 0 || filter?.name !== void 0;
if (!agentOnly) return views;
return views.filter((s) => {
if (s.agentId === void 0) return false;
if (filter?.threadId !== void 0 && s.threadId !== filter.threadId) return false;
if (filter?.resourceId !== void 0 && s.resourceId !== filter.resourceId) return false;
if (filter?.name !== void 0 && s.name !== filter.name) return false;
return true;
});
}
async update(id, patch) {
const store = await this.#getStore();
const existing = await this.#load(id);
if (!existing) {
throw new MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
}
const nextCron = patch.cron ?? existing.cron;
const nextTimezone = patch.timezone !== void 0 ? patch.timezone : existing.timezone;
if (patch.cron !== void 0 || patch.timezone !== void 0) {
validateCron(nextCron, nextTimezone);
}
const nextTarget = existing.target.type === "agent" ? this.#patchAgentTarget(existing.target, patch) : this.#patchWorkflowTarget(existing.target, patch);
const resuming = patch.status === "active" && existing.status === "paused";
const nextFireAt = patch.cron !== void 0 || patch.timezone !== void 0 || resuming ? computeNextFireAt(nextCron, { timezone: nextTimezone, after: Date.now() }) : void 0;
const updated = await store.updateSchedule(existing.id, {
...patch.cron !== void 0 ? { cron: patch.cron } : {},
...patch.timezone !== void 0 ? { timezone: patch.timezone } : {},
target: nextTarget,
...nextFireAt !== void 0 ? { nextFireAt } : {},
...patch.metadata !== void 0 ? { metadata: patch.metadata } : {},
...patch.status !== void 0 ? { status: patch.status } : {}
});
return toScheduleView(updated);
}
#patchAgentTarget(existingTarget, patch) {
if (!existingTarget.threadId) {
const offenders = [];
if (patch.signalType !== void 0) offenders.push("signalType");
if (patch.ifActive !== void 0) offenders.push("ifActive");
if (patch.ifIdle !== void 0) offenders.push("ifIdle");
if (offenders.length > 0) {
throw new MastraError({
id: "SCHEDULES_THREADLESS_OPTIONS",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `schedules.update: ${offenders.join(", ")} require a threadId.`
});
}
}
return {
...existingTarget,
...patch.prompt !== void 0 ? { prompt: patch.prompt } : {},
...patch.name !== void 0 ? { name: patch.name } : {},
...patch.signalType !== void 0 ? { signalType: patch.signalType } : {},
...patch.tagName !== void 0 ? { tagName: patch.tagName } : {},
...patch.attributes !== void 0 ? { attributes: patch.attributes } : {},
...patch.providerOptions !== void 0 ? { providerOptions: patch.providerOptions } : {},
...patch.ifActive !== void 0 ? { ifActive: patch.ifActive } : {},
...patch.ifIdle !== void 0 ? { ifIdle: patch.ifIdle } : {}
};
}
#patchWorkflowTarget(existingTarget, patch) {
const agentOnly = [
"prompt",
"name",
"signalType",
"tagName",
"attributes",
"providerOptions",
"ifActive",
"ifIdle"
];
const offenders = agentOnly.filter((key) => patch[key] !== void 0);
if (offenders.length > 0) {
throw new MastraError({
id: "SCHEDULES_INVALID_WORKFLOW_PATCH",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `schedules.update: ${offenders.join(", ")} only apply to agent schedules.`
});
}
const wfPatch = patch;
return {
...existingTarget,
...wfPatch.inputData !== void 0 ? { inputData: wfPatch.inputData } : {},
...wfPatch.initialState !== void 0 ? { initialState: wfPatch.initialState } : {},
...wfPatch.requestContext !== void 0 ? { requestContext: wfPatch.requestContext } : {}
};
}
async delete(id) {
const store = await this.#getStore();
const existing = await this.#load(id);
if (!existing) return;
await store.deleteSchedule(existing.id);
}
async pause(id) {
const store = await this.#getStore();
const existing = await this.#load(id);
if (!existing) {
throw new MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
}
if (existing.status === "paused") return toScheduleView(existing);
const updated = await store.updateSchedule(existing.id, { status: "paused" });
return toScheduleView(updated);
}
async resume(id) {
const store = await this.#getStore();
const existing = await this.#load(id);
if (!existing) {
throw new MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
}
if (existing.status === "active") return toScheduleView(existing);
const nextFireAt = computeNextFireAt(existing.cron, {
timezone: existing.timezone,
after: Date.now()
});
const updated = await store.updateSchedule(existing.id, { status: "active", nextFireAt });
return toScheduleView(updated);
}
async run(id) {
const existing = await this.#load(id);
if (!existing) {
throw new MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
}
const now = Date.now();
if (existing.target.type === "agent") {
const claimId2 = `manual_${existing.id}_${now}`;
await this.#mastra.pubsub.publish("agent-schedules", {
type: "agent-schedule.fire",
runId: claimId2,
data: {
scheduleId: existing.id,
claimId: claimId2,
scheduledFireAt: now,
target: existing.target,
triggerKind: "manual"
}
});
return { scheduleId: existing.id, claimId: claimId2, scheduledFireAt: now };
}
const { workflowId, inputData, initialState, requestContext } = existing.target;
const claimId = `sched_${existing.id}_${now}`;
await this.#mastra.pubsub.publish(TOPIC_WORKFLOWS, {
type: "workflow.start",
runId: claimId,
data: {
workflowId,
runId: claimId,
prevResult: { status: "success", output: inputData ?? {} },
requestContext: requestContext ?? {},
initialState: initialState ?? {}
}
});
const store = await this.#getStore();
try {
await store.recordTrigger({
scheduleId: existing.id,
runId: claimId,
scheduledFireAt: now,
actualFireAt: now,
outcome: "published",
triggerKind: "manual"
});
} catch {
}
return { scheduleId: existing.id, claimId, scheduledFireAt: now };
}
};
function toAgentSchedule(schedule) {
if (schedule.target?.type !== "agent") return null;
const target = schedule.target;
return {
id: schedule.id,
agentId: target.agentId,
...target.name !== void 0 ? { name: target.name } : {},
...target.threadId ? { threadId: target.threadId } : {},
...target.resourceId ? { resourceId: target.resourceId } : {},
prompt: target.prompt,
cron: schedule.cron,
...schedule.timezone ? { timezone: schedule.timezone } : {},
status: schedule.status,
nextFireAt: schedule.nextFireAt,
...schedule.lastFireAt !== void 0 ? { lastFireAt: schedule.lastFireAt } : {},
...schedule.lastRunId ? { lastRunId: schedule.lastRunId } : {},
...target.signalType ? { signalType: target.signalType } : {},
...target.tagName ? { tagName: target.tagName } : {},
...target.attributes ? { attributes: target.attributes } : {},
...target.providerOptions ? { providerOptions: target.providerOptions } : {},
...target.ifActive ? { ifActive: target.ifActive } : {},
...target.ifIdle ? { ifIdle: target.ifIdle } : {},
...schedule.metadata ? { metadata: schedule.metadata } : {},
createdAt: schedule.createdAt,
updatedAt: schedule.updatedAt
};
}
function toWorkflowSchedule(schedule) {
if (schedule.target?.type !== "workflow") return null;
const target = schedule.target;
return {
id: schedule.id,
workflowId: target.workflowId,
cron: schedule.cron,
...schedule.timezone ? { timezone: schedule.timezone } : {},
status: schedule.status,
nextFireAt: schedule.nextFireAt,
...schedule.lastFireAt !== void 0 ? { lastFireAt: schedule.lastFireAt } : {},
...schedule.lastRunId ? { lastRunId: schedule.lastRunId } : {},
...target.inputData !== void 0 ? { inputData: target.inputData } : {},
...target.initialState !== void 0 ? { initialState: target.initialState } : {},
...target.requestContext !== void 0 ? { requestContext: target.requestContext } : {},
...schedule.metadata ? { metadata: schedule.metadata } : {},
createdAt: schedule.createdAt,
updatedAt: schedule.updatedAt
};
}
function toScheduleView(schedule) {
return toAgentSchedule(schedule) ?? toWorkflowSchedule(schedule);
}
export { AGENT_SCHEDULE_PREFIX, ScheduleInputSchema, ScheduleOutputSchema, Schedules, WORKFLOW_SCHEDULE_PREFIX, computeNextFireAt, toAgentSchedule, toScheduleView, toWorkflowSchedule, validateCron };
//# sourceMappingURL=chunk-F3BBI4YR.js.map
//# sourceMappingURL=chunk-F3BBI4YR.js.map