docs/channels/bluebubbles.md
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. Recommended for iMessage integration due to its richer API and easier setup compared to the legacy imsg channel.
<Note> Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not need a separate `openclaw plugins install` step. </Note>GET /api/v1/ping, POST /message/text, POST /chat/:id/*)./channels/pairing etc) with channels.bluebubbles.allowFrom + pairing codes.```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```
channels.bluebubbles.password (for example ?password=<password> or x-password), regardless of loopback/proxy topology.Some macOS VM / always-on setups can end up with Messages.app going "idle" (incoming events stop until the app is opened/foregrounded). A simple workaround is to poke Messages every 5 minutes using an AppleScript + LaunchAgent.
<Steps> <Step title="Save the AppleScript"> Save this as `~/Scripts/poke-messages.scpt`:```applescript
try
tell application "Messages"
if not running then
launch
end if
-- Touch the scripting interface to keep the process responsive.
set _chatCount to (count of chats)
end tell
on error
-- Ignore transient failures (first-run prompts, locked session, etc).
end try
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.poke-messages</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>/usr/bin/osascript "$HOME/Scripts/poke-messages.scpt"</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>StandardOutPath</key>
<string>/tmp/poke-messages.log</string>
<key>StandardErrorPath</key>
<string>/tmp/poke-messages.err</string>
</dict>
</plist>
```
This runs **every 300 seconds** and **on login**. The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
BlueBubbles is available in interactive onboarding:
openclaw onboard
The wizard prompts for:
<ParamField path="Server URL" type="string" required> BlueBubbles server address (e.g., `http://192.168.1.100:1234`). </ParamField> <ParamField path="Password" type="string" required> API password from BlueBubbles Server settings. </ParamField> <ParamField path="Webhook path" type="string" default="/bluebubbles-webhook"> Webhook endpoint path. </ParamField> <ParamField path="DM policy" type="string"> `pairing`, `allowlist`, `open`, or `disabled`. </ParamField> <ParamField path="Allow list" type="string[]"> Phone numbers, emails, or chat targets. </ParamField>You can also add BlueBubbles via CLI:
openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --password <password>
BlueBubbles group webhooks often only include raw participant addresses. If you want GroupMembers context to show local contact names instead, you can opt in to local Contacts enrichment on macOS:
channels.bluebubbles.enrichGroupParticipantsFromContacts = true enables the lookup. Default: false.{
channels: {
bluebubbles: {
enrichGroupParticipantsFromContacts: true,
},
},
}
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
agents.list[].groupChat.mentionPatterns (or messages.groupChat.mentionPatterns) to detect mentions.requireMention is enabled for a group, the agent only responds when mentioned.Per-group configuration:
{
channels: {
bluebubbles: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"*": { requireMention: true }, // default for all groups
"iMessage;-;chat123": { requireMention: false }, // override for specific group
},
},
},
}
/config, /model) require authorization.allowFrom and groupAllowFrom to determine command authorization.Each entry under channels.bluebubbles.groups.* accepts an optional systemPrompt string. The value is injected into the agent's system prompt on every turn that handles a message in that group, so you can set per-group persona or behavioral rules without editing agent prompts:
{
channels: {
bluebubbles: {
groups: {
"iMessage;-;chat123": {
systemPrompt: "Keep responses under 3 sentences. Mirror the group's casual tone.",
},
},
},
},
}
The key matches whatever BlueBubbles reports as chatGuid / chatIdentifier / numeric chatId for the group, and a "*" wildcard entry provides a default for every group without an exact match (same pattern used by requireMention and per-group tool policies). Exact matches always win over the wildcard. DMs ignore this field; use agent-level or account-level prompt customization instead.
With the BlueBubbles Private API enabled, inbound messages arrive with short message IDs (for example [[reply_to:5]]) and the agent can call action=reply to thread into a specific message or action=react to drop a tapback. A per-group systemPrompt is a reliable way to keep the agent choosing the right tool:
{
channels: {
bluebubbles: {
groups: {
"iMessage;+;chat-family": {
systemPrompt: "When replying in this group, always call action=reply with the [[reply_to:N]] messageId from context so your response threads under the triggering message. Never send a new unlinked message. For short acknowledgements ('ok', 'got it', 'on it'), use action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓) instead of sending a text reply.",
},
},
},
},
}
Tapback reactions and threaded replies both require the BlueBubbles Private API; see Advanced actions and Message IDs for the underlying mechanics.
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer.
Fast operator flow:
/acp spawn codex --bind here inside the DM or allowed group chat./new and /reset reset the same bound ACP session in place./acp close closes the ACP session and removes the binding.Configured persistent bindings are also supported through top-level bindings[] entries with type: "acp" and match.channel: "bluebubbles".
match.peer.id can use any supported BlueBubbles target form:
+15555550123 or [email protected]chat_id:<id>chat_guid:<guid>chat_identifier:<identifier>For stable group bindings, prefer chat_id:* or chat_identifier:*.
Example:
{
agents: {
list: [
{
id: "codex",
runtime: {
type: "acp",
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
},
},
],
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "bluebubbles",
accountId: "default",
peer: { kind: "dm", id: "+15555550123" },
},
acp: { label: "codex-imessage" },
},
],
}
See ACP Agents for shared ACP binding behavior.
channels.bluebubbles.sendReadReceipts (default: true).{
channels: {
bluebubbles: {
sendReadReceipts: false, // disable read receipts
},
},
}
BlueBubbles supports advanced message actions when enabled in config:
{
channels: {
bluebubbles: {
actions: {
reactions: true, // tapbacks (default: true)
edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
unsend: true, // unsend messages (macOS 13+)
reply: true, // reply threading by message GUID
sendWithEffect: true, // message effects (slam, loud, etc.)
renameGroup: true, // rename group chats
setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
addParticipant: true, // add participants to groups
removeParticipant: true, // remove participants from groups
leaveGroup: true, // leave group chats
sendAttachment: true, // send attachments/media
},
},
},
}
OpenClaw may surface short message IDs (e.g., 1, 2) to save tokens.
MessageSid / ReplyToId can be short IDs.MessageSidFull / ReplyToIdFull contain the provider full IDs.messageId, but short IDs will error if no longer available.Use full IDs for durable automations and storage:
{{MessageSidFull}}, {{ReplyToIdFull}}MessageSidFull / ReplyToIdFull in inbound payloadsSee Configuration for template variables.
<a id="coalescing-split-send-dms-command--url-in-one-composition"></a>
When a user types a command and a URL together in iMessage — e.g. Dump https://example.com/article — Apple splits the send into two separate webhook deliveries:
"Dump")."https://...") with OG-preview images as attachments.The two webhooks arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent receives the command alone on turn 1, replies (often "send me the URL"), and only sees the URL on turn 2 — at which point the command context is already lost.
channels.bluebubbles.coalesceSameSenderDms opts a DM into merging consecutive same-sender webhooks into a single agent turn. Group chats continue to key per-message so multi-user turn structure is preserved.
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
Leave disabled when:
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required — Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
// or under memory pressure (observed gap can stretch past 2 s then).
bluebubbles: 2500,
},
},
},
}
```
| User composes | Apple delivers | Flag off (default) | Flag on + 2500 ms window |
|---|---|---|---|
Dump https://example.com (one send) | 2 webhooks ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text Dump https://example.com |
Save this 📎image.jpg caption (attachment + text) | 2 webhooks | Two turns | One turn: text + image |
/status (standalone command) | 1 webhook | Instant dispatch | Wait up to window, then dispatch |
| URL pasted alone | 1 webhook | Instant dispatch | Instant dispatch (only one entry in bucket) |
| Text + URL sent as two deliberate separate messages, minutes apart | 2 webhooks outside window | Two turns | Two turns (window expires between them) |
| Rapid flood (>10 small DMs inside window) | N webhooks | N turns | One turn, bounded output (first + latest, text/attachment caps applied) |
If the flag is on and split-sends still arrive as two turns, check each layer:
<AccordionGroup> <Accordion title="Config actually loaded"> ``` grep coalesceSameSenderDms ~/.openclaw/openclaw.json ```Then `openclaw gateway restart` — the flag is read at debouncer-registry creation.
```
grep -E "Dispatching event to webhook" main.log | tail -20
```
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
Control whether responses are sent as a single message or streamed in blocks:
{
channels: {
bluebubbles: {
blockStreaming: true, // enable block streaming (off by default)
},
},
}
channels.bluebubbles.mediaMaxMb for inbound and outbound media (default: 8 MB).channels.bluebubbles.textChunkLimit (default: 4000 chars).Full configuration: Configuration
<AccordionGroup> <Accordion title="Connection and webhook"> - `channels.bluebubbles.enabled`: Enable/disable the channel. - `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL. - `channels.bluebubbles.password`: API password. - `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`). </Accordion> <Accordion title="Access policy"> - `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`). - `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`). - `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`). - `channels.bluebubbles.groupAllowFrom`: Group sender allowlist. - `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`. - `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.). </Accordion> <Accordion title="Delivery and chunking"> - `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`). - `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies). - `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000). - `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`. - `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking. </Accordion> <Accordion title="Media and history"> - `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8). - `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`. - `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`. - `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables). - `channels.bluebubbles.dmHistoryLimit`: DM history limit. - `channels.bluebubbles.replyContextApiFallback`: When an inbound reply lands without `replyToBody`/`replyToSender` and the in-memory reply-context cache misses, fetch the original message from the BlueBubbles HTTP API as a best-effort fallback (default: `false`). Useful for multi-instance deployments sharing one BlueBubbles account, after process restarts, or after long-lived TTL/LRU cache eviction. The fetch is SSRF-guarded by the same policy as every other BlueBubbles client request, never throws, and populates the cache so subsequent replies amortize. Per-account override: `channels.bluebubbles.accounts.<accountId>.replyContextApiFallback`. A channel-level setting propagates to accounts that omit the flag. </Accordion> <Accordion title="Actions and accounts"> - `channels.bluebubbles.actions`: Enable/disable specific actions. - `channels.bluebubbles.accounts`: Multi-account configuration. </Accordion> </AccordionGroup>Related global options:
agents.list[].groupChat.mentionPatterns (or messages.groupChat.mentionPatterns).messages.responsePrefix.Prefer chat_guid for stable routing:
chat_guid:iMessage;-;+15555550123 (preferred for groups)chat_id:123chat_identifier:...+15555550123, [email protected]
POST /api/v1/chat/new. This requires the BlueBubbles Private API to be enabled.When the same handle has both an iMessage and an SMS chat on the Mac (for example a phone number that is iMessage-registered but has also received green-bubble fallbacks), OpenClaw prefers the iMessage chat and never silently downgrades to SMS. To force the SMS chat, use an explicit sms: target prefix (for example sms:+15555550123). Handles without a matching iMessage chat still send through whatever chat BlueBubbles reports.
guid/password query params or headers against channels.bluebubbles.password.gateway.trustedProxies does not replace channels.bluebubbles.password here. See Gateway security.channels.bluebubbles.webhookPath.openclaw pairing list bluebubbles and openclaw pairing approve bluebubbles <code>.POST /api/v1/message/react); ensure the server version exposes it.channels.bluebubbles.actions.edit=false.coalesceSameSenderDms enabled but split-sends (e.g. Dump + URL) still arrive as two turns: see the split-send coalescing troubleshooting checklist — common causes are too-tight debounce window, session-log timestamps misread as webhook arrival, or a reply-quote send (which uses replyToBody, not a second webhook).openclaw status --all or openclaw status --deep.For general channel workflow reference, see Channels and the Plugins guide.