Skip to content

feat(core): scheduled agent heartbeats (core + server + client-js)#18184

Merged
CalebBarnes merged 54 commits into
mainfrom
caleb/heartbeats-on-accepted
Jun 29, 2026
Merged

feat(core): scheduled agent heartbeats (core + server + client-js)#18184
CalebBarnes merged 54 commits into
mainfrom
caleb/heartbeats-on-accepted

Conversation

@CalebBarnes

@CalebBarnes CalebBarnes commented Jun 19, 2026

Copy link
Copy Markdown
Member

What

Adds heartbeats — a first-class type: 'heartbeat' schedule target that runs an agent periodically on a cron. A heartbeat either:

  • runs inside an existing thread via agent.sendSignal(), so subscribers receive the message through the normal channel pipeline, or
  • runs in isolation via a one-off agent.generate().

Each heartbeat has a random hb_<uuid> id and an optional name, so one agent/thread can host multiple heartbeats with different crons and prompts.

API

New methods on the Mastra heartbeats service: create / update / get / list / pause / resume / delete. Each heartbeat carries a cron, optional timezone, a prompt, optional threadId/resourceId, ifActive/ifIdle policy, and lifecycle hooks. HTTP routes and a @mastra/client-js client surface the same operations.

How it works

  • Persisted as one mastra_schedules row (target.type === 'heartbeat'); one mastra_schedule_triggers row per fire.
  • The Scheduler publishes due fires on the heartbeats pubsub topic; a HeartbeatWorker subscribes and runs the agent.
  • Each fire wakes / delivers / persists / discards / skips per the heartbeat's ifActive / ifIdle policy.
  • Lifecycle hooks: prepare / onFinish / onError / onAbort.
  • Self-cleans when the target agent or thread no longer exists.
  • Heartbeat is a first-class ScheduleTarget variant (not a hidden internal workflow), so it shows up cleanly in schedule listings and trigger history.

Signals integration

The worker signal consumer is written against the unified accepted: Promise<SendAgentSignalAccepted> API:

  • awaits the settled result,
  • narrows the discriminated union by action (deliver / persist / discard / blocked / wake),
  • maps blocked (thread can't currently accept the signal) to a skipped-thread-blocked outcome rather than treating it as success,
  • derives runId asymmetrically via 'runId' in settled,
  • treats a rejected accepted (misconfig) as an error outcome.

Scope

@mastra/core + @mastra/server + @mastra/client-js. Studio/playground UI follows separately.

Tests

  • Core: heartbeat module (54), scheduler (36), signals/harness all green
  • Server: heartbeats handler 22/22
  • client-js: builds + typechecks clean

The update … recomputes nextFireAt test is now pinned with fake timers so it's deterministic (previously wallclock-flaky at the top of the hour, where */5 * * * * and 0 * * * * resolve to the same next-fire timestamp).

ELI5

This PR adds heartbeats: a way to make an agent automatically run over and over on a cron schedule. Each heartbeat can either poke an existing chat thread or run by itself, and you can manage it with server endpoints or the client SDK.

Summary

  • Added scheduled agent heartbeats as a first-class schedule target (type: 'heartbeat') across @mastra/core, @mastra/server, and @mastra/client-js.
  • Implemented the new mastra.heartbeats service with: create, get, list, update, pause, resume, delete, and manual run.
  • Persisted heartbeats as durable schedule rows (survive restarts) and emitted per-fire trigger rows when they run.
  • Generalized scheduler execution to a unified Scheduler model:
    • Scheduler publishes heartbeat ticks on a dedicated heartbeats pubsub topic
    • HeartbeatWorker executes each tick using:
      • Threaded mode (threadId + resourceId) via agent.sendSignal()
      • Threadless mode via agent.generate() in isolation
  • Added heartbeat lifecycle hooks on the Mastra constructor (heartbeat.prepare, onFinish, onError, onAbort), including:
    • skipping execution (prepare returns null)
    • overriding effective run settings before sending/signaling/generating
    • safe/logged hook error handling
    • propagation of manual trigger context (triggerKind: 'manual')
  • Updated scheduling/storage and trigger typing to align outcome recording with the unified accepted-result outcome mapping (and expanded trigger outcomes/kinds for heartbeat + manual runs).
  • Implemented self-cleaning: heartbeat schedule rows are deleted when the target agent (and required thread state) no longer exists.
  • Added full REST API for heartbeats under /api/heartbeats (list/get/create/update/delete/pause/resume/run), including corresponding schemas and generated route typings/metadata.
  • Added MastraClient client-js APIs mirroring server operations, plus updated SDK types for heartbeat targets, run outcomes, and route contracts.
  • Extended permissions/auth and CLI route metadata to include the new heartbeats resource/action support.
  • Improved cron validation diagnostics:
    • validateCron now clearly distinguishes invalid cron from invalid timezone errors, and rejects empty/whitespace cron with an explicit message.

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment Jun 29, 2026 5:40pm
mastra-playground-ui Ready Ready Preview, Comment Jun 29, 2026 5:40pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-studio-preview Ignored Ignored Preview Jun 29, 2026 5:40pm

Request Review

@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 363f06a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
@mastra/core Minor
@mastra/server Minor
@mastra/client-js Minor
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
@mastra/deployer Minor
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/next Patch
@mastra/tanstack-start Patch
@mastra/playground-ui Major
@mastra/react Patch
mastra Patch
@mastra/deployer-cloud Minor
@mastra/temporal Patch
create-mastra Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds heartbeat scheduling for agents, including storage-backed CRUD, runtime execution, scheduler support, server routes, client APIs, permissions, tests, and documentation.

Changes

Agent Heartbeats

Layer / File(s) Summary
Heartbeat contracts
packages/core/src/agent/heartbeat/types.ts, packages/core/src/storage/domains/schedules/base.ts, packages/server/src/server/schemas/schedules.ts, packages/core/src/agent/heartbeat/index.ts, packages/core/src/agent/index.ts, packages/core/src/agent/signals.ts
Defines heartbeat payload schemas, storage target unions, schedule response schemas, and public re-exports for the heartbeat feature.
Scheduler and boot wiring
packages/core/src/workflows/scheduler/*, packages/core/src/worker/transport/pull-transport.ts, packages/core/src/worker/workers/scheduler-worker.ts, packages/core/src/mastra/index.ts, packages/core/src/mastra/scheduler-integration.test.ts, packages/core/src/agent/heartbeat/integration.test.ts, packages/core/src/mastra/workers-filter.test.ts, packages/core/src/workflows/scheduler/cron.ts, packages/core/src/workflows/scheduler/cron.test.ts
Generalizes the scheduler and Mastra boot path for workflow and heartbeat targets, including topic-aware transport, lazy worker injection, boot rehydration tests, and cron validation updates.
Heartbeats service
packages/core/src/agent/heartbeat/heartbeats.ts, packages/core/src/agent/heartbeat/api.test.ts, packages/core/src/agent/heartbeat/heartbeats.test.ts
Implements Heartbeats CRUD, list, pause, resume, and manual run operations on schedule rows and projects heartbeat schedules into the flat API shape.
Heartbeat worker
packages/core/src/agent/heartbeat/worker.ts, packages/core/src/agent/heartbeat/worker.test.ts, packages/core/src/agent/heartbeat/hooks.test.ts, packages/core/src/agent/heartbeat/provider-options.test.ts
Implements HeartbeatWorker event handling, executeHeartbeat execution paths, lifecycle hooks, provider metadata propagation, and related tests.
Heartbeat HTTP API
packages/server/src/server/schemas/heartbeats.ts, packages/server/src/server/handlers/heartbeats.ts, packages/server/src/server/handlers/heartbeats.test.ts, packages/server/src/server/server-adapter/routes/heartbeats.ts, packages/server/src/server/server-adapter/routes/index.ts
Adds heartbeat HTTP schemas, handlers, route registration, and handler tests.
Server support artifacts
packages/server/src/server/handlers/schedules-workflows-shim.ts, packages/server/scripts/permission-generator.ts, packages/_internals/auth/src/ee/interfaces/permissions.generated.ts, packages/cli/src/commands/api/route-metadata.generated.ts, server-adapters/_test-utils/src/route-test-utils.ts, server-adapters/_test-utils/src/test-helpers.ts, stores/_test-utils/src/domains/schedules/index.ts
Updates generated route metadata, permissions, compatibility shims, and test fixtures for heartbeat routes and trigger outcomes.
Client SDK surface
client-sdks/client-js/src/types.ts, client-sdks/client-js/src/route-types.generated.ts, client-sdks/client-js/src/client.ts
Adds heartbeat client types, generated route contracts, and MastraClient methods.
Docs and changelog
.changeset/heartbeats-on-accepted.md, docs/src/content/en/docs/agents/heartbeats.mdx, docs/src/content/en/docs/sidebars.js
Adds the heartbeat docs page, sidebar entry, and changeset note.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • mastra-ai/mastra#17723: Introduces the accepted-result contract shape that this PR’s heartbeat worker maps into threaded execution outcomes.

Suggested labels

Documentation, tests: green ✅, complexity: critical

Suggested reviewers

  • wardpeet
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the heartbeat feature work across core, server, and client-js.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch caleb/heartbeats-on-accepted

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from caleb/pubsub-lease-owner-stream to main June 19, 2026 21:26
@CalebBarnes CalebBarnes changed the title feat(heartbeats): scheduled agent check-ins (core/server/SDK) feat(core): heartbeats Jun 23, 2026
Adds heartbeats: a first-class `type: 'heartbeat'` schedule target that
runs an agent periodically on a cron — either inside an existing thread
(via agent.sendSignal(), so subscribers receive the message through the
normal channel pipeline) or in isolation (via a one-off agent.generate()).

Backed by HeartbeatWorker subscribing to the 'heartbeats' pubsub topic
published by the Scheduler. Each fire wakes/delivers/persists/discards/skips
per the heartbeat's ifActive/ifIdle policy and lifecycle hooks
(prepare/onFinish/onError/onAbort). Broadcast modes (live/on-complete/never)
are enforced channel-side.

The worker signal consumer is written against the unified
`accepted: Promise<SendAgentSignalAccepted>` API: it awaits the settled
result, narrows the discriminated union by action (present-tense
deliver/persist/discard/wake), and derives runId asymmetrically via
'runId' in settled.

Scope (PR1 of stacked pair per ship plan): @mastra/core, @mastra/server,
@mastra/client-js. Studio/playground UI follows in the stacked PR2.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@CalebBarnes CalebBarnes force-pushed the caleb/heartbeats-on-accepted branch from 06f4616 to 0fdf149 Compare June 23, 2026 16:03
@CalebBarnes CalebBarnes changed the title feat(core): heartbeats feat(core): scheduled agent heartbeats (core + server + client-js) Jun 23, 2026
…ats as the sole CRUD surface

Collapse heartbeat management onto a single canonical surface so there is
one API to document, test, and keep in sync. Callers scope to an agent by
passing agentId to create/list (mastra.heartbeats.list({ agentId })), which
the getter only wrapped anyway.

- Remove the agent.heartbeats getter and its now-unused type imports.
- Remove Heartbeats.listTriggers and the heartbeat-specific triggers route;
  trigger listing for any schedule is already served by the generic
  /schedules/:scheduleId/triggers endpoint, which skips workflow-run
  hydration when target.type !== 'workflow'.
- Drop the HeartbeatTriggerRow re-exports left dangling by the above.
- Retarget api.test.ts to mastra.heartbeats.*; the getter-only ownership
  guards (owner-mismatch, non-owned get) no longer apply, replaced the
  unregistered-agent guard with a missing-agentId guard.
- Regenerate client-js route types and CLI route metadata.

Agent-constructor heartbeat hooks (config.heartbeat) are intentionally left
in place; hook placement is a separate, still-open v1 decision.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 23, 2026 19:05 Inactive
Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-playground-ui June 23, 2026 19:06 Inactive
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 23, 2026 19:06 Inactive
Move heartbeat lifecycle hooks from the Agent constructor to the Mastra
constructor, keyed by agentId, so they apply to both code-defined and
stored agents (whose serialized config cannot carry functions). Adds
heartbeat?: HeartbeatConfig ({ hooks?: Record<agentId, HeartbeatHooks> })
to the Mastra config and a __getHeartbeatHooks(agentId) accessor the
worker resolves against; removes the agent-level heartbeat hooks field,
#heartbeatHooks, and __getHeartbeatHooks().

Drop the static activeHours window. With cadence locked to plain cron,
a clock-window filter is a lossier re-encoding of cron's hour/day fields
and a footgun: it only suppresses a fire, never advances nextFireAt, so a
coarse cron + narrow window silently skips forever. Static windows belong
in cron; dynamic gating stays via idleThresholdMs and the prepare-hook
skip. Removes the target field, type/schema, the skipped-outside-hours
status, the isWithinActiveHours worker gate, the server route
schemas/handler plumbing, and regenerates CLI route metadata.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 23, 2026 20:15 Inactive
…removal

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 23, 2026 20:16 Inactive
…heartbeats sugar

The changeset still documented the removed agent.heartbeats.* sugar and the
agent-constructor heartbeat hooks. Realign with the shipped surface:
mastra.heartbeats.* as the sole CRUD entrypoint and Mastra-level
heartbeat.hooks keyed by agentId.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
… hook context

Hooks register as a flat bundle on the Mastra constructor
(new Mastra({ heartbeat: { onFinish, prepare, ... } })) instead of being
keyed by agentId. agentId is now provided on each hook context, so a single
hook set can serve all agents and branch on ctx.agentId. Also adds the
heartbeats docs page.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Jun 23, 2026
CalebBarnes and others added 2 commits June 25, 2026 09:57
The heartbeats work retargeted ScheduleTriggerOutcome to the shipped
outcome set and dropped the older queue/notification outcomes (acked,
alerted, deferred, appended-from-queue, dropped-stale, dropped-superseded,
dropped-busy). Trigger rows persisted by older builds may still carry
those strings; dropping them from the union makes such rows lie about
their value and can break exhaustive handling when listing triggers.

Retain the legacy literals in the union (annotated as no-longer-written)
so reads stay correctly typed without resurrecting their write paths.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
heartbeats.update({ status: 'active' }) on a paused heartbeat flipped the
status field directly but left the stale nextFireAt from when it was
paused — often in the past — causing an immediate spurious fire. The
dedicated resume() method recomputes nextFireAt forward from now; update()
now mirrors that when a patch transitions paused -> active, so both entry
points share the same resume semantics. Non-resume updates leave
nextFireAt untouched.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 25, 2026 16:58 Inactive
CalebBarnes and others added 4 commits June 25, 2026 10:09
Resolve the quick-win review threads on the heartbeats PR so the public
surface and docs match the shipped behavior:

- Keep legacy trigger-outcome values readable in the server response
  validator (zod enum widened to mirror the core ScheduleTriggerOutcome
  union) so trigger rows persisted by older builds still parse.
- Correct the documented threaded-signal default to 'system-reminder'
  (the worker default), not 'user-message'.
- Use the public /api/heartbeats path in the changeset.
- Update listSchedules() JSDoc to mention the owner filters it accepts.
- Translate getAgentById() (which throws on miss) into a clean 404 in
  the create-heartbeat handler instead of relying on an undefined return.
- Always shut down Mastra instances in the heartbeat integration tests
  via an afterEach registry so a failing assertion cannot leak workers.
- Document the broadcast option (live / on-complete / never) in the docs.
- Drop internal mechanics (workflow/HeartbeatWorker/processor) from
  client-js JSDoc; keep observable behavior only.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
validateCron now rejects missing or empty cron strings with an actionable
message and wraps croner's raw parser errors (e.g. "Pattern has to be of
type string.") with the offending pattern. Previously calling
heartbeats.create() or scheduling a workflow without a cron surfaced a wall
of minified croner internals instead of a usable error.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/workflows/scheduler/cron.ts`:
- Around line 15-22: The catch in validateCron currently wraps both invalid cron
patterns and invalid timezone errors into the same “Invalid cron expression”
message. Update validateCron to distinguish failures from new Cron(cron, {
timezone }) / job.nextRun() so timezone-related errors are reported as timezone
validation issues, while cron parsing errors still mention the cron expression.
Keep the behavior covered by validateCron in sync with the expectations in
cron.test.ts for invalid timezone input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 07a97bb6-8b62-4ffe-8fd9-9349592eafdf

📥 Commits

Reviewing files that changed from the base of the PR and between 69ffaf9 and 62ff8f4.

📒 Files selected for processing (2)
  • packages/core/src/workflows/scheduler/cron.test.ts
  • packages/core/src/workflows/scheduler/cron.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/workflows/scheduler/cron.test.ts

Comment thread packages/core/src/workflows/scheduler/cron.ts Outdated
CalebBarnes and others added 2 commits June 25, 2026 12:23
validateCron's catch path also caught croner's lazy timezone validation, so
a bad timezone was reported as an invalid cron expression. Validate the cron
pattern and the timezone separately so each failure is labeled correctly.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
…ifIdle defaults to sendSignal

idleThresholdMs was a fuzzy idle proxy (thread updatedAt) that overlapped with
ifActive routing and added API surface for behavior ifActive already covers.
Removing it across core, server, client-js, and docs.

The heartbeat worker no longer synthesizes its own ifActive/ifIdle defaults.
When the caller omits them, nothing is passed and sendSignal applies its own
defaults (deliver/wake), keeping heartbeat delivery consistent with direct
signal usage instead of diverging (worker previously defaulted ifActive to
discard). HeartbeatIfActive/HeartbeatIfIdle now alias the signal behavior
unions so the two stay in lockstep.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

Comment thread packages/core/src/agent/heartbeat/worker.ts Outdated
CalebBarnes and others added 16 commits June 25, 2026 14:02
A guard added in #18183 (shouldStartBroadcastOnRegister) only started the
broadcast pump when `fullStream` was an own data property on the output.
On real `agent.stream()` results `fullStream` is a getter, so the check was
always false and no real threaded run self-pumped. Interactive runs only
completed because the caller iterated the returned stream; a fire-and-forget
wake (e.g. a heartbeat) had no consumer, so the run never reached a terminal
state and its active-run record + thread lease never released — permanently
wedging the thread so every later signal coalesced into the stuck run.

Always start the broadcast pump on register. The broadcast tee buffers every
part, so a later/external subscriber still replays the full stream, while the
run now completes on its own.

Also remove debug console.log statements left in agent.ts, agent-channels.ts,
and the heartbeat worker.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
…ccepted

# Conflicts:
#	client-sdks/client-js/src/route-types.generated.ts
#	packages/_internals/auth/src/ee/interfaces/permissions.generated.ts
#	packages/server/scripts/permission-generator.ts
#	packages/server/src/server/server-adapter/routes/index.ts
Heartbeat ids were always random hb_<uuid>. Callers often want a stable,
predictable handle (e.g. 'nightly-summary') to look up, update, or delete a
heartbeat without first persisting the generated id. Accept an optional id
on create that is normalized to hb_<slug> (prefix added if missing, rest
slugified) and reject duplicates so a typo doesn't silently clobber an
existing schedule. Threaded through core, the /heartbeats POST body, and the
client-js createHeartbeat input.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
… pre mode

Regenerate route-types.generated.ts and route-metadata.generated.ts to
include the optional heartbeat `id` field added to POST /heartbeats.
Restore .changeset/pre.json so the version validation runs in pre mode,
matching main.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
Heartbeat config now mirrors the agent.sendSignal options surface while
staying JSON-serializable for schedule storage.

- ifActive/ifIdle widen from bare behavior strings to { behavior, attributes }
  (plus streamOptions on ifIdle), matching SendAgentSignalOptions.
- ifIdle.streamOptions accepts a serializable requestContext that the worker
  rehydrates into a RequestContext on the woken run, so heartbeat-woken runs
  can carry channel/render context.
- Top-level providerOptions persist through create/update/rehydrate and merge
  into the signal payload on every fire, regardless of ifActive/ifIdle.
- Keeps the notification signalType / heartbeat tagName defaults, top-level
  attributes, and resolveHeartbeatId lookup symmetry.

This is a breaking shape change to an unreleased API (heartbeats ship in
PR #18184); folded into the existing unreleased changeset. Docs and the
changeset example updated to the object-shaped API.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
…ough HTTP surface

The core heartbeat target gained object-shaped ifActive/ifIdle plus tagName,
attributes, providerOptions, and ifIdle.streamOptions.requestContext, but the
server route schemas, handler threading, and client-js types still described the
old bare-string shape. That left the server build failing (response schema no
longer assignable to ScheduleTarget) and the generated route-types stale.

- Widen the heartbeat schemas in @mastra/server (create/update body, view, and
  the schedule target) to the object shape with shared ifActive/ifIdle/attributes
  fragments; thread tagName/attributes/providerOptions through the create/update
  handlers.
- Mirror the same shape on the handwritten @mastra/client-js Heartbeat,
  CreateHeartbeatInput, and UpdateHeartbeatOptions types.
- Regenerate route-types.generated.ts and route-metadata.generated.ts so build
  output validation is clean.
- Fix resolveHeartbeatId: an id that already carries the hb_ prefix is a
  fully-formed stored id and must resolve verbatim. Re-slugifying it mangled
  characters create already accepted, making the heartbeat unaddressable via
  get/update/delete. Bare caller ids still canonicalize to hb_<slug>.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
…ixture

The custom-id symmetry fix made heartbeat lookups (get/update/delete/
pause/resume/run) canonicalize a bare path-param id to hb_<slug> before
hitting storage. The shared adapter route fixture seeded a bare
'test-heartbeat' id, which no longer matched the canonicalized lookup,
so every heartbeat route returned 404.

Real heartbeats are always stored with the hb_ prefix, so seed the
fixture (and its path-param default) as 'hb_test-heartbeat' to match
production id shape. resolveHeartbeatId returns hb_-prefixed ids
verbatim, so the lookup now resolves.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
The broadcast field was advisory-only: it was stamped on
providerOptions.mastra.heartbeat but nothing read it, so it had no
effect on a heartbeat run's behavior. Removing it keeps the v1 surface
honest rather than shipping a dead knob. Future channel-broadcast policy
will ride ifIdle.streamOptions.requestContext via a well-known key, which
scopes the concern to channels instead of a top-level heartbeat field.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
The test asserted providerOptions.mastra.heartbeat passthrough on a
downstream stream chunk that no consumer reads, and its comments
described AgentChannels heartbeat detection that was never built. The
real worker-boundary contract (providerOptions merged with heartbeat
run metadata) is already covered by worker.test.ts.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
…ate()

create() already rejects signalType/ifActive/ifIdle on threadless
heartbeats. Mirror that guard in update() so the invalid state can't be
set after the fact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: critical Critical-complexity PR Documentation Improvements or additions to documentation tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants