@mastra/core
Version:
583 lines (582 loc) • 21.7 kB
JavaScript
const require_rolldown_runtime = require("./rolldown-runtime-uwYp4b74.cjs");
const require_error = require("./error-B-e62x-A.cjs");
const require_cron = require("./cron-CrmzJKFJ.cjs");
let crypto = require("crypto");
let zod_v4 = require("zod/v4");
let _sindresorhus_slugify = require("@sindresorhus/slugify");
_sindresorhus_slugify = require_rolldown_runtime.__toESM(_sindresorhus_slugify, 1);
//#region src/schedules/types.ts
/** Stable schedule id prefix for agent schedules. */
const AGENT_SCHEDULE_PREFIX = "agent_";
/**
* Stable schedule id prefix for imperative workflow schedules created via
* `mastra.schedules.create({ workflowId, ... })`. Intentionally distinct from
* the `wf_` prefix used by declarative `createWorkflow({ schedule })` rows —
* the boot-time declarative sync sweeps `wf_` rows against the in-code
* config and must never delete imperative rows.
*/
const WORKFLOW_SCHEDULE_PREFIX = "schedule_";
/** Shared zod for {@link AgentSignalAttributes} (XML tag attribute values). */
const ScheduleAttributesSchema = zod_v4.z.record(zod_v4.z.string(), zod_v4.z.union([
zod_v4.z.string(),
zod_v4.z.number(),
zod_v4.z.boolean(),
zod_v4.z.null()
]));
/** Serializable stream options applied to a woken run. See {@link ScheduleStreamOptions}. */
const ScheduleStreamOptionsSchema = zod_v4.z.object({ requestContext: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional() });
/** Options applied when the target thread is actively streaming. */
const ScheduleIfActiveSchema = zod_v4.z.object({
behavior: zod_v4.z.enum([
"deliver",
"persist",
"discard"
]).optional(),
attributes: ScheduleAttributesSchema.optional()
});
/** Options applied when the target thread is idle. */
const ScheduleIfIdleSchema = zod_v4.z.object({
behavior: zod_v4.z.enum([
"wake",
"persist",
"discard"
]).optional(),
attributes: ScheduleAttributesSchema.optional(),
streamOptions: ScheduleStreamOptionsSchema.optional()
});
/**
* Input payload persisted in `Schedule.target.inputData` for the built-in
* agent-schedule fire. The scheduler tick rehydrates this on every fire.
*/
const ScheduleInputSchema = zod_v4.z.object({
scheduleId: zod_v4.z.string(),
agentId: zod_v4.z.string(),
prompt: zod_v4.z.string(),
threadId: zod_v4.z.string().optional(),
resourceId: zod_v4.z.string().optional(),
signalType: zod_v4.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: zod_v4.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: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional(),
ifActive: ScheduleIfActiveSchema.optional(),
ifIdle: ScheduleIfIdleSchema.optional()
});
const ScheduleOutputSchema = zod_v4.z.object({
status: zod_v4.z.enum([
"fired",
"signal-accepted",
"skipped-thread-blocked",
"thread-missing",
"agent-missing",
"invalid-input"
]),
reason: zod_v4.z.string().optional()
});
//#endregion
//#region src/schedules/schedules.ts
/** PubSub topic consumed by the workflow event processor. */
const TOPIC_WORKFLOWS = "workflows";
/**
* Slugify the caller-facing portion of a schedule id into a canonical
* `<prefix><slug>` shape. The slug part is lowercased and stripped of
* characters that are unsafe in storage keys / URLs; the prefix is added only
* if missing so a caller can pass either `nightly-summary` or
* `agent_nightly-summary` and get the same canonical id. Returns an empty
* string when nothing slug-able remains.
*/
function canonicalizeScheduleId(rawId, prefix) {
const trimmed = rawId.trim();
const slug = (0, _sindresorhus_slugify.default)(trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed);
if (!slug) return "";
return `${prefix}${slug}`;
}
/**
* Normalize a caller-supplied schedule id for `create`. Throws
* `SCHEDULES_INVALID_ID` when the id is empty after normalization so callers
* cannot create an unaddressable schedule.
*/
function normalizeScheduleId(rawId, prefix) {
const canonical = canonicalizeScheduleId(rawId, prefix);
if (!canonical) throw new require_error.MastraError({
id: "SCHEDULES_INVALID_ID",
domain: require_error.ErrorDomain.AGENT,
category: require_error.ErrorCategory.USER,
text: `schedules.create: id "${rawId}" is empty after normalization. Provide an id with at least one alphanumeric character.`
});
return canonical;
}
/**
* Unified service for cron schedules. Schedules are persisted as `Schedule`
* rows whose `target` discriminates what fires: `type: 'agent'` rows run an
* agent (via signal or `agent.generate`), `type: 'workflow'` rows start a
* workflow run. This class is a typed projection over `SchedulesStorage`
* that knows how to build targets and surface flat
* {@link AgentSchedule} / {@link WorkflowSchedule} views.
*
* Use via `mastra.schedules` (the canonical CRUD surface).
*/
var Schedules = class {
#mastra;
constructor(mastra) {
this.#mastra = mastra;
}
async #getStore() {
const store = await this.#mastra.getStorage()?.getStore("schedules");
if (!store) throw new require_error.MastraError({
id: "SCHEDULES_NO_SCHEDULES_STORAGE",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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) {
require_cron.validateCron(input.cron, input.timezone);
if (!input.agentId) throw new require_error.MastraError({
id: "SCHEDULES_MISSING_TARGET_ID",
domain: require_error.ErrorDomain.AGENT,
category: require_error.ErrorCategory.USER,
text: "schedules.create requires `agentId` or `workflowId`."
});
if (input.threadId && !input.resourceId) throw new require_error.MastraError({
id: "SCHEDULES_MISSING_RESOURCE_ID",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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 require_error.MastraError({
id: "SCHEDULES_THREADLESS_OPTIONS",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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}${(0, crypto.randomUUID)()}`;
await this.#assertIdAvailable(store, id, input.id !== void 0);
const now = Date.now();
const nextFireAt = require_cron.computeNextFireAt(input.cron, {
timezone: input.timezone,
after: now
});
const schedule = {
id,
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 } : {}
},
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 } : {}
};
return toAgentSchedule(await store.createSchedule(schedule));
}
async #createWorkflowSchedule(input) {
require_cron.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}${(0, crypto.randomUUID)()}`;
await this.#assertIdAvailable(store, id, input.id !== void 0);
const now = Date.now();
const nextFireAt = require_cron.computeNextFireAt(input.cron, {
timezone: input.timezone,
after: now
});
const schedule = {
id,
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 } : {}
},
cron: input.cron,
timezone: input.timezone,
status: input.status ?? "active",
nextFireAt,
createdAt: now,
updatedAt: now,
...input.metadata ? { metadata: input.metadata } : {}
};
return toWorkflowSchedule(await store.createSchedule(schedule));
}
async #assertIdAvailable(store, id, callerProvided) {
if (!callerProvided) return;
if (await store.getSchedule(id)) throw new require_error.MastraError({
id: "SCHEDULES_ID_EXISTS",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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 views = (await (await this.#getStore()).listSchedules({
...filter?.agentId ? {
ownerType: "agent",
ownerId: filter.agentId
} : {},
...filter?.workflowId ? { workflowId: filter.workflowId } : {},
...filter?.status ? { status: filter.status } : {}
})).map(toScheduleView).filter((s) => s !== null).filter((s) => filter?.agentId ? s.agentId !== void 0 : true);
if (!(filter?.threadId !== void 0 || filter?.resourceId !== void 0 || filter?.name !== void 0)) 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 require_error.MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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) require_cron.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 ? require_cron.computeNextFireAt(nextCron, {
timezone: nextTimezone,
after: Date.now()
}) : void 0;
return toScheduleView(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 } : {}
}));
}
#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 require_error.MastraError({
id: "SCHEDULES_THREADLESS_OPTIONS",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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 offenders = [
"prompt",
"name",
"signalType",
"tagName",
"attributes",
"providerOptions",
"ifActive",
"ifIdle"
].filter((key) => patch[key] !== void 0);
if (offenders.length > 0) throw new require_error.MastraError({
id: "SCHEDULES_INVALID_WORKFLOW_PATCH",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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 require_error.MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: require_error.ErrorDomain.AGENT,
category: require_error.ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
if (existing.status === "paused") return toScheduleView(existing);
return toScheduleView(await store.updateSchedule(existing.id, { status: "paused" }));
}
async resume(id) {
const store = await this.#getStore();
const existing = await this.#load(id);
if (!existing) throw new require_error.MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: require_error.ErrorDomain.AGENT,
category: require_error.ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
if (existing.status === "active") return toScheduleView(existing);
const nextFireAt = require_cron.computeNextFireAt(existing.cron, {
timezone: existing.timezone,
after: Date.now()
});
return toScheduleView(await store.updateSchedule(existing.id, {
status: "active",
nextFireAt
}));
}
async run(id) {
const existing = await this.#load(id);
if (!existing) throw new require_error.MastraError({
id: "SCHEDULES_NOT_FOUND",
domain: require_error.ErrorDomain.AGENT,
category: require_error.ErrorCategory.USER,
text: `Schedule "${id}" not found.`
});
const now = Date.now();
if (existing.target.type === "agent") {
const claimId = `manual_${existing.id}_${now}`;
await this.#mastra.pubsub.publish("agent-schedules", {
type: "agent-schedule.fire",
runId: claimId,
data: {
scheduleId: existing.id,
claimId,
scheduledFireAt: now,
target: existing.target,
triggerKind: "manual"
}
});
return {
scheduleId: existing.id,
claimId,
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
};
}
};
/**
* Project a `Schedule` row to a flat {@link AgentSchedule} view. Returns
* `null` when the schedule is not an agent schedule
* (`target.type !== 'agent'`), allowing callers to filter mixed result sets
* in one pass.
*/
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
};
}
/**
* Project a `Schedule` row to a flat {@link WorkflowSchedule} view. Returns
* `null` when the schedule is not a workflow schedule.
*/
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
};
}
/** Project a `Schedule` row to whichever flat view matches its target type. */
function toScheduleView(schedule) {
return toAgentSchedule(schedule) ?? toWorkflowSchedule(schedule);
}
//#endregion
Object.defineProperty(exports, "AGENT_SCHEDULE_PREFIX", {
enumerable: true,
get: function() {
return AGENT_SCHEDULE_PREFIX;
}
});
Object.defineProperty(exports, "ScheduleInputSchema", {
enumerable: true,
get: function() {
return ScheduleInputSchema;
}
});
Object.defineProperty(exports, "ScheduleOutputSchema", {
enumerable: true,
get: function() {
return ScheduleOutputSchema;
}
});
Object.defineProperty(exports, "Schedules", {
enumerable: true,
get: function() {
return Schedules;
}
});
Object.defineProperty(exports, "WORKFLOW_SCHEDULE_PREFIX", {
enumerable: true,
get: function() {
return WORKFLOW_SCHEDULE_PREFIX;
}
});
Object.defineProperty(exports, "toAgentSchedule", {
enumerable: true,
get: function() {
return toAgentSchedule;
}
});
Object.defineProperty(exports, "toScheduleView", {
enumerable: true,
get: function() {
return toScheduleView;
}
});
Object.defineProperty(exports, "toWorkflowSchedule", {
enumerable: true,
get: function() {
return toWorkflowSchedule;
}
});
//# sourceMappingURL=schedules-D_aayWxz.cjs.map