> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wednesdayai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Heartbeat

# Heartbeat (Gateway)

> **Heartbeat vs Cron?** See [Cron vs heartbeat](/users/cron#cron-vs-heartbeat) for guidance on when to use each.

Heartbeat runs **periodic agent turns** on a configured session so the model can
surface anything that needs attention without spamming you. By default that is
the agent main session; for high-traffic sessions, use `isolatedSession: true`
so each heartbeat runs in a fresh sibling transcript while delivery still uses
the base session.

Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)

## Quick start (beginner)

1. Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/setup-token) or set your own cadence.
2. Create a tiny `HEARTBEAT.md` checklist in the agent workspace (optional but recommended).
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
4. Recommended for active agents: enable `lightContext`, `isolatedSession`, and `skipWhenBusy`.
5. Optional: enable heartbeat reasoning delivery for transparency.
6. Optional: restrict heartbeats to active hours (local time).

Example config:

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",
        target: "last", // explicit delivery to last contact (default is "none")
        directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
        lightContext: true, // keep heartbeat context small
        isolatedSession: true, // run in a fresh sibling transcript
        skipWhenBusy: true, // defer while this agent has nested work in flight
        // activeHours: { start: "08:00", end: "24:00" },
        // includeReasoning: true, // optional: send separate `Reasoning:` message too
      },
    },
  },
}
```

## Defaults

* Interval: `30m` (or `1h` when Anthropic OAuth/setup-token is the detected auth mode). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
* Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
  `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
* The heartbeat prompt is sent **verbatim** as the user message. The system
  prompt includes a “Heartbeat” section and the run is flagged internally.
* Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
  Outside the window, heartbeats are skipped until the next tick inside the window.

## Configuration

| Key                         | Type               | Default       | Description                                                                                                                        |
| --------------------------- | ------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `every`                     | duration string    | `"30m"`       | Heartbeat interval. `0m` disables. Default is `1h` when Anthropic OAuth/setup-token is detected.                                   |
| `model`                     | string             | —             | Model override for heartbeat runs (`provider/model`).                                                                              |
| `target`                    | string             | `"none"`      | Delivery target: `none`, `last`, or an explicit channel id (`whatsapp`, `telegram`, `discord`, `slack`, etc.).                     |
| `to`                        | string             | —             | Channel-specific recipient override (e.g. E.164 for WhatsApp, Telegram chat id). For Telegram topics: `<chatId>:topic:<threadId>`. |
| `accountId`                 | string             | —             | Account id for multi-account channels (e.g. Telegram with multiple bots).                                                          |
| `identity`                  | string             | —             | Attribute heartbeat runs to a registered identity. See [Identity attribution](#identity-attribution).                              |
| `directPolicy`              | `allow` \| `block` | `allow`       | Controls direct/DM delivery. `block` suppresses DM delivery while still running the turn.                                          |
| `prompt`                    | string             | *(see below)* | Overrides the default prompt body (sent verbatim; not merged).                                                                     |
| `ackMaxChars`               | number             | `300`         | Max chars allowed after `HEARTBEAT_OK` before the reply is delivered.                                                              |
| `lightContext`              | boolean            | —             | Use lightweight bootstrap context; keeps only heartbeat-specific files like `HEARTBEAT.md`.                                        |
| `isolatedSession`           | boolean            | —             | Each tick runs in a fresh sibling `:heartbeat` session. Delivery and duplicate suppression still use the base session.             |
| `skipWhenBusy`              | boolean            | —             | Skip this agent's heartbeat while its session-keyed subagent or nested lanes are busy.                                             |
| `activeHours`               | object             | —             | Restrict heartbeats to a time window. See [Active hours](#active-hours).                                                           |
| `includeReasoning`          | boolean            | `false`       | Also deliver a separate `Reasoning:` message when extended thinking is available.                                                  |
| `session`                   | string             | `"main"`      | Session key for the heartbeat run. When `isolatedSession: true`, set the base key here — the runtime appends `:heartbeat`.         |
| `suppressToolErrorWarnings` | boolean            | —             | Suppress tool-error warning payloads during heartbeat runs.                                                                        |
| `wakeGate`                  | object             | —             | Cheap declarative check that decides whether the persona turn runs. See [Wake-gate](#wake-gate).                                   |

## What the heartbeat prompt is for

The default prompt is intentionally broad:

* **Background tasks**: “Consider outstanding tasks” nudges the agent to review
  follow-ups (inbox, calendar, reminders, queued work) and surface anything urgent.
* **Human check-in**: “Checkup sometimes on your human during day time” nudges an
  occasional lightweight “anything you need?” message, but avoids night-time spam
  by using your configured local timezone (see [/concepts/timezone](/concepts/timezone)).

If you want a heartbeat to do something very specific (e.g. “check Gmail PubSub
stats” or “verify gateway health”), set `agents.defaults.heartbeat.prompt` (or
`agents.list[].heartbeat.prompt`) to a custom body (sent verbatim).

## Response contract

* If nothing needs attention, reply with **`HEARTBEAT_OK`**.
* During heartbeat runs, OpenClaw treats `HEARTBEAT_OK` as an ack when it appears
  at the **start or end** of the reply. The token is stripped and the reply is
  dropped if the remaining content is **≤ `ackMaxChars`** (default: 300).
* If `HEARTBEAT_OK` appears in the **middle** of a reply, it is not treated
  specially.
* For alerts, **do not** include `HEARTBEAT_OK`; return only the alert text.

Outside heartbeats, stray `HEARTBEAT_OK` at the start/end of a message is stripped
and logged; a message that is only `HEARTBEAT_OK` is dropped.

## Config

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m", // default: 30m (0m disables)
        model: "anthropic/claude-opus-4-6",
        includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
        target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
        to: "+15551234567", // optional channel-specific override
        accountId: "ops-bot", // optional multi-account channel id
        prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
        ackMaxChars: 300, // max chars allowed after HEARTBEAT_OK
        lightContext: true,
        isolatedSession: true,
        skipWhenBusy: true,
      },
    },
  },
}
```

### Scope and precedence

* `agents.defaults.heartbeat` sets global heartbeat behavior.
* `agents.list[].heartbeat` merges on top; if any agent has a `heartbeat` block, **only those agents** run heartbeats.
* `channels.defaults.heartbeat` sets visibility defaults for all channels.
* `channels.<channel>.heartbeat` overrides channel defaults.
* `channels.<channel>.accounts.<id>.heartbeat` (multi-account channels) overrides per-channel settings.

### Per-agent heartbeats

If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
run heartbeats. The per-agent block merges on top of `agents.defaults.heartbeat`
(so you can set shared defaults once and override per agent).

Example: two agents, only the second agent runs heartbeats.

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",
        target: "last", // explicit delivery to last contact (default is "none")
      },
    },
    list: [
      { id: "main", default: true },
      {
        id: "ops",
        heartbeat: {
          every: "1h",
          target: "whatsapp",
          to: "+15551234567",
          prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
        },
      },
    ],
  },
}
```

### Active hours

Restrict heartbeats to business hours in a specific timezone:

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",
        target: "last", // explicit delivery to last contact (default is "none")
        activeHours: {
          start: "09:00",
          end: "22:00",
          timezone: "America/New_York", // optional; uses your userTimezone if set, otherwise host tz
        },
      },
    },
  },
}
```

Outside this window (before 9am or after 10pm Eastern), heartbeats are skipped. The next scheduled tick inside the window will run normally.

### 24/7 setup

If you want heartbeats to run all day, use one of these patterns:

* Omit `activeHours` entirely (no time-window restriction; this is the default behavior).
* Set a full-day window: `activeHours: { start: "00:00", end: "24:00" }`.

Do not set the same `start` and `end` time (for example `08:00` to `08:00`).
That is treated as a zero-width window, so heartbeats are always skipped.

### Multi account example

Use `accountId` to target a specific account on multi-account channels like Telegram:

```json5 theme={"dark"}
{
  agents: {
    list: [
      {
        id: "ops",
        heartbeat: {
          every: "1h",
          target: "telegram",
          to: "12345678:topic:42", // optional: route to a specific topic/thread
          accountId: "ops-bot",
        },
      },
    ],
  },
  channels: {
    telegram: {
      accounts: {
        "ops-bot": { botToken: "YOUR_TELEGRAM_BOT_TOKEN" },
      },
    },
  },
}
```

### Field notes

* `every`: heartbeat interval (duration string; default unit = minutes).
* `model`: optional model override for heartbeat runs (`provider/model`).
* `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
* `session`: optional session key for heartbeat runs.
  * `main` (default): agent main session.
  * Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
  * Session key formats: see [Sessions](/concepts/session) and [Groups](/channels/groups).
  * When `isolatedSession: true`, configure the base session key here. Do not
    pre-append `:heartbeat`; the runtime creates and refreshes the sibling
    `:heartbeat` session itself.
* `target`:
  * `last`: deliver to the last used external channel.
  * explicit channel: `whatsapp` / `telegram` / `discord` / `googlechat` / `slack` / `msteams` / `signal` / `imessage`.
  * `none` (default): run the heartbeat but **do not deliver** externally.
* `directPolicy`: controls direct/DM delivery behavior:
  * `allow` (default): allow direct/DM heartbeat delivery.
  * `block`: suppress direct/DM delivery (`reason=dm-blocked`).
* `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). For Telegram topics/threads, use `<chatId>:topic:<messageThreadId>`.
* `accountId`: optional account id for multi-account channels. When `target: "last"`, the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.
* `prompt`: overrides the default prompt body (not merged).
* `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
* `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
* `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only heartbeat-specific bootstrap files such as `HEARTBEAT.md`.
* `isolatedSession`: when true, each heartbeat runs in a fresh sibling `:heartbeat` session with no prior conversation transcript. Delivery routing and duplicate suppression still use the base session. The `:heartbeat` session key suffix causes the storage layer to tag these entries with `trigger_source = 'heartbeat'`; session recall (`readByKey`) and analytics queries automatically exclude them so heartbeat turns do not appear in conversation history or inflate per-turn metrics. See [Session History Hygiene](/concepts/session-history-hygiene).
  * Event ownership: the run session key is resolved **before** the preflight queue peek, so an isolated run only ever peeks its own `:heartbeat` queue — never the base session's. `signal:*` prompt events are the exception that can transfer: when the run session holds signal events, the turn is served as a signal-event turn and may relay to the user through the base lane.
* `skipWhenBusy`: when true, also skips this agent's heartbeat while its session-keyed subagent or nested lanes are busy.
* `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
  * Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
  * `"local"`: always uses the host system timezone.
  * Any IANA identifier (e.g. `America/New_York`): used directly; if invalid, falls back to the `"user"` behavior above.
  * `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
  * Outside the active window, heartbeats are skipped until the next tick inside the window.

## Delivery behavior

* Heartbeats run in the agent’s main session by default (`agent:<id>:<mainKey>`),
  or `global` when `session.scope = "global"`. Set `session` to override to a
  specific channel session (Discord/WhatsApp/etc.).
* `session` only affects the run context; delivery is controlled by `target` and `to`.
* To deliver to a specific channel/recipient, set `target` + `to`. With
  `target: "last"`, delivery uses the last external channel for that session.
* Heartbeat deliveries allow direct/DM targets by default. Set `directPolicy: "block"` to suppress direct-target sends while still running the heartbeat turn.
* If the main queue, cron queue, or target session lane is busy, the heartbeat is skipped and retried later.
* With `skipWhenBusy: true`, this agent's own session-keyed subagent/nested lanes also defer heartbeat runs. Other agents' busy lanes do not defer this agent.
* If `target` resolves to no external destination, the run still happens but no
  outbound message is sent.
* Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
  is restored so idle expiry behaves normally.

## Identity attribution

Key: `heartbeat.identity` — default unset (the heartbeat runs as a pure system turn with no identity).

Set it to a registered identity key (a `session.identityLinks` entry, matched case-insensitively) to run the heartbeat under that user's identity lane — the turn is attributed to them the same way their chat-initiated turns are, instead of running identity-less. An unmatched value is never fabricated into an identity: the run simply stays unattributed.

This attribute governs the **run**, not the destination: `target` / `to` / `accountId` still decide where check-ins are delivered. The companion behaviour on the cron side is identity-linked delivery, where a `delivery.to` holding an identity key resolves to the channel-native chat id linked to that identity — see [Cron jobs: Identity attribution](/admin/automation/cron-jobs#identity-attribution).

## Visibility controls

By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
delivered. You can adjust this per channel or per account:

```yaml theme={"dark"}
channels:
  defaults:
    heartbeat:
      showOk: false # Hide HEARTBEAT_OK (default)
      showAlerts: true # Show alert messages (default)
      useIndicator: true # Emit indicator events (default)
  telegram:
    heartbeat:
      showOk: true # Show OK acknowledgments on Telegram
  whatsapp:
    accounts:
      work:
        heartbeat:
          showAlerts: false # Suppress alert delivery for this account
```

Precedence: per-account → per-channel → channel defaults → built-in defaults.

### What each flag does

* `showOk`: sends a `HEARTBEAT_OK` acknowledgment when the model returns an OK-only reply.
* `showAlerts`: sends the alert content when the model returns a non-OK reply.
* `useIndicator`: emits indicator events for UI status surfaces.

If **all three** are false, OpenClaw skips the heartbeat run entirely (no model call).

### Per-channel vs per-account examples

```yaml theme={"dark"}
channels:
  defaults:
    heartbeat:
      showOk: false
      showAlerts: true
      useIndicator: true
  slack:
    heartbeat:
      showOk: true # all Slack accounts
    accounts:
      ops:
        heartbeat:
          showAlerts: false # suppress alerts for the ops account only
  telegram:
    heartbeat:
      showOk: true
```

### Common patterns

| Goal                                     | Config                                                                                   |
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| Default behavior (silent OKs, alerts on) | *(no config needed)*                                                                     |
| Fully silent (no messages, no indicator) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false }` |
| Indicator-only (no messages)             | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true }`  |
| OKs in one channel only                  | `channels.telegram.heartbeat: { showOk: true }`                                          |

## HEARTBEAT.md (optional)

If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.

If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
If the file is missing, the heartbeat still runs and the model decides what to do.

When `wakeGate` is configured, the empty-file check is bypassed — the gate
evaluates regardless of `HEARTBEAT.md` content. If the gate is negative, the
heartbeat is still skipped (with reason `wake-gate-empty`); if the gate is
positive, the persona turn runs.

Keep it tiny (short checklist or reminders) to avoid prompt bloat.

Example `HEARTBEAT.md`:

```md theme={"dark"}
# Heartbeat checklist

- Quick scan: anything urgent in inboxes?
- If it’s daytime, do a lightweight check-in if nothing else is pending.
- If a task is blocked, write down _what is missing_ and ask Peter next time.
```

### Can the agent update HEARTBEAT.md?

Yes — if you ask it to.

`HEARTBEAT.md` is just a normal file in the agent workspace, so you can tell the
agent (in a normal chat) something like:

* “Update `HEARTBEAT.md` to add a daily calendar check.”
* “Rewrite `HEARTBEAT.md` so it’s shorter and focused on inbox follow-ups.”

If you want this to happen proactively, you can also include an explicit line in
your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md
with a better one.”

Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
`HEARTBEAT.md` — it becomes part of the prompt context.

## Manual wake (on-demand)

You can enqueue a system event and trigger an immediate heartbeat with:

```bash theme={"dark"}
openclaw system event --text "Check for urgent follow-ups" --mode now
```

If multiple agents have `heartbeat` configured, a manual wake runs each of those
agent heartbeats immediately.

Use `--mode next-heartbeat` to wait for the next scheduled tick.

## Nudge: wake the main persona

A **nudge** hands a payload to an agent's **main session** as non-user context and wakes the main
persona to decide what to do. Detection is cheap and lives in the caller; action is expensive and
lives in the persona. A nudge is **not** a new signal mode — it is a named operation over the
existing heartbeat bridge (`enqueueSystemEvent` + a targeted heartbeat wake), so it inherits every
heartbeat gate.

The nudge contract is one payload shared by every trigger surface:

* **`text`** (required): the human-readable context the persona sees.
* **`data`** (optional): a structured JSON object, rendered into the system-event text and also
  carried raw for programmatic subscribers.
* **target** (optional): `agentId` or an explicit `sessionKey`; defaults to the default agent's main
  session.

**Silent-unless-it-decides.** The persona receives the payload as non-user context and produces a
user-facing message **only when warranted**, otherwise acknowledges with `HEARTBEAT_OK` and no
delivery. A nudge whose check found nothing actionable sends no message.

**Gates are honoured.** A nudge routes through the heartbeat dispatch, so active-hours, the
main/cron/subagent/session lane busy checks, `skipWhenBusy`, and duplicate suppression all apply. A
nudge against a busy target session defers; it does not bypass any check.

The nudge is reachable from four trigger surfaces, all producing the same main-session injection and
persona wake:

* **Cron** — a job with a `wakeGate` payload (see [Cron jobs](/admin/automation/cron-jobs))
* **Heartbeat** — a heartbeat configured with a `wakeGate` (see [Wake-gate](#wake-gate) below)
* **Plugin / hook** — `api.runtime.signals.nudge(...)` (see [Agent Signals](/developers/agent-signals#nudge))
* **CLI** — `openclaw agent nudge`

### Plugin / hook example

```ts theme={"dark"}
api.runtime.signals.nudge({
  agentId: "ops",
  text: "Calendar: standup starts in 10 minutes.",
  data: { eventId: "evt_123", startsInMin: 10 },
  reason: "calendar",
});
```

## Wake-gate: gate the expensive persona turn

A **wake-gate** is a cheap, declarative check that decides whether the expensive persona turn runs
at all. On a negative result **no main-session persona turn runs** — the turn is gated, not silenced
after running. Set `agents.defaults.heartbeat.wakeGate` (or per-agent
`agents.list[].heartbeat.wakeGate`) to turn a heartbeat into a wake-gate.

There are two gate kinds:

* **`command`** — runs a shell command and parses its **last non-empty stdout line** as JSON
  `{ "wakeAgent": boolean, "text"?: string, "data"?: unknown }`. A non-zero exit, a timeout,
  unparseable output, or `wakeAgent !== true`, all mean "do not wake". This kind invokes **no model
  runner at all** — the cheapest path, ideal for git/curl/SQL checks.
* **`model`** — runs an isolated cheap turn (a sibling session on the `Analysis` lane, with optional
  `model`/`provider`/`lightContext` overrides) that inspects sources and returns a structured wake
  decision. The gate evaluation stays in the isolated lane and never touches the main session.

When the gate signals wake, its `text` (and rendered `data`) is folded into **this** heartbeat
turn's prompt before the persona runs. When the gate is negative, the heartbeat is skipped with the
reason `wake-gate-empty` and the persona is not invoked.

Default behaviour is unchanged: a heartbeat with no `wakeGate` set runs exactly as it does today.

### Command wake-gate example

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "15m",
        target: "last",
        wakeGate: {
          kind: "command",
          // Emits e.g. {"wakeAgent":true,"text":"2 unread","data":{"n":2}} on its last stdout line.
          command: "node ./scripts/inbox-check.mjs",
          timeoutMs: 10000, // optional; default is 30000 ms
        },
      },
    },
  },
}
```

Command wake-gates run through the Gateway shell (`/bin/sh -c` on Unix, `cmd.exe /c` on Windows).
Stdout and stderr are drained so noisy checks do not hang the process; the retained output window is
bounded to the last 64 KiB, and only the final non-empty stdout line is parsed as the wake decision.
If the command exceeds `timeoutMs` or the heartbeat runner is stopped, the Gateway terminates the
gate. On Unix it signals the whole process group, then force-kills stubborn processes after a short
grace period. A stopped or timed-out gate resolves as "do not wake" instead of crashing the
heartbeat runner.

<Warning>
  Command wake-gates execute with the same host permissions as the Gateway. Only configure commands
  you trust, keep them idempotent, and prefer short timeouts for checks that call external services.
</Warning>

### Model wake-gate example

```json5 theme={"dark"}
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",
        wakeGate: {
          kind: "model",
          model: "anthropic/claude-haiku-4-5",
          lightContext: true,
          prompt: "Scan recent activity. Decide whether the main persona should engage.",
        },
      },
    },
  },
}
```

Model wake-gates use the same `timeoutMs` default as command gates (30000 ms). They also receive
heartbeat cancellation, so a Gateway shutdown or aborted heartbeat run stops the isolated analysis
turn instead of leaving it running in the background.

### Wake-gate gateway logs

The Gateway emits structured log lines at the following levels so operators can observe and
troubleshoot wake-gate decisions without enabling verbose debug mode:

| Condition                                                      | Level   | Fields                                                                   |
| -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
| Command gate: non-zero exit, empty output, or unparseable JSON | `warn`  | `exit`, `tail` (last ≤512 chars of stdout)                               |
| Model gate: empty output                                       | `warn`  | *(none beyond `kind`)*                                                   |
| Model gate: unparseable JSON                                   | `warn`  | `tail` (last ≤512 chars of model output)                                 |
| Gate returned `wakeAgent: false`                               | `debug` | `tail` (bounded stdout or model output)                                  |
| Gate returned `wakeAgent: true`                                | `info`  | `hasText`, `hasData` (boolean flags only — **never** the payload values) |
| Run suppressed by a negative gate                              | `debug` | `agentId`, `reason: "wake-gate-empty"`                                   |

The `info`-level wake-true log intentionally carries only boolean presence flags (`hasText`,
`hasData`) to prevent payload content and secrets from appearing in the gateway log.

To see gate-false and suppression decisions in real time, set the logging level to `debug`:

```bash theme={"dark"}
wednesdayai logs --follow
```

## Reasoning delivery (optional)

By default, heartbeats deliver only the final “answer” payload.

If you want transparency, enable:

* `agents.defaults.heartbeat.includeReasoning: true`

When enabled, heartbeats will also deliver a separate message prefixed
`Reasoning:` (same shape as `/reasoning on`). This can be useful when the agent
is managing multiple sessions/codexes and you want to see why it decided to ping
you — but it can also leak more internal detail than you want. Prefer keeping it
off in group chats.

## Cost awareness

Heartbeats run full agent turns. Shorter intervals burn more tokens. Keep
`HEARTBEAT.md` small and consider a cheaper `model` or `target: "none"` if you
only want internal state updates.
