Scheduled workflows
Declare a schedule field on a workflow and Mastra will fire it on the cron you specify. The same workflow remains callable directly with workflow.start(), scheduled fires and manual runs share a single execution path.
QuickstartDirect link to Quickstart
The following workflow runs every day at 9am New York time. Register it on Mastra as you would any other workflow, the scheduler picks it up automatically.
import { createWorkflow, createStep } from '@mastra/core/workflows'
import { z } from 'zod'
const sendReport = createStep({
id: 'send-report',
inputSchema: z.object({ userId: z.string() }),
outputSchema: z.object({ ok: z.boolean() }),
execute: async ({ inputData }) => {
// ...send the report for inputData.userId
return { ok: true }
},
})
export const dailyReport = createWorkflow({
id: 'daily-report',
inputSchema: z.object({ userId: z.string() }),
outputSchema: z.object({ ok: z.boolean() }),
schedule: {
cron: '0 9 * * *',
timezone: 'America/New_York',
inputData: { userId: 'system' },
},
})
.then(sendReport)
.commit()
A separate "register schedule" call won't happen. The scheduler reads schedule straight off the workflow when Mastra boots.
Execution engineDirect link to Execution engine
Declaring schedule doesn't change how the workflow runs. Scheduled fires run in-process on the host that runs the scheduler, the same way workflow.start() or an HTTP-triggered run does. A workflow that declares schedule needs no extra storage or worker setup to run directly.
Split-worker deployments are the exception. When the MASTRA_WORKERS environment variable is set to any value, including false, createWorkflow builds scheduled workflows on the evented execution engine so that every process in the deployment runs them the same way. Scheduled fires then run on the orchestration worker, and Mastra logs a warning to the console when the workflow is created. The public API (workflow.start(), workflow.startAsync(), resume()) doesn't change.
Evented runs require a storage adapter that applies concurrent updates atomically, such as @mastra/libsql or @mastra/pg. With any other adapter, createRun() throws an error that lists the supported adapters.
Only the environment variable triggers the switch, because createWorkflow runs before Mastra reads its workers option. If you split workers with the workers option instead, import createWorkflow from @mastra/core/workflows/evented for your scheduled workflows.
Single scheduleDirect link to Single schedule
Pass an object to schedule for a workflow that fires on one cadence:
const dailyReport = createWorkflow({
id: 'daily-report',
schedule: {
cron: '0 9 * * *',
timezone: 'America/New_York',
inputData: { userId: 'system' },
},
// ...
})
Fields:
cron(required): a 5-, 6-, or 7-part cron expression. Validated at workflow construction time.timezone(optional): IANA timezone, for exampleAmerica/New_York. Defaults to the host's local timezone. Set this explicitly in production so fire times don't depend on server locale.inputData(optional): payload passed as the workflow's input on every fire.initialState(optional): initial state for the run.requestContext(optional): request context attached to the run.metadata(optional): arbitrary metadata persisted alongside the schedule row.
Multiple schedulesDirect link to Multiple schedules
Pass an array to fire the same workflow on multiple cadences. Each entry needs a unique stable id:
const statusCheck = createWorkflow({
id: 'status-check',
schedule: [
{ id: 'morning', cron: '0 9 * * *', inputData: { window: 'morning' } },
{ id: 'evening', cron: '0 18 * * *', inputData: { window: 'evening' } },
],
// ...
})
Each entry creates an independent schedule row and fires on its own cron, plus shows up separately in the Studio Schedules view.
Viewing schedules in StudioDirect link to Viewing schedules in Studio
Studio surfaces schedules as a top-level area, not as a tab inside a workflow:
- All schedules: open
/workflows/schedulesfor a cross-workflow list. Each row shows the workflow id, cron, next fire, and the most recent run's status. The list answers "is anything broken?" at a glance. - Filtered by workflow: append
?workflowId=<id>to scope the list to a single workflow, for example/workflows/schedules?workflowId=daily-report. - Schedule detail: Select any row to open
/workflows/schedules/:scheduleId. The page shows schedule metadata and Pause / Resume controls, followed by the full trigger history.
A workflow's header includes a Schedules action when the workflow has at least one schedule:
- When one schedule matches, the action links straight to its detail page.
- Multiple schedules: the action links to the workflow-filtered list at
/workflows/schedules?workflowId=<id>. - No schedules: the action is hidden.
Trigger historyDirect link to Trigger history
Every fire records a trigger row with the run id, scheduled time, actual fire time, and publish status. The schedule detail page joins each trigger to the corresponding workflow run and shows:
- The run's status (
running,success,failed,suspended,canceled) as a badge. - The run's start time and duration.
- A link to the run's full graph view at
/workflows/:workflowId/graph/:runId. - A
pendingbadge for triggers whose run record hasn't been written yet, due to a race between trigger publish and run snapshot. - A
publish failedbadge with the publish error when the scheduler couldn't enqueue the run at all.
Triggers in a non-terminal state cause the panel to poll every five seconds until they reach a terminal state. The list paginates, so long-running schedules don't load thousands of rows up front.
Pausing a schedule at runtimeDirect link to Pausing a schedule at runtime
When a scheduled workflow misfires in production, you don't have to redeploy or hand-edit the database. Pause it from the SDK:
import { MastraClient } from '@mastra/client-js'
const client = new MastraClient({ baseUrl: 'http://localhost:4111' })
// Schedule ids are derived from the workflow id: `wf_<workflowId>` for a
// single declarative schedule, or `wf_<workflowId>__<scheduleId>` when you
// declare multiple schedules per workflow as an array.
await client.pauseSchedule('wf_daily-report')
// ...investigate, ship a fix, then:
await client.resumeSchedule('wf_daily-report')
In Studio, open the schedule detail page and select Pause or Resume in the header.
A few rules to know:
- Pause is durable. The status is written to the schedules table and survives process restarts and redeploys. The declarative-config upsert never overwrites a user-set status, even when you change
cron,timezone, or other fields. - Resume recomputes
nextFireAtfrom now. A schedule paused for a week doesn't fire seven backlogged runs the moment you resume it. It fires on the next regular cron tick. - Unpause with
resumeScheduleor the Resume button in Studio. Editing the workflow'sscheduleconfig doesn't unpause a paused row. - Pause and resume are idempotent. Calling pause on an already-paused schedule is a no-op.
- This operational override controls existing schedules. Author declarative schedules in code. Declarative schedules are created, deleted, and edited in code via the
schedulefield oncreateWorkflow. To create schedules imperatively at runtime instead, use the unifiedmastra.schedulesservice with aworkflowId.
The underlying HTTP routes are POST /api/schedules/:scheduleId/pause and POST /api/schedules/:scheduleId/resume. Both require the schedules:write permission.
Redeploying with changesDirect link to Redeploying with changes
When you change the schedule config and redeploy, Mastra diffs the existing schedule row against the new config:
- If
cronortimezonechanged,nextFireAtis recomputed. A cadence with no future occurrence completes the schedule. A recomputed cadence that can still fire reactivates a completed schedule. - If only
inputData,initialState, ormetadatachanged, the row is patched in place and the next fire time is preserved. - User-set status (for example, paused via
client.pauseSchedule) and fire history are never overwritten, unless the cadence change above requires a status update.
Removing a schedule entry from a workflow's schedule array deletes its row on the next boot.
Deployment topologyDirect link to Deployment topology
The built-in scheduler uses a setInterval tick loop to poll the schedules table and claim due rows. It dispatches workflow runs through the in-process pubsub. It assumes a long-lived host process.
Long-lived host (recommended)Direct link to Long-lived host (recommended)
Deploy targets such as Fly Machines, Railway, Render, AWS ECS, GKE, or your own server keep the Mastra process alive between cron ticks. The server generated by mastra build starts the scheduler automatically, so schedules work without extra setup.
If you run Mastra with a server adapter, call mastra.startWorkers() after your server starts listening. Adapters don't start the scheduler, and schedules never fire without this call. See Start background workers.
For production deployments, you can run the scheduler as a dedicated worker process to isolate it from the API layer.
Serverless platformsDirect link to Serverless platforms
Functions-as-a-service platforms such as Vercel, Netlify, AWS Lambda, and Cloudflare Workers shut the process down after each request, so the tick loop doesn't get a second tick and schedules declared in code don't fire with the built-in scheduler today. Direct runs still work: calling run.start() from an HTTP route runs a workflow that declares schedule in-process, like any other workflow.
For cron fires on these platforms, use @mastra/inngest instead. Inngest is serverless-native and holds the cron state for you.
Inngest workflowsDirect link to Inngest workflows
The schedule field documented on this page drives Mastra's built-in scheduler. If you use @mastra/inngest, scheduled workflows are configured through Inngest's own cron field on createFunction and fire on Inngest's scheduler instead.
Practical implications:
- Inngest schedules don't appear in Studio's
/workflows/schedulesview. - The workflow header Schedules action doesn't show for Inngest workflows.
client.pauseScheduleandclient.resumeScheduledon't control Inngest schedules.
Manage Inngest schedules from the Inngest dashboard. Use Mastra schedules when you want Mastra to own scheduling end to end.
RelatedDirect link to Related
- Workflow overview
- Suspend and resume
- Workers: The scheduler worker runs cron schedules in a dedicated process
- Agent schedules: Run an agent rather than a workflow on a cron schedule, and manage both schedule types at runtime through
mastra.schedules.