# Heartbeats **Added in:** `@mastra/core@1.46.0` > **Beta:** This feature is in alpha. Breaking changes may occur without a major version bump until the API is stable. A heartbeat runs an agent on a cron schedule. On each fire, Mastra sends a prompt to the agent, either as a [signal](https://mastra.ai/docs/agents/signals) into a thread or as a threadless `agent.generate()` run. Use heartbeats for recurring agent work such as daily summaries, periodic checks, or scheduled nudges into a conversation. Heartbeats are persisted, so they survive restarts and redeploys. Manage them at runtime through `mastra.heartbeats`, the canonical create, read, update, and delete (CRUD) surface. ## Prerequisites Heartbeats require a [storage](https://mastra.ai/docs/memory/storage) adapter that implements the schedules domain, for example `@mastra/libsql`. Without one, `mastra.heartbeats.create()` throws. ## Quickstart The following heartbeat runs the `pinger` agent every hour. It has no thread, so each fire is an isolated `agent.generate()` run. ```typescript import { Mastra } from '@mastra/core' import { Agent } from '@mastra/core/agent' import { LibSQLStore } from '@mastra/libsql' const pinger = new Agent({ id: 'pinger', name: 'Pinger', instructions: 'Report the current system status in one sentence.', model: 'openai/gpt-5.5', }) const mastra = new Mastra({ agents: { pinger }, storage: new LibSQLStore({ url: 'file:./mastra.db' }), }) await mastra.heartbeats.create({ agentId: 'pinger', cron: '0 * * * *', prompt: 'Give me a status update.', }) ``` Mastra starts the scheduler the first time a heartbeat is created, then fires the agent on the cron you specify. ## Cadence Heartbeats fire on a cron expression. The `cron` field accepts a standard 5-, 6-, or 7-part cron expression, and it's validated when you create or update the heartbeat. Croner nicknames also work, for example `@hourly`, `@daily`, `@weekly`, `@monthly`, and `@midnight`. For day-and-time combinations, write the cron field directly: ```typescript // Every weekday at 9am await mastra.heartbeats.create({ agentId: 'pinger', cron: '0 9 * * 1-5', prompt: 'Start-of-day check.', }) ``` Set `timezone` to an IANA timezone, for example `America/New_York`, so fire times don't depend on the host's locale. When omitted, the cron resolves against the host's local timezone. For more readable cron construction, you can use a userland builder such as [`cron-time-generator`](https://www.npmjs.com/package/cron-time-generator) and pass its output to `cron`. ## Threadless and threaded heartbeats A heartbeat fires in one of two modes, decided by whether you pass a `threadId`. ### Threadless Without a `threadId`, each fire is an isolated `agent.generate()` run. Nothing is written to a conversation thread. This is the simplest mode and suits status checks, reports, and other work that doesn't need conversation context. ### Threaded With a `threadId`, the heartbeat sends a [signal](https://mastra.ai/docs/agents/signals) into that thread, so the prompt joins the agent's conversation. Threaded heartbeats require a `resourceId` alongside the `threadId`. ```typescript await mastra.heartbeats.create({ agentId: 'pinger', cron: '0 9 * * *', prompt: 'Summarize anything new since yesterday.', threadId: 'thread-123', resourceId: 'user-456', }) ``` Threaded heartbeats accept extra fields that control how the signal behaves. They mirror the options [`agent.sendSignal`](https://mastra.ai/docs/agents/signals) accepts and stay JSON-serializable so they persist with the schedule. - `signalType`: the [signal type](https://mastra.ai/docs/agents/signals) to send, for example `notification` or `system-reminder`. Defaults to `notification`. - `tagName`: the XML tag the signal renders as. Defaults to `heartbeat`, so a fire surfaces to the agent as ``. - `attributes`: values rendered onto the signal's XML tag. - `ifActive`: behavior when the thread is already streaming, as `{ behavior, attributes }`. `behavior` is one of `deliver`, `persist`, or `discard`. - `ifIdle`: behavior when the thread is idle, as `{ behavior, attributes, streamOptions }`. `behavior` is one of `wake`, `persist`, or `discard`. `streamOptions.requestContext` is applied to the woken run. These fields require a `threadId`. Passing them on a threadless heartbeat throws. ```typescript await mastra.heartbeats.create({ agentId: 'pinger', cron: '0 9 * * *', prompt: 'Summarize anything new since yesterday.', threadId: 'thread-123', resourceId: 'user-456', tagName: 'check-in', // renders as attributes: { source: 'cron' }, ifActive: { behavior: 'discard' }, // skip if the thread is mid-stream ifIdle: { behavior: 'wake', // wake the agent if the thread is idle streamOptions: { requestContext: { locale: 'en-US' } }, }, }) ``` `providerOptions` are merged into the signal payload on every fire and apply to both threaded and threadless heartbeats. ## Managing heartbeats Use `mastra.heartbeats` for all heartbeat operations. To scope to a single agent, pass `agentId` to `create` or `list`. ```typescript // Create const hb = await mastra.heartbeats.create({ agentId: 'pinger', cron: '0 * * * *', prompt: 'Status check.', }) // Read await mastra.heartbeats.get(hb.id) await mastra.heartbeats.list({ agentId: 'pinger' }) // Update — changing cron or timezone recomputes the next fire time await mastra.heartbeats.update(hb.id, { cron: '*/30 * * * *' }) // Pause and resume await mastra.heartbeats.pause(hb.id) await mastra.heartbeats.resume(hb.id) // Fire once now, off-schedule await mastra.heartbeats.run(hb.id) // Delete await mastra.heartbeats.delete(hb.id) ``` A few rules worth knowing: - `pause` and `resume` are durable and idempotent. A paused heartbeat survives restarts, and `resume` recomputes the next fire time from now rather than firing backlogged runs. - `run` fires the heartbeat once immediately without affecting its schedule. - `list` filters by `agentId`, `threadId`, `resourceId`, and `name`. ### Custom IDs By default `create` generates a random `hb_` id. Pass `id` to choose a stable one, for example when you want a predictable handle to look up, update, or delete later: ```typescript await mastra.heartbeats.create({ id: 'nightly-summary', agentId: 'pinger', cron: '0 9 * * *', prompt: 'Summarize anything new since yesterday.', }) // stored as `hb_nightly-summary` ``` The id is normalized to `hb_`: the `hb_` prefix is added if missing and the rest is slugified. Creating a heartbeat with an id that already exists throws, so use `update` to change an existing one. ### From the client The same operations are available from `@mastra/client-js` over the server routes, so you can manage heartbeats from a separate process or a UI. ## Lifecycle hooks Hooks let you run code at key points in a heartbeat's lifecycle, for example to compute fire-time parameters or react to the outcome. Configure them on the `Mastra` constructor under `heartbeat`. The hooks are a single flat bundle that runs for every agent's heartbeats; each hook context carries the firing `agentId`, so branch on it when you need per-agent behavior. Hooks live at the `Mastra` level so they apply to both code-defined and stored agents. ```typescript const mastra = new Mastra({ agents: { pinger }, storage: new LibSQLStore({ url: 'file:./mastra.db' }), heartbeat: { prepare: async ({ agentId, heartbeat, trigger }) => { // Return overrides, null to skip this fire, or undefined for defaults return { prompt: `Status as of ${trigger.firedAt.toISOString()}` } }, onFinish: async ({ agentId, outcome, runId }) => { // Runs on any non-error, non-abort outcome }, onError: async ({ agentId, phase, error }) => { // Runs when prepare, the signal, or the agent run threw }, onAbort: async ({ agentId, runId }) => { // Runs when the run was aborted mid-stream }, }, }) ``` The hooks are: - `prepare`: runs before the fire. Return an object to override fire-time parameters such as `prompt` or `threadId`, `null` to skip the fire, or `undefined` to use the stored defaults. - `onFinish`: runs once per trigger that reached a non-error, non-abort terminal state. - `onError`: runs when `prepare`, the signal, or the agent run threw. - `onAbort`: runs when the run was aborted mid-stream. Every hook context includes `agentId` (the agent the heartbeat fired for) alongside `heartbeat` and `trigger`. Hook exceptions are caught and logged. They never re-route the worker or trigger another hook. ## Related - [Signals](https://mastra.ai/docs/agents/signals): the delivery mechanism behind threaded heartbeats. - [Scheduled workflows](https://mastra.ai/docs/workflows/scheduled-workflows): run a workflow, rather than an agent, on a cron schedule.