6 custom skills (assign-task, dispatch-webhook, daily-briefing, task-capture, qmd-brain, tts-voice) with technical documentation. Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
1.5 MiB
Executable File
Auth Monitoring
Source: https://docs.openclaw.ai/automation/auth-monitoring
Auth monitoring
OpenClaw exposes OAuth expiry health via openclaw models status. Use that for
automation and alerting; scripts are optional extras for phone workflows.
Preferred: CLI check (portable)
openclaw models status --check
Exit codes:
0: OK1: expired or missing credentials2: expiring soon (within 24h)
This works in cron/systemd and requires no extra scripts.
Optional scripts (ops / phone workflows)
These live under scripts/ and are optional. They assume SSH access to the
gateway host and are tuned for systemd + Termux.
scripts/claude-auth-status.shnow usesopenclaw models status --jsonas the source of truth (falling back to direct file reads if the CLI is unavailable), so keepopenclawonPATHfor timers.scripts/auth-monitor.sh: cron/systemd timer target; sends alerts (ntfy or phone).scripts/systemd/openclaw-auth-monitor.{service,timer}: systemd user timer.scripts/claude-auth-status.sh: Claude Code + OpenClaw auth checker (full/json/simple).scripts/mobile-reauth.sh: guided re‑auth flow over SSH.scripts/termux-quick-auth.sh: one‑tap widget status + open auth URL.scripts/termux-auth-widget.sh: full guided widget flow.scripts/termux-sync-widget.sh: sync Claude Code creds → OpenClaw.
If you don’t need phone automation or systemd timers, skip these scripts.
Cron Jobs
Source: https://docs.openclaw.ai/automation/cron-jobs
Cron jobs (Gateway scheduler)
Cron vs Heartbeat? See Cron vs Heartbeat for guidance on when to use each.
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at the right time, and can optionally deliver output back to a chat.
If you want “run this every morning” or “poke the agent in 20 minutes”, cron is the mechanism.
TL;DR
- Cron runs inside the Gateway (not inside the model).
- Jobs persist under
~/.openclaw/cron/so restarts don’t lose schedules. - Two execution styles:
- Main session: enqueue a system event, then run on the next heartbeat.
- Isolated: run a dedicated agent turn in
cron:<jobId>, with delivery (announce by default or none).
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
Quick start (actionable)
Create a one-shot reminder, verify it exists, and run it immediately:
openclaw cron add \
--name "Reminder" \
--at "2026-02-01T16:00:00Z" \
--session main \
--system-event "Reminder: check the cron docs draft" \
--wake now \
--delete-after-run
openclaw cron list
openclaw cron run <job-id> --force
openclaw cron runs --id <job-id>
Schedule a recurring isolated job with delivery:
openclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--announce \
--channel slack \
--to "channel:C1234567890"
Tool-call equivalents (Gateway cron tool)
For the canonical JSON shapes and examples, see JSON schema for tool calls.
Where cron jobs are stored
Cron jobs are persisted on the Gateway host at ~/.openclaw/cron/jobs.json by default.
The Gateway loads the file into memory and writes it back on changes, so manual edits
are only safe when the Gateway is stopped. Prefer openclaw cron add/edit or the cron
tool call API for changes.
Beginner-friendly overview
Think of a cron job as: when to run + what to do.
-
Choose a schedule
- One-shot reminder →
schedule.kind = "at"(CLI:--at) - Repeating job →
schedule.kind = "every"orschedule.kind = "cron" - If your ISO timestamp omits a timezone, it is treated as UTC.
- One-shot reminder →
-
Choose where it runs
sessionTarget: "main"→ run during the next heartbeat with main context.sessionTarget: "isolated"→ run a dedicated agent turn incron:<jobId>.
-
Choose the payload
- Main session →
payload.kind = "systemEvent" - Isolated session →
payload.kind = "agentTurn"
- Main session →
Optional: one-shot jobs (schedule.kind = "at") delete after success by default. Set
deleteAfterRun: false to keep them (they will disable after success).
Concepts
Jobs
A cron job is a stored record with:
- a schedule (when it should run),
- a payload (what it should do),
- optional delivery mode (announce or none).
- optional agent binding (
agentId): run the job under a specific agent; if missing or unknown, the gateway falls back to the default agent.
Jobs are identified by a stable jobId (used by CLI/Gateway APIs).
In agent tool calls, jobId is canonical; legacy id is accepted for compatibility.
One-shot jobs auto-delete after success by default; set deleteAfterRun: false to keep them.
Schedules
Cron supports three schedule kinds:
at: one-shot timestamp viaschedule.at(ISO 8601).every: fixed interval (ms).cron: 5-field cron expression with optional IANA timezone.
Cron expressions use croner. If a timezone is omitted, the Gateway host’s
local timezone is used.
Main vs isolated execution
Main session jobs (system events)
Main jobs enqueue a system event and optionally wake the heartbeat runner.
They must use payload.kind = "systemEvent".
wakeMode: "next-heartbeat"(default): event waits for the next scheduled heartbeat.wakeMode: "now": event triggers an immediate heartbeat run.
This is the best fit when you want the normal heartbeat prompt + main-session context. See Heartbeat.
Isolated jobs (dedicated cron sessions)
Isolated jobs run a dedicated agent turn in session cron:<jobId>.
Key behaviors:
- Prompt is prefixed with
[cron:<jobId> <job name>]for traceability. - Each run starts a fresh session id (no prior conversation carry-over).
- Default behavior: if
deliveryis omitted, isolated jobs announce a summary (delivery.mode = "announce"). delivery.mode(isolated-only) chooses what happens:announce: deliver a summary to the target channel and post a brief summary to the main session.none: internal only (no delivery, no main-session summary).
wakeModecontrols when the main-session summary posts:now: immediate heartbeat.next-heartbeat: waits for the next scheduled heartbeat.
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam your main chat history.
Payload shapes (what runs)
Two payload kinds are supported:
systemEvent: main-session only, routed through the heartbeat prompt.agentTurn: isolated-session only, runs a dedicated agent turn.
Common agentTurn fields:
message: required text prompt.model/thinking: optional overrides (see below).timeoutSeconds: optional timeout override.
Delivery config (isolated jobs only):
delivery.mode:none|announce.delivery.channel:lastor a specific channel.delivery.to: channel-specific target (phone/chat/channel id).delivery.bestEffort: avoid failing the job if announce delivery fails.
Announce delivery suppresses messaging tool sends for the run; use delivery.channel/delivery.to
to target the chat instead. When delivery.mode = "none", no summary is posted to the main session.
If delivery is omitted for isolated jobs, OpenClaw defaults to announce.
Announce delivery flow
When delivery.mode = "announce", cron delivers directly via the outbound channel adapters.
The main agent is not spun up to craft or forward the message.
Behavior details:
- Content: delivery uses the isolated run's outbound payloads (text/media) with normal chunking and channel formatting.
- Heartbeat-only responses (
HEARTBEAT_OKwith no real content) are not delivered. - If the isolated run already sent a message to the same target via the message tool, delivery is skipped to avoid duplicates.
- Missing or invalid delivery targets fail the job unless
delivery.bestEffort = true. - A short summary is posted to the main session only when
delivery.mode = "announce". - The main-session summary respects
wakeMode:nowtriggers an immediate heartbeat andnext-heartbeatwaits for the next scheduled heartbeat.
Model and thinking overrides
Isolated jobs (agentTurn) can override the model and thinking level:
model: Provider/model string (e.g.,anthropic/claude-sonnet-4-20250514) or alias (e.g.,opus)thinking: Thinking level (off,minimal,low,medium,high,xhigh; GPT-5.2 + Codex models only)
Note: You can set model on main-session jobs too, but it changes the shared main
session model. We recommend model overrides only for isolated jobs to avoid
unexpected context shifts.
Resolution priority:
- Job payload override (highest)
- Hook-specific defaults (e.g.,
hooks.gmail.model) - Agent config default
Delivery (channel + target)
Isolated jobs can deliver output to a channel via the top-level delivery config:
delivery.mode:announce(deliver a summary) ornone.delivery.channel:whatsapp/telegram/discord/slack/mattermost(plugin) /signal/imessage/last.delivery.to: channel-specific recipient target.
Delivery config is only valid for isolated jobs (sessionTarget: "isolated").
If delivery.channel or delivery.to is omitted, cron can fall back to the main session’s
“last route” (the last place the agent replied).
Target format reminders:
- Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g.
channel:<id>,user:<id>) to avoid ambiguity. - Telegram topics should use the
:topic:form (see below).
Telegram delivery targets (topics / forum threads)
Telegram supports forum topics via message_thread_id. For cron delivery, you can encode
the topic/thread into the to field:
-1001234567890(chat id only)-1001234567890:topic:123(preferred: explicit topic marker)-1001234567890:123(shorthand: numeric suffix)
Prefixed targets like telegram:... / telegram:group:... are also accepted:
telegram:group:-1001234567890:topic:123
JSON schema for tool calls
Use these shapes when calling Gateway cron.* tools directly (agent tool calls or RPC).
CLI flags accept human durations like 20m, but tool calls should use an ISO 8601 string
for schedule.at and milliseconds for schedule.everyMs.
cron.add params
One-shot, main session job (system event):
{
"name": "Reminder",
"schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" },
"sessionTarget": "main",
"wakeMode": "now",
"payload": { "kind": "systemEvent", "text": "Reminder text" },
"deleteAfterRun": true
}
Recurring, isolated job with delivery:
{
"name": "Morning brief",
"schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Summarize overnight updates."
},
"delivery": {
"mode": "announce",
"channel": "slack",
"to": "channel:C1234567890",
"bestEffort": true
}
}
Notes:
schedule.kind:at(at),every(everyMs), orcron(expr, optionaltz).schedule.ataccepts ISO 8601 (timezone optional; treated as UTC when omitted).everyMsis milliseconds.sessionTargetmust be"main"or"isolated"and must matchpayload.kind.- Optional fields:
agentId,description,enabled,deleteAfterRun(defaults to true forat),delivery. wakeModedefaults to"next-heartbeat"when omitted.
cron.update params
{
"jobId": "job-123",
"patch": {
"enabled": false,
"schedule": { "kind": "every", "everyMs": 3600000 }
}
}
Notes:
jobIdis canonical;idis accepted for compatibility.- Use
agentId: nullin the patch to clear an agent binding.
cron.run and cron.remove params
{ "jobId": "job-123", "mode": "force" }
{ "jobId": "job-123" }
Storage & history
- Job store:
~/.openclaw/cron/jobs.json(Gateway-managed JSON). - Run history:
~/.openclaw/cron/runs/<jobId>.jsonl(JSONL, auto-pruned). - Override store path:
cron.storein config.
Configuration
{
cron: {
enabled: true, // default true
store: "~/.openclaw/cron/jobs.json",
maxConcurrentRuns: 1, // default 1
},
}
Disable cron entirely:
cron.enabled: false(config)OPENCLAW_SKIP_CRON=1(env)
CLI quickstart
One-shot reminder (UTC ISO, auto-delete after success):
openclaw cron add \
--name "Send reminder" \
--at "2026-01-12T18:00:00Z" \
--session main \
--system-event "Reminder: submit expense report." \
--wake now \
--delete-after-run
One-shot reminder (main session, wake immediately):
openclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
Recurring isolated job (announce to WhatsApp):
openclaw cron add \
--name "Morning status" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize inbox + calendar for today." \
--announce \
--channel whatsapp \
--to "+15551234567"
Recurring isolated job (deliver to a Telegram topic):
openclaw cron add \
--name "Nightly summary (topic)" \
--cron "0 22 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize today; send to the nightly topic." \
--announce \
--channel telegram \
--to "-1001234567890:topic:123"
Isolated job with model and thinking override:
openclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--announce \
--channel whatsapp \
--to "+15551234567"
Agent selection (multi-agent setups):
# Pin a job to agent "ops" (falls back to default if that agent is missing)
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
# Switch or clear the agent on an existing job
openclaw cron edit <jobId> --agent ops
openclaw cron edit <jobId> --clear-agent
Manual run (debug):
openclaw cron run <jobId> --force
Edit an existing job (patch fields):
openclaw cron edit <jobId> \
--message "Updated prompt" \
--model "opus" \
--thinking low
Run history:
openclaw cron runs --id <jobId> --limit 50
Immediate system event without creating a job:
openclaw system event --mode now --text "Next heartbeat: check battery."
Gateway API surface
cron.list,cron.status,cron.add,cron.update,cron.removecron.run(force or due),cron.runsFor immediate system events without a job, useopenclaw system event.
Troubleshooting
“Nothing runs”
- Check cron is enabled:
cron.enabledandOPENCLAW_SKIP_CRON. - Check the Gateway is running continuously (cron runs inside the Gateway process).
- For
cronschedules: confirm timezone (--tz) vs the host timezone.
Telegram delivers to the wrong place
- For forum topics, use
-100…:topic:<id>so it’s explicit and unambiguous. - If you see
telegram:...prefixes in logs or stored “last route” targets, that’s normal; cron delivery accepts them and still parses topic IDs correctly.
Cron vs Heartbeat
Source: https://docs.openclaw.ai/automation/cron-vs-heartbeat
Cron vs Heartbeat: When to Use Each
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
Quick Decision Guide
| Use Case | Recommended | Why |
|---|---|---|
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
| Remind me in 20 minutes | Cron (main, --at) |
One-shot with precise timing |
| Background project health check | Heartbeat | Piggybacks on existing cycle |
Heartbeat: Periodic Awareness
Heartbeats run in the main session at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
When to use heartbeat
- Multiple periodic checks: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
- Context-aware decisions: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
- Conversational continuity: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
- Low-overhead monitoring: One heartbeat replaces many small polling tasks.
Heartbeat advantages
- Batches multiple checks: One agent turn can review inbox, calendar, and notifications together.
- Reduces API calls: A single heartbeat is cheaper than 5 isolated cron jobs.
- Context-aware: The agent knows what you've been working on and can prioritize accordingly.
- Smart suppression: If nothing needs attention, the agent replies
HEARTBEAT_OKand no message is delivered. - Natural timing: Drifts slightly based on queue load, which is fine for most monitoring.
Heartbeat example: HEARTBEAT.md checklist
# Heartbeat checklist
- Check email for urgent messages
- Review calendar for events in next 2 hours
- If a background task finished, summarize results
- If idle for 8+ hours, send a brief check-in
The agent reads this on each heartbeat and handles all items in one turn.
Configuring heartbeat
{
agents: {
defaults: {
heartbeat: {
every: "30m", // interval
target: "last", // where to deliver alerts
activeHours: { start: "08:00", end: "22:00" }, // optional
},
},
},
}
See Heartbeat for full configuration.
Cron: Precise Scheduling
Cron jobs run at exact times and can run in isolated sessions without affecting main context.
When to use cron
- Exact timing required: "Send this at 9:00 AM every Monday" (not "sometime around 9").
- Standalone tasks: Tasks that don't need conversational context.
- Different model/thinking: Heavy analysis that warrants a more powerful model.
- One-shot reminders: "Remind me in 20 minutes" with
--at. - Noisy/frequent tasks: Tasks that would clutter main session history.
- External triggers: Tasks that should run independently of whether the agent is otherwise active.
Cron advantages
- Exact timing: 5-field cron expressions with timezone support.
- Session isolation: Runs in
cron:<jobId>without polluting main history. - Model overrides: Use a cheaper or more powerful model per job.
- Delivery control: Isolated jobs default to
announce(summary); choosenoneas needed. - Immediate delivery: Announce mode posts directly without waiting for heartbeat.
- No agent context needed: Runs even if main session is idle or compacted.
- One-shot support:
--atfor precise future timestamps.
Cron example: Daily morning briefing
openclaw cron add \
--name "Morning briefing" \
--cron "0 7 * * *" \
--tz "America/New_York" \
--session isolated \
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
--model opus \
--announce \
--channel whatsapp \
--to "+15551234567"
This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces a summary directly to WhatsApp.
Cron example: One-shot reminder
openclaw cron add \
--name "Meeting reminder" \
--at "20m" \
--session main \
--system-event "Reminder: standup meeting starts in 10 minutes." \
--wake now \
--delete-after-run
See Cron jobs for full CLI reference.
Decision Flowchart
Does the task need to run at an EXACT time?
YES -> Use cron
NO -> Continue...
Does the task need isolation from main session?
YES -> Use cron (isolated)
NO -> Continue...
Can this task be batched with other periodic checks?
YES -> Use heartbeat (add to HEARTBEAT.md)
NO -> Use cron
Is this a one-shot reminder?
YES -> Use cron with --at
NO -> Continue...
Does it need a different model or thinking level?
YES -> Use cron (isolated) with --model/--thinking
NO -> Use heartbeat
Combining Both
The most efficient setup uses both:
- Heartbeat handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
- Cron handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
Example: Efficient automation setup
HEARTBEAT.md (checked every 30 min):
# Heartbeat checklist
- Scan inbox for urgent emails
- Check calendar for events in next 2h
- Review any pending tasks
- Light check-in if quiet for 8+ hours
Cron jobs (precise timing):
# Daily morning briefing at 7am
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce
# Weekly project review on Mondays at 9am
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
# One-shot reminder
openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
Lobster: Deterministic workflows with approvals
Lobster is the workflow runtime for multi-step tool pipelines that need deterministic execution and explicit approvals. Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
When Lobster fits
- Multi-step automation: You need a fixed pipeline of tool calls, not a one-off prompt.
- Approval gates: Side effects should pause until you approve, then resume.
- Resumable runs: Continue a paused workflow without re-running earlier steps.
How it pairs with heartbeat and cron
- Heartbeat/cron decide when a run happens.
- Lobster defines what steps happen once the run starts.
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster. For ad-hoc workflows, call Lobster directly.
Operational notes (from the code)
- Lobster runs as a local subprocess (
lobsterCLI) in tool mode and returns a JSON envelope. - If the tool returns
needs_approval, you resume with aresumeTokenandapproveflag. - The tool is an optional plugin; enable it additively via
tools.alsoAllow: ["lobster"](recommended). - If you pass
lobsterPath, it must be an absolute path.
See Lobster for full usage and examples.
Main Session vs Isolated Session
Both heartbeat and cron can interact with the main session, but differently:
| Heartbeat | Cron (main) | Cron (isolated) | |
|---|---|---|---|
| Session | Main | Main (via system event) | cron:<jobId> |
| History | Shared | Shared | Fresh each run |
| Context | Full | Full | None (starts clean) |
| Model | Main session model | Main session model | Can override |
| Output | Delivered if not HEARTBEAT_OK |
Heartbeat prompt + event | Announce summary (default) |
When to use main session cron
Use --session main with --system-event when you want:
- The reminder/event to appear in main session context
- The agent to handle it during the next heartbeat with full context
- No separate isolated run
openclaw cron add \
--name "Check project" \
--every "4h" \
--session main \
--system-event "Time for a project health check" \
--wake now
When to use isolated cron
Use --session isolated when you want:
- A clean slate without prior context
- Different model or thinking settings
- Announce summaries directly to a channel
- History that doesn't clutter main session
openclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 0" \
--session isolated \
--message "Weekly codebase analysis..." \
--model opus \
--thinking high \
--announce
Cost Considerations
| Mechanism | Cost Profile |
|---|---|
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
| Cron (isolated) | Full agent turn per job; can use cheaper model |
Tips:
- Keep
HEARTBEAT.mdsmall to minimize token overhead. - Batch similar checks into heartbeat instead of multiple cron jobs.
- Use
target: "none"on heartbeat if you only want internal processing. - Use isolated cron with a cheaper model for routine tasks.
Related
- Heartbeat - full heartbeat configuration
- Cron jobs - full cron CLI and API reference
- System - system events + heartbeat controls
Gmail PubSub
Source: https://docs.openclaw.ai/automation/gmail-pubsub
Gmail Pub/Sub -> OpenClaw
Goal: Gmail watch -> Pub/Sub push -> gog gmail watch serve -> OpenClaw webhook.
Prereqs
gcloudinstalled and logged in (install guide).gog(gogcli) installed and authorized for the Gmail account (gogcli.sh).- OpenClaw hooks enabled (see Webhooks).
tailscalelogged in (tailscale.com). Supported setup uses Tailscale Funnel for the public HTTPS endpoint. Other tunnel services can work, but are DIY/unsupported and require manual wiring. Right now, Tailscale is what we support.
Example hook config (enable Gmail preset mapping):
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
path: "/hooks",
presets: ["gmail"],
},
}
To deliver the Gmail summary to a chat surface, override the preset with a mapping
that sets deliver + optional channel/to:
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
presets: ["gmail"],
mappings: [
{
match: { path: "gmail" },
action: "agent",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate: "New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
model: "openai/gpt-5.2-mini",
deliver: true,
channel: "last",
// to: "+15551234567"
},
],
},
}
If you want a fixed channel, set channel + to. Otherwise channel: "last"
uses the last delivery route (falls back to WhatsApp).
To force a cheaper model for Gmail runs, set model in the mapping
(provider/model or alias). If you enforce agents.defaults.models, include it there.
To set a default model and thinking level specifically for Gmail hooks, add
hooks.gmail.model / hooks.gmail.thinking in your config:
{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}
Notes:
- Per-hook
model/thinkingin the mapping still overrides these defaults. - Fallback order:
hooks.gmail.model→agents.defaults.model.fallbacks→ primary (auth/rate-limit/timeouts). - If
agents.defaults.modelsis set, the Gmail model must be in the allowlist. - Gmail hook content is wrapped with external-content safety boundaries by default.
To disable (dangerous), set
hooks.gmail.allowUnsafeExternalContent: true.
To customize payload handling further, add hooks.mappings or a JS/TS transform module
under hooks.transformsDir (see Webhooks).
Wizard (recommended)
Use the OpenClaw helper to wire everything together (installs deps on macOS via brew):
openclaw webhooks gmail setup \
--account openclaw@gmail.com
Defaults:
- Uses Tailscale Funnel for the public push endpoint.
- Writes
hooks.gmailconfig foropenclaw webhooks gmail run. - Enables the Gmail hook preset (
hooks.presets: ["gmail"]).
Path note: when tailscale.mode is enabled, OpenClaw automatically sets
hooks.gmail.serve.path to / and keeps the public path at
hooks.gmail.tailscale.path (default /gmail-pubsub) because Tailscale
strips the set-path prefix before proxying.
If you need the backend to receive the prefixed path, set
hooks.gmail.tailscale.target (or --tailscale-target) to a full URL like
http://127.0.0.1:8788/gmail-pubsub and match hooks.gmail.serve.path.
Want a custom endpoint? Use --push-endpoint <url> or --tailscale off.
Platform note: on macOS the wizard installs gcloud, gogcli, and tailscale
via Homebrew; on Linux install them manually first.
Gateway auto-start (recommended):
- When
hooks.enabled=trueandhooks.gmail.accountis set, the Gateway startsgog gmail watch serveon boot and auto-renews the watch. - Set
OPENCLAW_SKIP_GMAIL_WATCHER=1to opt out (useful if you run the daemon yourself). - Do not run the manual daemon at the same time, or you will hit
listen tcp 127.0.0.1:8788: bind: address already in use.
Manual daemon (starts gog gmail watch serve + auto-renew):
openclaw webhooks gmail run
One-time setup
- Select the GCP project that owns the OAuth client used by
gog.
gcloud auth login
gcloud config set project <project-id>
Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.
- Enable APIs:
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
- Create a topic:
gcloud pubsub topics create gog-gmail-watch
- Allow Gmail push to publish:
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
Start the watch
gog gmail watch start \
--account openclaw@gmail.com \
--label INBOX \
--topic projects/<project-id>/topics/gog-gmail-watch
Save the history_id from the output (for debugging).
Run the push handler
Local example (shared token auth):
gog gmail watch serve \
--account openclaw@gmail.com \
--bind 127.0.0.1 \
--port 8788 \
--path /gmail-pubsub \
--token <shared> \
--hook-url http://127.0.0.1:18789/hooks/gmail \
--hook-token OPENCLAW_HOOK_TOKEN \
--include-body \
--max-bytes 20000
Notes:
--tokenprotects the push endpoint (x-gog-tokenor?token=).--hook-urlpoints to OpenClaw/hooks/gmail(mapped; isolated run + summary to main).--include-bodyand--max-bytescontrol the body snippet sent to OpenClaw.
Recommended: openclaw webhooks gmail run wraps the same flow and auto-renews the watch.
Expose the handler (advanced, unsupported)
If you need a non-Tailscale tunnel, wire it manually and use the public URL in the push subscription (unsupported, no guardrails):
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
Use the generated URL as the push endpoint:
gcloud pubsub subscriptions create gog-gmail-watch-push \
--topic gog-gmail-watch \
--push-endpoint "https://<public-url>/gmail-pubsub?token=<shared>"
Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:
gog gmail watch serve --verify-oidc --oidc-email <svc@...>
Test
Send a message to the watched inbox:
gog gmail send \
--account openclaw@gmail.com \
--to openclaw@gmail.com \
--subject "watch test" \
--body "ping"
Check watch state and history:
gog gmail watch status --account openclaw@gmail.com
gog gmail history --account openclaw@gmail.com --since <historyId>
Troubleshooting
Invalid topicName: project mismatch (topic not in the OAuth client project).User not authorized: missingroles/pubsub.publisheron the topic.- Empty messages: Gmail push only provides
historyId; fetch viagog gmail history.
Cleanup
gog gmail watch stop --account openclaw@gmail.com
gcloud pubsub subscriptions delete gog-gmail-watch-push
gcloud pubsub topics delete gog-gmail-watch
Polls
Source: https://docs.openclaw.ai/automation/poll
Polls
Supported channels
- WhatsApp (web channel)
- Discord
- MS Teams (Adaptive Cards)
CLI
# WhatsApp
openclaw message poll --target +15555550123 \
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
openclaw message poll --target 123456789@g.us \
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
# Discord
openclaw message poll --channel discord --target channel:123456789 \
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
openclaw message poll --channel discord --target channel:123456789 \
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
# MS Teams
openclaw message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
Options:
--channel:whatsapp(default),discord, ormsteams--poll-multi: allow selecting multiple options--poll-duration-hours: Discord-only (defaults to 24 when omitted)
Gateway RPC
Method: poll
Params:
to(string, required)question(string, required)options(string[], required)maxSelections(number, optional)durationHours(number, optional)channel(string, optional, default:whatsapp)idempotencyKey(string, required)
Channel differences
- WhatsApp: 2-12 options,
maxSelectionsmust be within option count, ignoresdurationHours. - Discord: 2-10 options,
durationHoursclamped to 1-768 hours (default 24).maxSelections > 1enables multi-select; Discord does not support a strict selection count. - MS Teams: Adaptive Card polls (OpenClaw-managed). No native poll API;
durationHoursis ignored.
Agent tool (Message)
Use the message tool with poll action (to, pollQuestion, pollOption, optional pollMulti, pollDurationHours, channel).
Note: Discord has no “pick exactly N” mode; pollMulti maps to multi-select.
Teams polls are rendered as Adaptive Cards and require the gateway to stay online
to record votes in ~/.openclaw/msteams-polls.json.
Webhooks
Source: https://docs.openclaw.ai/automation/webhook
Webhooks
Gateway can expose a small HTTP webhook endpoint for external triggers.
Enable
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
},
}
Notes:
hooks.tokenis required whenhooks.enabled=true.hooks.pathdefaults to/hooks.
Auth
Every request must include the hook token. Prefer headers:
Authorization: Bearer <token>(recommended)x-openclaw-token: <token>?token=<token>(deprecated; logs a warning and will be removed in a future major release)
Endpoints
POST /hooks/wake
Payload:
{ "text": "System line", "mode": "now" }
textrequired (string): The description of the event (e.g., "New email received").modeoptional (now|next-heartbeat): Whether to trigger an immediate heartbeat (defaultnow) or wait for the next periodic check.
Effect:
- Enqueues a system event for the main session
- If
mode=now, triggers an immediate heartbeat
POST /hooks/agent
Payload:
{
"message": "Run this",
"name": "Email",
"sessionKey": "hook:email:msg-123",
"wakeMode": "now",
"deliver": true,
"channel": "last",
"to": "+15551234567",
"model": "openai/gpt-5.2-mini",
"thinking": "low",
"timeoutSeconds": 120
}
messagerequired (string): The prompt or message for the agent to process.nameoptional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.sessionKeyoptional (string): The key used to identify the agent's session. Defaults to a randomhook:<uuid>. Using a consistent key allows for a multi-turn conversation within the hook context.wakeModeoptional (now|next-heartbeat): Whether to trigger an immediate heartbeat (defaultnow) or wait for the next periodic check.deliveroptional (boolean): Iftrue, the agent's response will be sent to the messaging channel. Defaults totrue. Responses that are only heartbeat acknowledgments are automatically skipped.channeloptional (string): The messaging channel for delivery. One of:last,whatsapp,telegram,discord,slack,mattermost(plugin),signal,imessage,msteams. Defaults tolast.tooptional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.modeloptional (string): Model override (e.g.,anthropic/claude-3-5-sonnetor an alias). Must be in the allowed model list if restricted.thinkingoptional (string): Thinking level override (e.g.,low,medium,high).timeoutSecondsoptional (number): Maximum duration for the agent run in seconds.
Effect:
- Runs an isolated agent turn (own session key)
- Always posts a summary into the main session
- If
wakeMode=now, triggers an immediate heartbeat
POST /hooks/<name> (mapped)
Custom hook names are resolved via hooks.mappings (see configuration). A mapping can
turn arbitrary payloads into wake or agent actions, with optional templates or
code transforms.
Mapping options (summary):
hooks.presets: ["gmail"]enables the built-in Gmail mapping.hooks.mappingslets you definematch,action, and templates in config.hooks.transformsDir+transform.moduleloads a JS/TS module for custom logic.- Use
match.sourceto keep a generic ingest endpoint (payload-driven routing). - TS transforms require a TS loader (e.g.
bunortsx) or precompiled.jsat runtime. - Set
deliver: true+channel/toon mappings to route replies to a chat surface (channeldefaults tolastand falls back to WhatsApp). allowUnsafeExternalContent: truedisables the external content safety wrapper for that hook (dangerous; only for trusted internal sources).openclaw webhooks gmail setupwriteshooks.gmailconfig foropenclaw webhooks gmail run. See Gmail Pub/Sub for the full Gmail watch flow.
Responses
200for/hooks/wake202for/hooks/agent(async run started)401on auth failure400on invalid payload413on oversized payloads
Examples
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-openclaw-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'
Use a different model
Add model to the agent payload (or mapping) to override the model for that run:
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-openclaw-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
If you enforce agents.defaults.models, make sure the override model is included there.
curl -X POST http://127.0.0.1:18789/hooks/gmail \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'
Security
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
- Use a dedicated hook token; do not reuse gateway auth tokens.
- Avoid including sensitive raw payloads in webhook logs.
- Hook payloads are treated as untrusted and wrapped with safety boundaries by default.
If you must disable this for a specific hook, set
allowUnsafeExternalContent: truein that hook's mapping (dangerous).
Broadcast Groups
Source: https://docs.openclaw.ai/broadcast-groups
Broadcast Groups
Status: Experimental
Version: Added in 2026.1.9
Overview
Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM — all using one phone number.
Current scope: WhatsApp only (web channel).
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings).
Use Cases
1. Specialized Agent Teams
Deploy multiple agents with atomic, focused responsibilities:
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
Each agent processes the same message and provides its specialized perspective.
2. Multi-Language Support
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
3. Quality Assurance Workflows
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
4. Task Automation
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
Configuration
Basic Setup
Add a top-level broadcast section (next to bindings). Keys are WhatsApp peer ids:
- group chats: group JID (e.g.
120363403215116621@g.us) - DMs: E.164 phone number (e.g.
+15551234567)
{
"broadcast": {
"120363403215116621@g.us": ["alfred", "baerbel", "assistant3"]
}
}
Result: When OpenClaw would reply in this chat, it will run all three agents.
Processing Strategy
Control how agents process messages:
Parallel (Default)
All agents process simultaneously:
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
Sequential
Agents process in order (one waits for previous to finish):
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
Complete Example
{
"agents": {
"list": [
{
"id": "code-reviewer",
"name": "Code Reviewer",
"workspace": "/path/to/code-reviewer",
"sandbox": { "mode": "all" }
},
{
"id": "security-auditor",
"name": "Security Auditor",
"workspace": "/path/to/security-auditor",
"sandbox": { "mode": "all" }
},
{
"id": "docs-generator",
"name": "Documentation Generator",
"workspace": "/path/to/docs-generator",
"sandbox": { "mode": "all" }
}
]
},
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["code-reviewer", "security-auditor", "docs-generator"],
"120363424282127706@g.us": ["support-en", "support-de"],
"+15555550123": ["assistant", "logger"]
}
}
How It Works
Message Flow
- Incoming message arrives in a WhatsApp group
- Broadcast check: System checks if peer ID is in
broadcast - If in broadcast list:
- All listed agents process the message
- Each agent has its own session key and isolated context
- Agents process in parallel (default) or sequentially
- If not in broadcast list:
- Normal routing applies (first matching binding)
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change which agents run when a message is eligible for processing.
Session Isolation
Each agent in a broadcast group maintains completely separate:
- Session keys (
agent:alfred:whatsapp:group:120363...vsagent:baerbel:whatsapp:group:120363...) - Conversation history (agent doesn't see other agents' messages)
- Workspace (separate sandboxes if configured)
- Tool access (different allow/deny lists)
- Memory/context (separate IDENTITY.md, SOUL.md, etc.)
- Group context buffer (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered
This allows each agent to have:
- Different personalities
- Different tool access (e.g., read-only vs. read-write)
- Different models (e.g., opus vs. sonnet)
- Different skills installed
Example: Isolated Sessions
In group 120363403215116621@g.us with agents ["alfred", "baerbel"]:
Alfred's context:
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/pascal/openclaw-alfred/
Tools: read, write, exec
Bärbel's context:
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/pascal/openclaw-baerbel/
Tools: read only
Best Practices
1. Keep Agents Focused
Design each agent with a single, clear responsibility:
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
✅ Good: Each agent has one job
❌ Bad: One generic "dev-helper" agent
2. Use Descriptive Names
Make it clear what each agent does:
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
3. Configure Different Tool Access
Give agents only the tools they need:
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
}
}
}
4. Monitor Performance
With many agents, consider:
- Using
"strategy": "parallel"(default) for speed - Limiting broadcast groups to 5-10 agents
- Using faster models for simpler agents
5. Handle Failures Gracefully
Agents fail independently. One agent's error doesn't block others:
Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error
Compatibility
Providers
Broadcast groups currently work with:
- ✅ WhatsApp (implemented)
- 🚧 Telegram (planned)
- 🚧 Discord (planned)
- 🚧 Slack (planned)
Routing
Broadcast groups work alongside existing routing:
{
"bindings": [
{
"match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } },
"agentId": "alfred"
}
],
"broadcast": {
"GROUP_B": ["agent1", "agent2"]
}
}
GROUP_A: Only alfred responds (normal routing)GROUP_B: agent1 AND agent2 respond (broadcast)
Precedence: broadcast takes priority over bindings.
Troubleshooting
Agents Not Responding
Check:
- Agent IDs exist in
agents.list - Peer ID format is correct (e.g.,
120363403215116621@g.us) - Agents are not in deny lists
Debug:
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
Only One Agent Responding
Cause: Peer ID might be in bindings but not broadcast.
Fix: Add to broadcast config or remove from bindings.
Performance Issues
If slow with many agents:
- Reduce number of agents per group
- Use lighter models (sonnet instead of opus)
- Check sandbox startup time
Examples
Example 1: Code Review Team
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
"agents": {
"list": [
{
"id": "code-formatter",
"workspace": "~/agents/formatter",
"tools": { "allow": ["read", "write"] }
},
{
"id": "security-scanner",
"workspace": "~/agents/security",
"tools": { "allow": ["read", "exec"] }
},
{
"id": "test-coverage",
"workspace": "~/agents/testing",
"tools": { "allow": ["read", "exec"] }
},
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
User sends: Code snippet
Responses:
- code-formatter: "Fixed indentation and added type hints"
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
- test-coverage: "Coverage is 45%, missing tests for error cases"
- docs-checker: "Missing docstring for function
process_data"
Example 2: Multi-Language Support
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
API Reference
Config Schema
interface OpenClawConfig {
broadcast?: {
strategy?: "parallel" | "sequential";
[peerId: string]: string[];
};
}
Fields
strategy(optional): How to process agents"parallel"(default): All agents process simultaneously"sequential": Agents process in array order
[peerId]: WhatsApp group JID, E.164 number, or other peer ID- Value: Array of agent IDs that should process messages
Limitations
- Max agents: No hard limit, but 10+ agents may be slow
- Shared context: Agents don't see each other's responses (by design)
- Message ordering: Parallel responses may arrive in any order
- Rate limits: All agents count toward WhatsApp rate limits
Future Enhancements
Planned features:
- Shared context mode (agents see each other's responses)
- Agent coordination (agents can signal each other)
- Dynamic agent selection (choose agents based on message content)
- Agent priorities (some agents respond before others)
See Also
Discord
Source: https://docs.openclaw.ai/channels/discord
Discord (Bot API)
Status: ready for DM and guild text channels via the official Discord bot gateway.
Quick setup (beginner)
- Create a Discord bot and copy the bot token.
- In the Discord app settings, enable Message Content Intent (and Server Members Intent if you plan to use allowlists or name lookups).
- Set the token for OpenClaw:
- Env:
DISCORD_BOT_TOKEN=... - Or config:
channels.discord.token: "...". - If both are set, config takes precedence (env fallback is default-account only).
- Env:
- Invite the bot to your server with message permissions (create a private server if you just want DMs).
- Start the gateway.
- DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
{
channels: {
discord: {
enabled: true,
token: "YOUR_BOT_TOKEN",
},
},
}
Goals
- Talk to OpenClaw via Discord DMs or guild channels.
- Direct chats collapse into the agent's main session (default
agent:main:main); guild channels stay isolated asagent:<agentId>:discord:channel:<channelId>(display names usediscord:<guildSlug>#<channelSlug>). - Group DMs are ignored by default; enable via
channels.discord.dm.groupEnabledand optionally restrict bychannels.discord.dm.groupChannels. - Keep routing deterministic: replies always go back to the channel they arrived on.
How it works
- Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
- Invite the bot to your server with the permissions required to read/send messages where you want to use it.
- Configure OpenClaw with
channels.discord.token(orDISCORD_BOT_TOKENas a fallback). - Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and
channels.discord.enabledis notfalse.- If you prefer env vars, set
DISCORD_BOT_TOKEN(a config block is optional).
- If you prefer env vars, set
- Direct chats: use
user:<id>(or a<@id>mention) when delivering; all turns land in the sharedmainsession. Bare numeric IDs are ambiguous and rejected. - Guild channels: use
channel:<channelId>for delivery. Mentions are required by default and can be set per guild or per channel. - Direct chats: secure by default via
channels.discord.dm.policy(default:"pairing"). Unknown senders get a pairing code (expires after 1 hour); approve viaopenclaw pairing approve discord <code>.- To keep old “open to anyone” behavior: set
channels.discord.dm.policy="open"andchannels.discord.dm.allowFrom=["*"]. - To hard-allowlist: set
channels.discord.dm.policy="allowlist"and list senders inchannels.discord.dm.allowFrom. - To ignore all DMs: set
channels.discord.dm.enabled=falseorchannels.discord.dm.policy="disabled".
- To keep old “open to anyone” behavior: set
- Group DMs are ignored by default; enable via
channels.discord.dm.groupEnabledand optionally restrict bychannels.discord.dm.groupChannels. - Optional guild rules: set
channels.discord.guildskeyed by guild id (preferred) or slug, with per-channel rules. - Optional native commands:
commands.nativedefaults to"auto"(on for Discord/Telegram, off for Slack). Override withchannels.discord.commands.native: true|false|"auto";falseclears previously registered commands. Text commands are controlled bycommands.textand must be sent as standalone/...messages. Usecommands.useAccessGroups: falseto bypass access-group checks for commands.- Full command list + config: Slash commands
- Optional guild context history: set
channels.discord.historyLimit(default 20, falls back tomessages.groupChat.historyLimit) to include the last N guild messages as context when replying to a mention. Set0to disable. - Reactions: the agent can trigger reactions via the
discordtool (gated bychannels.discord.actions.*).- Reaction removal semantics: see /tools/reactions.
- The
discordtool is only exposed when the current channel is Discord.
- Native commands use isolated session keys (
agent:<agentId>:discord:slash:<userId>) rather than the sharedmainsession.
Note: Name → id resolution uses guild member search and requires Server Members Intent; if the bot can’t search members, use ids or <@id> mentions.
Note: Slugs are lowercase with spaces replaced by -. Channel names are slugged without the leading #.
Note: Guild context [from:] lines include author.tag + id to make ping-ready replies easy.
Config writes
By default, Discord is allowed to write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { discord: { configWrites: false } },
}
How to create your own bot
This is the “Discord Developer Portal” setup for running OpenClaw in a server (guild) channel like #help.
1) Create the Discord app + bot user
- Discord Developer Portal → Applications → New Application
- In your app:
- Bot → Add Bot
- Copy the Bot Token (this is what you put in
DISCORD_BOT_TOKEN)
2) Enable the gateway intents OpenClaw needs
Discord blocks “privileged intents” unless you explicitly enable them.
In Bot → Privileged Gateway Intents, enable:
- Message Content Intent (required to read message text in most guilds; without it you’ll see “Used disallowed intents” or the bot will connect but not react to messages)
- Server Members Intent (recommended; required for some member/user lookups and allowlist matching in guilds)
You usually do not need Presence Intent. Setting the bot's own presence (setPresence action) uses gateway OP3 and does not require this intent; it is only needed if you want to receive presence updates about other guild members.
3) Generate an invite URL (OAuth2 URL Generator)
In your app: OAuth2 → URL Generator
Scopes
- ✅
bot - ✅
applications.commands(required for native commands)
Bot Permissions (minimal baseline)
- ✅ View Channels
- ✅ Send Messages
- ✅ Read Message History
- ✅ Embed Links
- ✅ Attach Files
- ✅ Add Reactions (optional but recommended)
- ✅ Use External Emojis / Stickers (optional; only if you want them)
Avoid Administrator unless you’re debugging and fully trust the bot.
Copy the generated URL, open it, pick your server, and install the bot.
4) Get the ids (guild/user/channel)
Discord uses numeric ids everywhere; OpenClaw config prefers ids.
- Discord (desktop/web) → User Settings → Advanced → enable Developer Mode
- Right-click:
- Server name → Copy Server ID (guild id)
- Channel (e.g.
#help) → Copy Channel ID - Your user → Copy User ID
5) Configure OpenClaw
Token
Set the bot token via env var (recommended on servers):
DISCORD_BOT_TOKEN=...
Or via config:
{
channels: {
discord: {
enabled: true,
token: "YOUR_BOT_TOKEN",
},
},
}
Multi-account support: use channels.discord.accounts with per-account tokens and optional name. See gateway/configuration for the shared pattern.
Allowlist + channel routing
Example “single server, only allow me, only allow #help”:
{
channels: {
discord: {
enabled: true,
dm: { enabled: false },
guilds: {
YOUR_GUILD_ID: {
users: ["YOUR_USER_ID"],
requireMention: true,
channels: {
help: { allow: true, requireMention: true },
},
},
},
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1,
},
},
},
}
Notes:
requireMention: truemeans the bot only replies when mentioned (recommended for shared channels).agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns) also count as mentions for guild messages.- Multi-agent override: set per-agent patterns on
agents.list[].groupChat.mentionPatterns. - If
channelsis present, any channel not listed is denied by default. - Use a
"*"channel entry to apply defaults across all channels; explicit channel entries override the wildcard. - Threads inherit parent channel config (allowlist,
requireMention, skills, prompts, etc.) unless you add the thread channel id explicitly. - Owner hint: when a per-guild or per-channel
usersallowlist matches the sender, OpenClaw treats that sender as the owner in the system prompt. For a global owner across channels, setcommands.ownerAllowFrom. - Bot-authored messages are ignored by default; set
channels.discord.allowBots=trueto allow them (own messages remain filtered). - Warning: If you allow replies to other bots (
channels.discord.allowBots=true), prevent bot-to-bot reply loops withrequireMention,channels.discord.guilds.*.channels.<id>.usersallowlists, and/or clear guardrails inAGENTS.mdandSOUL.md.
6) Verify it works
- Start the gateway.
- In your server channel, send:
@Krill hello(or whatever your bot name is). - If nothing happens: check Troubleshooting below.
Troubleshooting
- First: run
openclaw doctorandopenclaw channels status --probe(actionable warnings + quick audits). - “Used disallowed intents”: enable Message Content Intent (and likely Server Members Intent) in the Developer Portal, then restart the gateway.
- Bot connects but never replies in a guild channel:
- Missing Message Content Intent, or
- The bot lacks channel permissions (View/Send/Read History), or
- Your config requires mentions and you didn’t mention it, or
- Your guild/channel allowlist denies the channel/user.
requireMention: falsebut still no replies:channels.discord.groupPolicydefaults to allowlist; set it to"open"or add a guild entry underchannels.discord.guilds(optionally list channels underchannels.discord.guilds.<id>.channelsto restrict).- If you only set
DISCORD_BOT_TOKENand never create achannels.discordsection, the runtime defaultsgroupPolicytoopen. Addchannels.discord.groupPolicy,channels.defaults.groupPolicy, or a guild/channel allowlist to lock it down.
- If you only set
requireMentionmust live underchannels.discord.guilds(or a specific channel).channels.discord.requireMentionat the top level is ignored.- Permission audits (
channels status --probe) only check numeric channel IDs. If you use slugs/names aschannels.discord.guilds.*.channelskeys, the audit can’t verify permissions. - DMs don’t work:
channels.discord.dm.enabled=false,channels.discord.dm.policy="disabled", or you haven’t been approved yet (channels.discord.dm.policy="pairing"). - Exec approvals in Discord: Discord supports a button UI for exec approvals in DMs (Allow once / Always allow / Deny).
/approve <id> ...is only for forwarded approvals and won’t resolve Discord’s button prompts. If you see❌ Failed to submit approval: Error: unknown approval idor the UI never shows up, check:channels.discord.execApprovals.enabled: truein your config.- Your Discord user ID is listed in
channels.discord.execApprovals.approvers(the UI is only sent to approvers). - Use the buttons in the DM prompt (Allow once, Always allow, Deny).
- See Exec approvals and Slash commands for the broader approvals and command flow.
Capabilities & limits
- DMs and guild text channels (threads are treated as separate channels; voice not supported).
- Typing indicators sent best-effort; message chunking uses
channels.discord.textChunkLimit(default 2000) and splits tall replies by line count (channels.discord.maxLinesPerMessage, default 17). - Optional newline chunking: set
channels.discord.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - File uploads supported up to the configured
channels.discord.mediaMaxMb(default 8 MB). - Mention-gated guild replies by default to avoid noisy bots.
- Reply context is injected when a message references another message (quoted content + ids).
- Native reply threading is off by default; enable with
channels.discord.replyToModeand reply tags.
Retry policy
Outbound Discord API calls retry on rate limits (429) using Discord retry_after when available, with exponential backoff and jitter. Configure via channels.discord.retry. See Retry policy.
Config
{
channels: {
discord: {
enabled: true,
token: "abc.123",
groupPolicy: "allowlist",
guilds: {
"*": {
channels: {
general: { allow: true },
},
},
},
mediaMaxMb: 8,
actions: {
reactions: true,
stickers: true,
emojiUploads: true,
stickerUploads: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
channels: true,
voiceStatus: true,
events: true,
moderation: false,
presence: false,
},
replyToMode: "off",
dm: {
enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["123456789012345678", "steipete"],
groupEnabled: false,
groupChannels: ["openclaw-dm"],
},
guilds: {
"*": { requireMention: true },
"123456789012345678": {
slug: "friends-of-openclaw",
requireMention: false,
reactionNotifications: "own",
users: ["987654321098765432", "steipete"],
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["search", "docs"],
systemPrompt: "Keep answers short.",
},
},
},
},
},
},
}
Ack reactions are controlled globally via messages.ackReaction +
messages.ackReactionScope. Use messages.removeAckAfterReply to clear the
ack reaction after the bot replies.
dm.enabled: setfalseto ignore all DMs (defaulttrue).dm.policy: DM access control (pairingrecommended)."open"requiresdm.allowFrom=["*"].dm.allowFrom: DM allowlist (user ids or names). Used bydm.policy="allowlist"and fordm.policy="open"validation. The wizard accepts usernames and resolves them to ids when the bot can search members.dm.groupEnabled: enable group DMs (defaultfalse).dm.groupChannels: optional allowlist for group DM channel ids or slugs.groupPolicy: controls guild channel handling (open|disabled|allowlist);allowlistrequires channel allowlists.guilds: per-guild rules keyed by guild id (preferred) or slug.guilds."*": default per-guild settings applied when no explicit entry exists.guilds.<id>.slug: optional friendly slug used for display names.guilds.<id>.users: optional per-guild user allowlist (ids or names).guilds.<id>.tools: optional per-guild tool policy overrides (allow/deny/alsoAllow) used when the channel override is missing.guilds.<id>.toolsBySender: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing;"*"wildcard supported).guilds.<id>.channels.<channel>.allow: allow/deny the channel whengroupPolicy="allowlist".guilds.<id>.channels.<channel>.requireMention: mention gating for the channel.guilds.<id>.channels.<channel>.tools: optional per-channel tool policy overrides (allow/deny/alsoAllow).guilds.<id>.channels.<channel>.toolsBySender: optional per-sender tool policy overrides within the channel ("*"wildcard supported).guilds.<id>.channels.<channel>.users: optional per-channel user allowlist.guilds.<id>.channels.<channel>.skills: skill filter (omit = all skills, empty = none).guilds.<id>.channels.<channel>.systemPrompt: extra system prompt for the channel. Discord channel topics are injected as untrusted context (not system prompt).guilds.<id>.channels.<channel>.enabled: setfalseto disable the channel.guilds.<id>.channels: channel rules (keys are channel slugs or ids).guilds.<id>.requireMention: per-guild mention requirement (overridable per channel).guilds.<id>.reactionNotifications: reaction system event mode (off,own,all,allowlist).textChunkLimit: outbound text chunk size (chars). Default: 2000.chunkMode:length(default) splits only when exceedingtextChunkLimit;newlinesplits on blank lines (paragraph boundaries) before length chunking.maxLinesPerMessage: soft max line count per message. Default: 17.mediaMaxMb: clamp inbound media saved to disk.historyLimit: number of recent guild messages to include as context when replying to a mention (default 20; falls back tomessages.groupChat.historyLimit;0disables).dmHistoryLimit: DM history limit in user turns. Per-user overrides:dms["<user_id>"].historyLimit.retry: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).pluralkit: resolve PluralKit proxied messages so system members appear as distinct senders.actions: per-action tool gates; omit to allow all (setfalseto disable).reactions(covers react + read reactions)stickers,emojiUploads,stickerUploads,polls,permissions,messages,threads,pins,searchmemberInfo,roleInfo,channelInfo,voiceStatus,eventschannels(create/edit/delete channels + categories + permissions)roles(role add/remove, defaultfalse)moderation(timeout/kick/ban, defaultfalse)presence(bot status/activity, defaultfalse)
execApprovals: Discord-only exec approval DMs (button UI). Supportsenabled,approvers,agentFilter,sessionFilter.
Reaction notifications use guilds.<id>.reactionNotifications:
off: no reaction events.own: reactions on the bot's own messages (default).all: all reactions on all messages.allowlist: reactions fromguilds.<id>.userson all messages (empty list disables).
PluralKit (PK) support
Enable PK lookups so proxied messages resolve to the underlying system + member.
When enabled, OpenClaw uses the member identity for allowlists and labels the
sender as Member (PK:System) to avoid accidental Discord pings.
{
channels: {
discord: {
pluralkit: {
enabled: true,
token: "pk_live_...", // optional; required for private systems
},
},
},
}
Allowlist notes (PK-enabled):
- Use
pk:<memberId>indm.allowFrom,guilds.<id>.users, or per-channelusers. - Member display names are also matched by name/slug.
- Lookups use the original Discord message ID (the pre-proxy message), so the PK API only resolves it within its 30-minute window.
- If PK lookups fail (e.g., private system without a token), proxied messages
are treated as bot messages and are dropped unless
channels.discord.allowBots=true.
Tool action defaults
| Action group | Default | Notes |
|---|---|---|
| reactions | enabled | React + list reactions + emojiList |
| stickers | enabled | Send stickers |
| emojiUploads | enabled | Upload emojis |
| stickerUploads | enabled | Upload stickers |
| polls | enabled | Create polls |
| permissions | enabled | Channel permission snapshot |
| messages | enabled | Read/send/edit/delete |
| threads | enabled | Create/list/reply |
| pins | enabled | Pin/unpin/list |
| search | enabled | Message search (preview feature) |
| memberInfo | enabled | Member info |
| roleInfo | enabled | Role list |
| channelInfo | enabled | Channel info + list |
| channels | enabled | Channel/category management |
| voiceStatus | enabled | Voice state lookup |
| events | enabled | List/create scheduled events |
| roles | disabled | Role add/remove |
| moderation | disabled | Timeout/kick/ban |
| presence | disabled | Bot status/activity (setPresence) |
replyToMode:off(default),first, orall. Applies only when the model includes a reply tag.
Reply tags
To request a threaded reply, the model can include one tag in its output:
[[reply_to_current]]— reply to the triggering Discord message.[[reply_to:<id>]]— reply to a specific message id from context/history. Current message ids are appended to prompts as[message_id: …]; history entries already include ids.
Behavior is controlled by channels.discord.replyToMode:
off: ignore tags.first: only the first outbound chunk/attachment is a reply.all: every outbound chunk/attachment is a reply.
Allowlist matching notes:
allowFrom/users/groupChannelsaccept ids, names, tags, or mentions like<@id>.- Prefixes like
discord:/user:(users) andchannel:(group DMs) are supported. - Use
*to allow any sender/channel. - When
guilds.<id>.channelsis present, channels not listed are denied by default. - When
guilds.<id>.channelsis omitted, all channels in the allowlisted guild are allowed. - To allow no channels, set
channels.discord.groupPolicy: "disabled"(or keep an empty allowlist). - The configure wizard accepts
Guild/Channelnames (public + private) and resolves them to IDs when possible. - On startup, OpenClaw resolves channel/user names in allowlists to IDs (when the bot can search members) and logs the mapping; unresolved entries are kept as typed.
Native command notes:
- The registered commands mirror OpenClaw’s chat commands.
- Native commands honor the same allowlists as DMs/guild messages (
channels.discord.dm.allowFrom,channels.discord.guilds, per-channel rules). - Slash commands may still be visible in Discord UI to users who aren’t allowlisted; OpenClaw enforces allowlists on execution and replies “not authorized”.
Tool actions
The agent can call discord with actions like:
react/reactions(add or list reactions)sticker,poll,permissionsreadMessages,sendMessage,editMessage,deleteMessage- Read/search/pin tool payloads include normalized
timestampMs(UTC epoch ms) andtimestampUtcalongside raw Discordtimestamp. threadCreate,threadList,threadReplypinMessage,unpinMessage,listPinssearchMessages,memberInfo,roleInfo,roleAdd,roleRemove,emojiListchannelInfo,channelList,voiceStatus,eventList,eventCreatetimeout,kick,bansetPresence(bot activity and online status)
Discord message ids are surfaced in the injected context ([discord message id: …] and history lines) so the agent can target them.
Emoji can be unicode (e.g., ✅) or custom emoji syntax like <:party_blob:1234567890>.
Safety & ops
- Treat the bot token like a password; prefer the
DISCORD_BOT_TOKENenv var on supervised hosts or lock down the config file permissions. - Only grant the bot permissions it needs (typically Read/Send Messages).
- If the bot is stuck or rate limited, restart the gateway (
openclaw gateway --force) after confirming no other processes own the Discord session.
Feishu
Source: https://docs.openclaw.ai/channels/feishu
Feishu bot
Feishu (Lark) is a team chat platform used by companies for messaging and collaboration. This plugin connects OpenClaw to a Feishu/Lark bot using the platform’s WebSocket event subscription so messages can be received without exposing a public webhook URL.
Plugin required
Install the Feishu plugin:
openclaw plugins install @openclaw/feishu
Local checkout (when running from a git repo):
openclaw plugins install ./extensions/feishu
Quickstart
There are two ways to add the Feishu channel:
Method 1: onboarding wizard (recommended)
If you just installed OpenClaw, run the wizard:
openclaw onboard
The wizard guides you through:
- Creating a Feishu app and collecting credentials
- Configuring app credentials in OpenClaw
- Starting the gateway
✅ After configuration, check gateway status:
openclaw gateway statusopenclaw logs --follow
Method 2: CLI setup
If you already completed initial install, add the channel via CLI:
openclaw channels add
Choose Feishu, then enter the App ID and App Secret.
✅ After configuration, manage the gateway:
openclaw gateway statusopenclaw gateway restartopenclaw logs --follow
Step 1: Create a Feishu app
1. Open Feishu Open Platform
Visit Feishu Open Platform and sign in.
Lark (global) tenants should use https://open.larksuite.com/app and set domain: "lark" in the Feishu config.
2. Create an app
- Click Create enterprise app
- Fill in the app name + description
- Choose an app icon
3. Copy credentials
From Credentials & Basic Info, copy:
- App ID (format:
cli_xxx) - App Secret
❗ Important: keep the App Secret private.
4. Configure permissions
On Permissions, click Batch import and paste:
{
"scopes": {
"tenant": [
"aily:file:read",
"aily:file:write",
"application:application.app_message_stats.overview:readonly",
"application:application:self_manage",
"application:bot.menu:write",
"contact:user.employee_id:readonly",
"corehr:file:download",
"event:ip_list",
"im:chat.access_event.bot_p2p_chat:read",
"im:chat.members:bot_access",
"im:message",
"im:message.group_at_msg:readonly",
"im:message.p2p_msg:readonly",
"im:message:readonly",
"im:message:send_as_bot",
"im:resource"
],
"user": ["aily:file:read", "aily:file:write", "im:chat.access_event.bot_p2p_chat:read"]
}
}
5. Enable bot capability
In App Capability > Bot:
- Enable bot capability
- Set the bot name
6. Configure event subscription
⚠️ Important: before setting event subscription, make sure:
- You already ran
openclaw channels addfor Feishu - The gateway is running (
openclaw gateway status)
In Event Subscription:
- Choose Use long connection to receive events (WebSocket)
- Add the event:
im.message.receive_v1
⚠️ If the gateway is not running, the long-connection setup may fail to save.
7. Publish the app
- Create a version in Version Management & Release
- Submit for review and publish
- Wait for admin approval (enterprise apps usually auto-approve)
Step 2: Configure OpenClaw
Configure with the wizard (recommended)
openclaw channels add
Choose Feishu and paste your App ID + App Secret.
Configure via config file
Edit ~/.openclaw/openclaw.json:
{
channels: {
feishu: {
enabled: true,
dmPolicy: "pairing",
accounts: {
main: {
appId: "cli_xxx",
appSecret: "xxx",
botName: "My AI assistant",
},
},
},
},
}
Configure via environment variables
export FEISHU_APP_ID="cli_xxx"
export FEISHU_APP_SECRET="xxx"
Lark (global) domain
If your tenant is on Lark (international), set the domain to lark (or a full domain string). You can set it at channels.feishu.domain or per account (channels.feishu.accounts.<id>.domain).
{
channels: {
feishu: {
domain: "lark",
accounts: {
main: {
appId: "cli_xxx",
appSecret: "xxx",
},
},
},
},
}
Step 3: Start + test
1. Start the gateway
openclaw gateway
2. Send a test message
In Feishu, find your bot and send a message.
3. Approve pairing
By default, the bot replies with a pairing code. Approve it:
openclaw pairing approve feishu <CODE>
After approval, you can chat normally.
Overview
- Feishu bot channel: Feishu bot managed by the gateway
- Deterministic routing: replies always return to Feishu
- Session isolation: DMs share a main session; groups are isolated
- WebSocket connection: long connection via Feishu SDK, no public URL needed
Access control
Direct messages
- Default:
dmPolicy: "pairing"(unknown users get a pairing code) - Approve pairing:
openclaw pairing list feishu openclaw pairing approve feishu <CODE> - Allowlist mode: set
channels.feishu.allowFromwith allowed Open IDs
Group chats
1. Group policy (channels.feishu.groupPolicy):
"open"= allow everyone in groups (default)"allowlist"= only allowgroupAllowFrom"disabled"= disable group messages
2. Mention requirement (channels.feishu.groups.<chat_id>.requireMention):
true= require @mention (default)false= respond without mentions
Group configuration examples
Allow all groups, require @mention (default)
{
channels: {
feishu: {
groupPolicy: "open",
// Default requireMention: true
},
},
}
Allow all groups, no @mention required
{
channels: {
feishu: {
groups: {
oc_xxx: { requireMention: false },
},
},
},
}
Allow specific users in groups only
{
channels: {
feishu: {
groupPolicy: "allowlist",
groupAllowFrom: ["ou_xxx", "ou_yyy"],
},
},
}
Get group/user IDs
Group IDs (chat_id)
Group IDs look like oc_xxx.
Method 1 (recommended)
- Start the gateway and @mention the bot in the group
- Run
openclaw logs --followand look forchat_id
Method 2
Use the Feishu API debugger to list group chats.
User IDs (open_id)
User IDs look like ou_xxx.
Method 1 (recommended)
- Start the gateway and DM the bot
- Run
openclaw logs --followand look foropen_id
Method 2
Check pairing requests for user Open IDs:
openclaw pairing list feishu
Common commands
| Command | Description |
|---|---|
/status |
Show bot status |
/reset |
Reset the session |
/model |
Show/switch model |
Note: Feishu does not support native command menus yet, so commands must be sent as text.
Gateway management commands
| Command | Description |
|---|---|
openclaw gateway status |
Show gateway status |
openclaw gateway install |
Install/start gateway service |
openclaw gateway stop |
Stop gateway service |
openclaw gateway restart |
Restart gateway service |
openclaw logs --follow |
Tail gateway logs |
Troubleshooting
Bot does not respond in group chats
- Ensure the bot is added to the group
- Ensure you @mention the bot (default behavior)
- Check
groupPolicyis not set to"disabled" - Check logs:
openclaw logs --follow
Bot does not receive messages
- Ensure the app is published and approved
- Ensure event subscription includes
im.message.receive_v1 - Ensure long connection is enabled
- Ensure app permissions are complete
- Ensure the gateway is running:
openclaw gateway status - Check logs:
openclaw logs --follow
App Secret leak
- Reset the App Secret in Feishu Open Platform
- Update the App Secret in your config
- Restart the gateway
Message send failures
- Ensure the app has
im:message:send_as_botpermission - Ensure the app is published
- Check logs for detailed errors
Advanced configuration
Multiple accounts
{
channels: {
feishu: {
accounts: {
main: {
appId: "cli_xxx",
appSecret: "xxx",
botName: "Primary bot",
},
backup: {
appId: "cli_yyy",
appSecret: "yyy",
botName: "Backup bot",
enabled: false,
},
},
},
},
}
Message limits
textChunkLimit: outbound text chunk size (default: 2000 chars)mediaMaxMb: media upload/download limit (default: 30MB)
Streaming
Feishu does not support message editing, so block streaming is enabled by default (blockStreaming: true). The bot waits for the full reply before sending.
Configuration reference
Full configuration: Gateway configuration
Key options:
| Setting | Description | Default |
|---|---|---|
channels.feishu.enabled |
Enable/disable channel | true |
channels.feishu.domain |
API domain (feishu or lark) |
feishu |
channels.feishu.accounts.<id>.appId |
App ID | - |
channels.feishu.accounts.<id>.appSecret |
App Secret | - |
channels.feishu.accounts.<id>.domain |
Per-account API domain override | feishu |
channels.feishu.dmPolicy |
DM policy | pairing |
channels.feishu.allowFrom |
DM allowlist (open_id list) | - |
channels.feishu.groupPolicy |
Group policy | open |
channels.feishu.groupAllowFrom |
Group allowlist | - |
channels.feishu.groups.<chat_id>.requireMention |
Require @mention | true |
channels.feishu.groups.<chat_id>.enabled |
Enable group | true |
channels.feishu.textChunkLimit |
Message chunk size | 2000 |
channels.feishu.mediaMaxMb |
Media size limit | 30 |
channels.feishu.blockStreaming |
Disable streaming | true |
dmPolicy reference
| Value | Behavior |
|---|---|
"pairing" |
Default. Unknown users get a pairing code; must be approved |
"allowlist" |
Only users in allowFrom can chat |
"open" |
Allow all users (requires "*" in allowFrom) |
"disabled" |
Disable DMs |
Supported message types
Receive
- ✅ Text
- ✅ Images
- ✅ Files
- ✅ Audio
- ✅ Video
- ✅ Stickers
Send
- ✅ Text
- ✅ Images
- ✅ Files
- ✅ Audio
- ⚠️ Rich text (partial support)
Google Chat
Source: https://docs.openclaw.ai/channels/googlechat
Google Chat (Chat API)
Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
Quick setup (beginner)
- Create a Google Cloud project and enable the Google Chat API.
- Go to: Google Chat API Credentials
- Enable the API if it is not already enabled.
- Create a Service Account:
- Press Create Credentials > Service Account.
- Name it whatever you want (e.g.,
openclaw-chat). - Leave permissions blank (press Continue).
- Leave principals with access blank (press Done).
- Create and download the JSON Key:
- In the list of service accounts, click on the one you just created.
- Go to the Keys tab.
- Click Add Key > Create new key.
- Select JSON and press Create.
- Store the downloaded JSON file on your gateway host (e.g.,
~/.openclaw/googlechat-service-account.json). - Create a Google Chat app in the Google Cloud Console Chat Configuration:
- Fill in the Application info:
- App name: (e.g.
OpenClaw) - Avatar URL: (e.g.
https://openclaw.ai/logo.png) - Description: (e.g.
Personal AI Assistant)
- App name: (e.g.
- Enable Interactive features.
- Under Functionality, check Join spaces and group conversations.
- Under Connection settings, select HTTP endpoint URL.
- Under Triggers, select Use a common HTTP endpoint URL for all triggers and set it to your gateway's public URL followed by
/googlechat.- Tip: Run
openclaw statusto find your gateway's public URL.
- Tip: Run
- Under Visibility, check Make this Chat app available to specific people and groups in <Your Domain>.
- Enter your email address (e.g.
user@example.com) in the text box. - Click Save at the bottom.
- Fill in the Application info:
- Enable the app status:
- After saving, refresh the page.
- Look for the App status section (usually near the top or bottom after saving).
- Change the status to Live - available to users.
- Click Save again.
- Configure OpenClaw with the service account path + webhook audience:
- Env:
GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json - Or config:
channels.googlechat.serviceAccountFile: "/path/to/service-account.json".
- Env:
- Set the webhook audience type + value (matches your Chat app config).
- Start the gateway. Google Chat will POST to your webhook path.
Add to Google Chat
Once the gateway is running and your email is added to the visibility list:
- Go to Google Chat.
- Click the + (plus) icon next to Direct Messages.
- In the search bar (where you usually add people), type the App name you configured in the Google Cloud Console.
- Note: The bot will not appear in the "Marketplace" browse list because it is a private app. You must search for it by name.
- Select your bot from the results.
- Click Add or Chat to start a 1:1 conversation.
- Send "Hello" to trigger the assistant!
Public URL (Webhook-only)
Google Chat webhooks require a public HTTPS endpoint. For security, only expose the /googlechat path to the internet. Keep the OpenClaw dashboard and other sensitive endpoints on your private network.
Option A: Tailscale Funnel (Recommended)
Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. This keeps / private while exposing only /googlechat.
-
Check what address your gateway is bound to:
ss -tlnp | grep 18789Note the IP address (e.g.,
127.0.0.1,0.0.0.0, or your Tailscale IP like100.x.x.x). -
Expose the dashboard to the tailnet only (port 8443):
# If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale serve --bg --https 8443 http://127.0.0.1:18789 # If bound to Tailscale IP only (e.g., 100.106.161.80): tailscale serve --bg --https 8443 http://100.106.161.80:18789 -
Expose only the webhook path publicly:
# If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat # If bound to Tailscale IP only (e.g., 100.106.161.80): tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat -
Authorize the node for Funnel access: If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy.
-
Verify the configuration:
tailscale serve status tailscale funnel status
Your public webhook URL will be:
https://<node-name>.<tailnet>.ts.net/googlechat
Your private dashboard stays tailnet-only:
https://<node-name>.<tailnet>.ts.net:8443/
Use the public URL (without :8443) in the Google Chat app config.
Note: This configuration persists across reboots. To remove it later, run
tailscale funnel resetandtailscale serve reset.
Option B: Reverse Proxy (Caddy)
If you use a reverse proxy like Caddy, only proxy the specific path:
your-domain.com {
reverse_proxy /googlechat* localhost:18789
}
With this config, any request to your-domain.com/ will be ignored or returned as 404, while your-domain.com/googlechat is safely routed to OpenClaw.
Option C: Cloudflare Tunnel
Configure your tunnel's ingress rules to only route the webhook path:
- Path:
/googlechat->http://localhost:18789/googlechat - Default Rule: HTTP 404 (Not Found)
How it works
- Google Chat sends webhook POSTs to the gateway. Each request includes an
Authorization: Bearer <token>header. - OpenClaw verifies the token against the configured
audienceType+audience:audienceType: "app-url"→ audience is your HTTPS webhook URL.audienceType: "project-number"→ audience is the Cloud project number.
- Messages are routed by space:
- DMs use session key
agent:<agentId>:googlechat:dm:<spaceId>. - Spaces use session key
agent:<agentId>:googlechat:group:<spaceId>.
- DMs use session key
- DM access is pairing by default. Unknown senders receive a pairing code; approve with:
openclaw pairing approve googlechat <code>
- Group spaces require @-mention by default. Use
botUserif mention detection needs the app’s user name.
Targets
Use these identifiers for delivery and allowlists:
- Direct messages:
users/<userId>orusers/<email>(email addresses are accepted). - Spaces:
spaces/<spaceId>.
Config highlights
{
channels: {
googlechat: {
enabled: true,
serviceAccountFile: "/path/to/service-account.json",
audienceType: "app-url",
audience: "https://gateway.example.com/googlechat",
webhookPath: "/googlechat",
botUser: "users/1234567890", // optional; helps mention detection
dm: {
policy: "pairing",
allowFrom: ["users/1234567890", "name@example.com"],
},
groupPolicy: "allowlist",
groups: {
"spaces/AAAA": {
allow: true,
requireMention: true,
users: ["users/1234567890"],
systemPrompt: "Short answers only.",
},
},
actions: { reactions: true },
typingIndicator: "message",
mediaMaxMb: 20,
},
},
}
Notes:
- Service account credentials can also be passed inline with
serviceAccount(JSON string). - Default webhook path is
/googlechatifwebhookPathisn’t set. - Reactions are available via the
reactionstool andchannels actionwhenactions.reactionsis enabled. typingIndicatorsupportsnone,message(default), andreaction(reaction requires user OAuth).- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by
mediaMaxMb).
Troubleshooting
405 Method Not Allowed
If Google Cloud Logs Explorer shows errors like:
status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed
This means the webhook handler isn't registered. Common causes:
-
Channel not configured: The
channels.googlechatsection is missing from your config. Verify with:openclaw config get channels.googlechatIf it returns "Config path not found", add the configuration (see Config highlights).
-
Plugin not enabled: Check plugin status:
openclaw plugins list | grep googlechatIf it shows "disabled", add
plugins.entries.googlechat.enabled: trueto your config. -
Gateway not restarted: After adding config, restart the gateway:
openclaw gateway restart
Verify the channel is running:
openclaw channels status
# Should show: Google Chat default: enabled, configured, ...
Other issues
- Check
openclaw channels status --probefor auth errors or missing audience config. - If no messages arrive, confirm the Chat app's webhook URL + event subscriptions.
- If mention gating blocks replies, set
botUserto the app's user resource name and verifyrequireMention. - Use
openclaw logs --followwhile sending a test message to see if requests reach the gateway.
Related docs:
grammY
Source: https://docs.openclaw.ai/channels/grammy
grammY Integration (Telegram Bot API)
Why grammY
- TS-first Bot API client with built-in long-poll + webhook helpers, middleware, error handling, rate limiter.
- Cleaner media helpers than hand-rolling fetch + FormData; supports all Bot API methods.
- Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
What we shipped
- Single client path: fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default.
- Gateway:
monitorTelegramProviderbuilds a grammYBot, wires mention/allowlist gating, media download viagetFile/download, and delivers replies withsendMessage/sendPhoto/sendVideo/sendAudio/sendDocument. Supports long-poll or webhook viawebhookCallback. - Proxy: optional
channels.telegram.proxyusesundici.ProxyAgentthrough grammY’sclient.baseFetch. - Webhook support:
webhook-set.tswrapssetWebhook/deleteWebhook;webhook.tshosts the callback with health + graceful shutdown. Gateway enables webhook mode whenchannels.telegram.webhookUrl+channels.telegram.webhookSecretare set (otherwise it long-polls). - Sessions: direct chats collapse into the agent main session (
agent:<agentId>:<mainKey>); groups useagent:<agentId>:telegram:group:<chatId>; replies route back to the same channel. - Config knobs:
channels.telegram.botToken,channels.telegram.dmPolicy,channels.telegram.groups(allowlist + mention defaults),channels.telegram.allowFrom,channels.telegram.groupAllowFrom,channels.telegram.groupPolicy,channels.telegram.mediaMaxMb,channels.telegram.linkPreview,channels.telegram.proxy,channels.telegram.webhookSecret,channels.telegram.webhookUrl. - Draft streaming: optional
channels.telegram.streamModeusessendMessageDraftin private topic chats (Bot API 9.3+). This is separate from channel block streaming. - Tests: grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions
- Optional grammY plugins (throttler) if we hit Bot API 429s.
- Add more structured media tests (stickers, voice notes).
- Make webhook listen port configurable (currently fixed to 8787 unless wired through the gateway).
iMessage
Source: https://docs.openclaw.ai/channels/imessage
iMessage (legacy: imsg)
Recommended: Use BlueBubbles for new iMessage setups.
The
imsgchannel is a legacy external-CLI integration and may be removed in a future release.
Status: legacy external CLI integration. Gateway spawns imsg rpc (JSON-RPC over stdio).
Quick setup (beginner)
- Ensure Messages is signed in on this Mac.
- Install
imsg:brew install steipete/tap/imsg
- Configure OpenClaw with
channels.imessage.cliPathandchannels.imessage.dbPath. - Start the gateway and approve any macOS prompts (Automation + Full Disk Access).
Minimal config:
{
channels: {
imessage: {
enabled: true,
cliPath: "/usr/local/bin/imsg",
dbPath: "/Users/<you>/Library/Messages/chat.db",
},
},
}
What it is
- iMessage channel backed by
imsgon macOS. - Deterministic routing: replies always go back to iMessage.
- DMs share the agent's main session; groups are isolated (
agent:<agentId>:imessage:group:<chat_id>). - If a multi-participant thread arrives with
is_group=false, you can still isolate it bychat_idusingchannels.imessage.groups(see “Group-ish threads” below).
Config writes
By default, iMessage is allowed to write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { imessage: { configWrites: false } },
}
Requirements
- macOS with Messages signed in.
- Full Disk Access for OpenClaw +
imsg(Messages DB access). - Automation permission when sending.
channels.imessage.cliPathcan point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runsimsg rpc).
Setup (fast path)
- Ensure Messages is signed in on this Mac.
- Configure iMessage and start the gateway.
Dedicated bot macOS user (for isolated identity)
If you want the bot to send from a separate iMessage identity (and keep your personal Messages clean), use a dedicated Apple ID + a dedicated macOS user.
- Create a dedicated Apple ID (example:
my-cool-bot@icloud.com).- Apple may require a phone number for verification / 2FA.
- Create a macOS user (example:
openclawhome) and sign into it. - Open Messages in that macOS user and sign into iMessage using the bot Apple ID.
- Enable Remote Login (System Settings → General → Sharing → Remote Login).
- Install
imsg:brew install steipete/tap/imsg
- Set up SSH so
ssh <bot-macos-user>@localhost trueworks without a password. - Point
channels.imessage.accounts.bot.cliPathat an SSH wrapper that runsimsgas the bot user.
First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the bot macOS user. If imsg rpc looks stuck or exits, log into that user (Screen Sharing helps), run a one-time imsg chats --limit 1 / imsg send ..., approve prompts, then retry.
Example wrapper (chmod +x). Replace <bot-macos-user> with your actual macOS username:
#!/usr/bin/env bash
set -euo pipefail
# Run an interactive SSH once first to accept host keys:
# ssh <bot-macos-user>@localhost true
exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T <bot-macos-user>@localhost \
"/usr/local/bin/imsg" "$@"
Example config:
{
channels: {
imessage: {
enabled: true,
accounts: {
bot: {
name: "Bot",
enabled: true,
cliPath: "/path/to/imsg-bot",
dbPath: "/Users/<bot-macos-user>/Library/Messages/chat.db",
},
},
},
},
}
For single-account setups, use flat options (channels.imessage.cliPath, channels.imessage.dbPath) instead of the accounts map.
Remote/SSH variant (optional)
If you want iMessage on another Mac, set channels.imessage.cliPath to a wrapper that runs imsg on the remote macOS host over SSH. OpenClaw only needs stdio.
Example wrapper:
#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"
Remote attachments: When cliPath points to a remote host via SSH, attachment paths in the Messages database reference files on the remote machine. OpenClaw can automatically fetch these over SCP by setting channels.imessage.remoteHost:
{
channels: {
imessage: {
cliPath: "~/imsg-ssh", // SSH wrapper to remote Mac
remoteHost: "user@gateway-host", // for SCP file transfer
includeAttachments: true,
},
},
}
If remoteHost is not set, OpenClaw attempts to auto-detect it by parsing the SSH command in your wrapper script. Explicit configuration is recommended for reliability.
Remote Mac via Tailscale (example)
If the Gateway runs on a Linux host/VM but iMessage must run on a Mac, Tailscale is the simplest bridge: the Gateway talks to the Mac over the tailnet, runs imsg via SSH, and SCPs attachments back.
Architecture:
┌──────────────────────────────┐ SSH (imsg rpc) ┌──────────────────────────┐
│ Gateway host (Linux/VM) │──────────────────────────────────▶│ Mac with Messages + imsg │
│ - openclaw gateway │ SCP (attachments) │ - Messages signed in │
│ - channels.imessage.cliPath │◀──────────────────────────────────│ - Remote Login enabled │
└──────────────────────────────┘ └──────────────────────────┘
▲
│ Tailscale tailnet (hostname or 100.x.y.z)
▼
user@gateway-host
Concrete config example (Tailscale hostname):
{
channels: {
imessage: {
enabled: true,
cliPath: "~/.openclaw/scripts/imsg-ssh",
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
includeAttachments: true,
dbPath: "/Users/bot/Library/Messages/chat.db",
},
},
}
Example wrapper (~/.openclaw/scripts/imsg-ssh):
#!/usr/bin/env bash
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
Notes:
- Ensure the Mac is signed in to Messages, and Remote Login is enabled.
- Use SSH keys so
ssh bot@mac-mini.tailnet-1234.ts.networks without prompts. remoteHostshould match the SSH target so SCP can fetch attachments.
Multi-account support: use channels.imessage.accounts with per-account config and optional name. See gateway/configuration for the shared pattern. Don't commit ~/.openclaw/openclaw.json (it often contains tokens).
Access control (DMs + groups)
DMs:
- Default:
channels.imessage.dmPolicy = "pairing". - Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
openclaw pairing list imessageopenclaw pairing approve imessage <CODE>
- Pairing is the default token exchange for iMessage DMs. Details: Pairing
Groups:
channels.imessage.groupPolicy = open | allowlist | disabled.channels.imessage.groupAllowFromcontrols who can trigger in groups whenallowlistis set.- Mention gating uses
agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns) because iMessage has no native mention metadata. - Multi-agent override: set per-agent patterns on
agents.list[].groupChat.mentionPatterns.
How it works (behavior)
imsgstreams message events; the gateway normalizes them into the shared channel envelope.- Replies always route back to the same chat id or handle.
Group-ish threads (is_group=false)
Some iMessage threads can have multiple participants but still arrive with is_group=false depending on how Messages stores the chat identifier.
If you explicitly configure a chat_id under channels.imessage.groups, OpenClaw treats that thread as a “group” for:
- session isolation (separate
agent:<agentId>:imessage:group:<chat_id>session key) - group allowlisting / mention gating behavior
Example:
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"42": { requireMention: false },
},
},
},
}
This is useful when you want an isolated personality/model for a specific thread (see Multi-agent routing). For filesystem isolation, see Sandboxing.
Media + limits
- Optional attachment ingestion via
channels.imessage.includeAttachments. - Media cap via
channels.imessage.mediaMaxMb.
Limits
- Outbound text is chunked to
channels.imessage.textChunkLimit(default 4000). - Optional newline chunking: set
channels.imessage.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - Media uploads are capped by
channels.imessage.mediaMaxMb(default 16).
Addressing / delivery targets
Prefer chat_id for stable routing:
chat_id:123(preferred)chat_guid:...chat_identifier:...- direct handles:
imessage:+1555/sms:+1555/user@example.com
List chats:
imsg chats --limit 20
Configuration reference (iMessage)
Full configuration: Configuration
Provider options:
channels.imessage.enabled: enable/disable channel startup.channels.imessage.cliPath: path toimsg.channels.imessage.dbPath: Messages DB path.channels.imessage.remoteHost: SSH host for SCP attachment transfer whencliPathpoints to a remote Mac (e.g.,user@gateway-host). Auto-detected from SSH wrapper if not set.channels.imessage.service:imessage | sms | auto.channels.imessage.region: SMS region.channels.imessage.dmPolicy:pairing | allowlist | open | disabled(default: pairing).channels.imessage.allowFrom: DM allowlist (handles, emails, E.164 numbers, orchat_id:*).openrequires"*". iMessage has no usernames; use handles or chat targets.channels.imessage.groupPolicy:open | allowlist | disabled(default: allowlist).channels.imessage.groupAllowFrom: group sender allowlist.channels.imessage.historyLimit/channels.imessage.accounts.*.historyLimit: max group messages to include as context (0 disables).channels.imessage.dmHistoryLimit: DM history limit in user turns. Per-user overrides:channels.imessage.dms["<handle>"].historyLimit.channels.imessage.groups: per-group defaults + allowlist (use"*"for global defaults).channels.imessage.includeAttachments: ingest attachments into context.channels.imessage.mediaMaxMb: inbound/outbound media cap (MB).channels.imessage.textChunkLimit: outbound chunk size (chars).channels.imessage.chunkMode:length(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.
Related global options:
agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns).messages.responsePrefix.
Chat Channels
Source: https://docs.openclaw.ai/channels/index
Chat Channels
OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway. Text is supported everywhere; media and reactions vary by channel.
Supported channels
- WhatsApp — Most popular; uses Baileys and requires QR pairing.
- Telegram — Bot API via grammY; supports groups.
- Discord — Discord Bot API + Gateway; supports servers, channels, and DMs.
- Slack — Bolt SDK; workspace apps.
- Feishu — Feishu/Lark bot via WebSocket (plugin, installed separately).
- Google Chat — Google Chat API app via HTTP webhook.
- Mattermost — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
- Signal — signal-cli; privacy-focused.
- BlueBubbles — Recommended for iMessage; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- iMessage (legacy) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- Microsoft Teams — Bot Framework; enterprise support (plugin, installed separately).
- LINE — LINE Messaging API bot (plugin, installed separately).
- Nextcloud Talk — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
- Matrix — Matrix protocol (plugin, installed separately).
- Nostr — Decentralized DMs via NIP-04 (plugin, installed separately).
- Tlon — Urbit-based messenger (plugin, installed separately).
- Twitch — Twitch chat via IRC connection (plugin, installed separately).
- Zalo — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
- Zalo Personal — Zalo personal account via QR login (plugin, installed separately).
- WebChat — Gateway WebChat UI over WebSocket.
Notes
- Channels can run simultaneously; configure multiple and OpenClaw will route per chat.
- Fastest setup is usually Telegram (simple bot token). WhatsApp requires QR pairing and stores more state on disk.
- Group behavior varies by channel; see Groups.
- DM pairing and allowlists are enforced for safety; see Security.
- Telegram internals: grammY notes.
- Troubleshooting: Channel troubleshooting.
- Model providers are documented separately; see Model Providers.
LINE
Source: https://docs.openclaw.ai/channels/line
LINE (plugin)
LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webhook receiver on the gateway and uses your channel access token + channel secret for authentication.
Status: supported via plugin. Direct messages, group chats, media, locations, Flex messages, template messages, and quick replies are supported. Reactions and threads are not supported.
Plugin required
Install the LINE plugin:
openclaw plugins install @openclaw/line
Local checkout (when running from a git repo):
openclaw plugins install ./extensions/line
Setup
- Create a LINE Developers account and open the Console: https://developers.line.biz/console/
- Create (or pick) a Provider and add a Messaging API channel.
- Copy the Channel access token and Channel secret from the channel settings.
- Enable Use webhook in the Messaging API settings.
- Set the webhook URL to your gateway endpoint (HTTPS required):
https://gateway-host/line/webhook
The gateway responds to LINE’s webhook verification (GET) and inbound events (POST).
If you need a custom path, set channels.line.webhookPath or
channels.line.accounts.<id>.webhookPath and update the URL accordingly.
Configure
Minimal config:
{
channels: {
line: {
enabled: true,
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
channelSecret: "LINE_CHANNEL_SECRET",
dmPolicy: "pairing",
},
},
}
Env vars (default account only):
LINE_CHANNEL_ACCESS_TOKENLINE_CHANNEL_SECRET
Token/secret files:
{
channels: {
line: {
tokenFile: "/path/to/line-token.txt",
secretFile: "/path/to/line-secret.txt",
},
},
}
Multiple accounts:
{
channels: {
line: {
accounts: {
marketing: {
channelAccessToken: "...",
channelSecret: "...",
webhookPath: "/line/marketing",
},
},
},
},
}
Access control
Direct messages default to pairing. Unknown senders get a pairing code and their messages are ignored until approved.
openclaw pairing list line
openclaw pairing approve line <CODE>
Allowlists and policies:
channels.line.dmPolicy:pairing | allowlist | open | disabledchannels.line.allowFrom: allowlisted LINE user IDs for DMschannels.line.groupPolicy:allowlist | open | disabledchannels.line.groupAllowFrom: allowlisted LINE user IDs for groups- Per-group overrides:
channels.line.groups.<groupId>.allowFrom
LINE IDs are case-sensitive. Valid IDs look like:
- User:
U+ 32 hex chars - Group:
C+ 32 hex chars - Room:
R+ 32 hex chars
Message behavior
- Text is chunked at 5000 characters.
- Markdown formatting is stripped; code blocks and tables are converted into Flex cards when possible.
- Streaming responses are buffered; LINE receives full chunks with a loading animation while the agent works.
- Media downloads are capped by
channels.line.mediaMaxMb(default 10).
Channel data (rich messages)
Use channelData.line to send quick replies, locations, Flex cards, or template
messages.
{
text: "Here you go",
channelData: {
line: {
quickReplies: ["Status", "Help"],
location: {
title: "Office",
address: "123 Main St",
latitude: 35.681236,
longitude: 139.767125,
},
flexMessage: {
altText: "Status card",
contents: {
/* Flex payload */
},
},
templateMessage: {
type: "confirm",
text: "Proceed?",
confirmLabel: "Yes",
confirmData: "yes",
cancelLabel: "No",
cancelData: "no",
},
},
},
}
The LINE plugin also ships a /card command for Flex message presets:
/card info "Welcome" "Thanks for joining!"
Troubleshooting
- Webhook verification fails: ensure the webhook URL is HTTPS and the
channelSecretmatches the LINE console. - No inbound events: confirm the webhook path matches
channels.line.webhookPathand that the gateway is reachable from LINE. - Media download errors: raise
channels.line.mediaMaxMbif media exceeds the default limit.
Channel Location Parsing
Source: https://docs.openclaw.ai/channels/location
Channel location parsing
OpenClaw normalizes shared locations from chat channels into:
- human-readable text appended to the inbound body, and
- structured fields in the auto-reply context payload.
Currently supported:
- Telegram (location pins + venues + live locations)
- WhatsApp (locationMessage + liveLocationMessage)
- Matrix (
m.locationwithgeo_uri)
Text formatting
Locations are rendered as friendly lines without brackets:
- Pin:
📍 48.858844, 2.294351 ±12m
- Named place:
📍 Eiffel Tower — Champ de Mars, Paris (48.858844, 2.294351 ±12m)
- Live share:
🛰 Live location: 48.858844, 2.294351 ±12m
If the channel includes a caption/comment, it is appended on the next line:
📍 48.858844, 2.294351 ±12m
Meet here
Context fields
When a location is present, these fields are added to ctx:
LocationLat(number)LocationLon(number)LocationAccuracy(number, meters; optional)LocationName(string; optional)LocationAddress(string; optional)LocationSource(pin | place | live)LocationIsLive(boolean)
Channel notes
- Telegram: venues map to
LocationName/LocationAddress; live locations uselive_period. - WhatsApp:
locationMessage.commentandliveLocationMessage.captionare appended as the caption line. - Matrix:
geo_uriis parsed as a pin location; altitude is ignored andLocationIsLiveis always false.
Matrix
Source: https://docs.openclaw.ai/channels/matrix
Matrix (plugin)
Matrix is an open, decentralized messaging protocol. OpenClaw connects as a Matrix user on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too, but it requires E2EE to be enabled.
Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions, polls (send + poll-start as text), location, and E2EE (with crypto support).
Plugin required
Matrix ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
openclaw plugins install @openclaw/matrix
Local checkout (when running from a git repo):
openclaw plugins install ./extensions/matrix
If you choose Matrix during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.
Details: Plugins
Setup
-
Install the Matrix plugin:
- From npm:
openclaw plugins install @openclaw/matrix - From a local checkout:
openclaw plugins install ./extensions/matrix
- From npm:
-
Create a Matrix account on a homeserver:
- Browse hosting options at https://matrix.org/ecosystem/hosting/
- Or host it yourself.
-
Get an access token for the bot account:
- Use the Matrix login API with
curlat your home server:
curl --request POST \ --url https://matrix.example.org/_matrix/client/v3/login \ --header 'Content-Type: application/json' \ --data '{ "type": "m.login.password", "identifier": { "type": "m.id.user", "user": "your-user-name" }, "password": "your-password" }'- Replace
matrix.example.orgwith your homeserver URL. - Or set
channels.matrix.userId+channels.matrix.password: OpenClaw calls the same login endpoint, stores the access token in~/.openclaw/credentials/matrix/credentials.json, and reuses it on next start.
- Use the Matrix login API with
-
Configure credentials:
- Env:
MATRIX_HOMESERVER,MATRIX_ACCESS_TOKEN(orMATRIX_USER_ID+MATRIX_PASSWORD) - Or config:
channels.matrix.* - If both are set, config takes precedence.
- With access token: user ID is fetched automatically via
/whoami. - When set,
channels.matrix.userIdshould be the full Matrix ID (example:@bot:example.org).
- Env:
-
Restart the gateway (or finish onboarding).
-
Start a DM with the bot or invite it to a room from any Matrix client (Element, Beeper, etc.; see https://matrix.org/ecosystem/clients/). Beeper requires E2EE, so set
channels.matrix.encryption: trueand verify the device.
Minimal config (access token, user ID auto-fetched):
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_***",
dm: { policy: "pairing" },
},
},
}
E2EE config (end to end encryption enabled):
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_***",
encryption: true,
dm: { policy: "pairing" },
},
},
}
Encryption (E2EE)
End-to-end encryption is supported via the Rust crypto SDK.
Enable with channels.matrix.encryption: true:
- If the crypto module loads, encrypted rooms are decrypted automatically.
- Outbound media is encrypted when sending to encrypted rooms.
- On first connection, OpenClaw requests device verification from your other sessions.
- Verify the device in another Matrix client (Element, etc.) to enable key sharing.
- If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt; OpenClaw logs a warning.
- If you see missing crypto module errors (for example,
@matrix-org/matrix-sdk-crypto-nodejs-*), allow build scripts for@matrix-org/matrix-sdk-crypto-nodejsand runpnpm rebuild @matrix-org/matrix-sdk-crypto-nodejsor fetch the binary withnode node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js.
Crypto state is stored per account + access token in
~/.openclaw/matrix/accounts/<account>/<homeserver>__<user>/<token-hash>/crypto/
(SQLite database). Sync state lives alongside it in bot-storage.json.
If the access token (device) changes, a new store is created and the bot must be
re-verified for encrypted rooms.
Device verification: When E2EE is enabled, the bot will request verification from your other sessions on startup. Open Element (or another client) and approve the verification request to establish trust. Once verified, the bot can decrypt messages in encrypted rooms.
Routing model
- Replies always go back to Matrix.
- DMs share the agent's main session; rooms map to group sessions.
Access control (DMs)
- Default:
channels.matrix.dm.policy = "pairing". Unknown senders get a pairing code. - Approve via:
openclaw pairing list matrixopenclaw pairing approve matrix <CODE>
- Public DMs:
channels.matrix.dm.policy="open"pluschannels.matrix.dm.allowFrom=["*"]. channels.matrix.dm.allowFromaccepts full Matrix user IDs (example:@user:server). The wizard resolves display names to user IDs when directory search finds a single exact match.
Rooms (groups)
- Default:
channels.matrix.groupPolicy = "allowlist"(mention-gated). Usechannels.defaults.groupPolicyto override the default when unset. - Allowlist rooms with
channels.matrix.groups(room IDs or aliases; names are resolved to IDs when directory search finds a single exact match):
{
channels: {
matrix: {
groupPolicy: "allowlist",
groups: {
"!roomId:example.org": { allow: true },
"#alias:example.org": { allow: true },
},
groupAllowFrom: ["@owner:example.org"],
},
},
}
requireMention: falseenables auto-reply in that room.groups."*"can set defaults for mention gating across rooms.groupAllowFromrestricts which senders can trigger the bot in rooms (full Matrix user IDs).- Per-room
usersallowlists can further restrict senders inside a specific room (use full Matrix user IDs). - The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names only on an exact, unique match.
- On startup, OpenClaw resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are ignored for allowlist matching.
- Invites are auto-joined by default; control with
channels.matrix.autoJoinandchannels.matrix.autoJoinAllowlist. - To allow no rooms, set
channels.matrix.groupPolicy: "disabled"(or keep an empty allowlist). - Legacy key:
channels.matrix.rooms(same shape asgroups).
Threads
- Reply threading is supported.
channels.matrix.threadRepliescontrols whether replies stay in threads:off,inbound(default),always
channels.matrix.replyToModecontrols reply-to metadata when not replying in a thread:off(default),first,all
Capabilities
| Feature | Status |
|---|---|
| Direct messages | ✅ Supported |
| Rooms | ✅ Supported |
| Threads | ✅ Supported |
| Media | ✅ Supported |
| E2EE | ✅ Supported (crypto module required) |
| Reactions | ✅ Supported (send/read via tools) |
| Polls | ✅ Send supported; inbound poll starts are converted to text (responses/ends ignored) |
| Location | ✅ Supported (geo URI; altitude ignored) |
| Native commands | ✅ Supported |
Configuration reference (Matrix)
Full configuration: Configuration
Provider options:
channels.matrix.enabled: enable/disable channel startup.channels.matrix.homeserver: homeserver URL.channels.matrix.userId: Matrix user ID (optional with access token).channels.matrix.accessToken: access token.channels.matrix.password: password for login (token stored).channels.matrix.deviceName: device display name.channels.matrix.encryption: enable E2EE (default: false).channels.matrix.initialSyncLimit: initial sync limit.channels.matrix.threadReplies:off | inbound | always(default: inbound).channels.matrix.textChunkLimit: outbound text chunk size (chars).channels.matrix.chunkMode:length(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.matrix.dm.policy:pairing | allowlist | open | disabled(default: pairing).channels.matrix.dm.allowFrom: DM allowlist (full Matrix user IDs).openrequires"*". The wizard resolves names to IDs when possible.channels.matrix.groupPolicy:allowlist | open | disabled(default: allowlist).channels.matrix.groupAllowFrom: allowlisted senders for group messages (full Matrix user IDs).channels.matrix.allowlistOnly: force allowlist rules for DMs + rooms.channels.matrix.groups: group allowlist + per-room settings map.channels.matrix.rooms: legacy group allowlist/config.channels.matrix.replyToMode: reply-to mode for threads/tags.channels.matrix.mediaMaxMb: inbound/outbound media cap (MB).channels.matrix.autoJoin: invite handling (always | allowlist | off, default: always).channels.matrix.autoJoinAllowlist: allowed room IDs/aliases for auto-join.channels.matrix.actions: per-action tool gating (reactions/messages/pins/memberInfo/channelInfo).
Mattermost
Source: https://docs.openclaw.ai/channels/mattermost
Mattermost (plugin)
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at mattermost.com for product details and downloads.
Plugin required
Mattermost ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
openclaw plugins install @openclaw/mattermost
Local checkout (when running from a git repo):
openclaw plugins install ./extensions/mattermost
If you choose Mattermost during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.
Details: Plugins
Quick setup
- Install the Mattermost plugin.
- Create a Mattermost bot account and copy the bot token.
- Copy the Mattermost base URL (e.g.,
https://chat.example.com). - Configure OpenClaw and start the gateway.
Minimal config:
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
Environment variables (default account)
Set these on the gateway host if you prefer env vars:
MATTERMOST_BOT_TOKEN=...MATTERMOST_URL=https://chat.example.com
Env vars apply only to the default account (default). Other accounts must use config values.
Chat modes
Mattermost responds to DMs automatically. Channel behavior is controlled by chatmode:
oncall(default): respond only when @mentioned in channels.onmessage: respond to every channel message.onchar: respond when a message starts with a trigger prefix.
Config example:
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"],
},
},
}
Notes:
oncharstill responds to explicit @mentions.channels.mattermost.requireMentionis honored for legacy configs butchatmodeis preferred.
Access control (DMs)
- Default:
channels.mattermost.dmPolicy = "pairing"(unknown senders get a pairing code). - Approve via:
openclaw pairing list mattermostopenclaw pairing approve mattermost <CODE>
- Public DMs:
channels.mattermost.dmPolicy="open"pluschannels.mattermost.allowFrom=["*"].
Channels (groups)
- Default:
channels.mattermost.groupPolicy = "allowlist"(mention-gated). - Allowlist senders with
channels.mattermost.groupAllowFrom(user IDs or@username). - Open channels:
channels.mattermost.groupPolicy="open"(mention-gated).
Targets for outbound delivery
Use these target formats with openclaw message send or cron/webhooks:
channel:<id>for a channeluser:<id>for a DM@usernamefor a DM (resolved via the Mattermost API)
Bare IDs are treated as channels.
Multi-account
Mattermost supports multiple accounts under channels.mattermost.accounts:
{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" },
},
},
},
}
Troubleshooting
- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set
chatmode: "onmessage". - Auth errors: check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the
defaultaccount.
Microsoft Teams
Source: https://docs.openclaw.ai/channels/msteams
Microsoft Teams (plugin)
"Abandon all hope, ye who enter here."
Updated: 2026-01-21
Status: text + DM attachments are supported; channel/group file sending requires sharePointSiteId + Graph permissions (see Sending files in group chats). Polls are sent via Adaptive Cards.
Plugin required
Microsoft Teams ships as a plugin and is not bundled with the core install.
Breaking change (2026.1.15): MS Teams moved out of core. If you use it, you must install the plugin.
Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.
Install via CLI (npm registry):
openclaw plugins install @openclaw/msteams
Local checkout (when running from a git repo):
openclaw plugins install ./extensions/msteams
If you choose Teams during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.
Details: Plugins
Quick setup (beginner)
- Install the Microsoft Teams plugin.
- Create an Azure Bot (App ID + client secret + tenant ID).
- Configure OpenClaw with those credentials.
- Expose
/api/messages(port 3978 by default) via a public URL or tunnel. - Install the Teams app package and start the gateway.
Minimal config:
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Note: group chats are blocked by default (channels.msteams.groupPolicy: "allowlist"). To allow group replies, set channels.msteams.groupAllowFrom (or use groupPolicy: "open" to allow any member, mention-gated).
Goals
- Talk to OpenClaw via Teams DMs, group chats, or channels.
- Keep routing deterministic: replies always go back to the channel they arrived on.
- Default to safe channel behavior (mentions required unless configured otherwise).
Config writes
By default, Microsoft Teams is allowed to write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { msteams: { configWrites: false } },
}
Access control (DMs + groups)
DM access
- Default:
channels.msteams.dmPolicy = "pairing". Unknown senders are ignored until approved. channels.msteams.allowFromaccepts AAD object IDs, UPNs, or display names. The wizard resolves names to IDs via Microsoft Graph when credentials allow.
Group access
- Default:
channels.msteams.groupPolicy = "allowlist"(blocked unless you addgroupAllowFrom). Usechannels.defaults.groupPolicyto override the default when unset. channels.msteams.groupAllowFromcontrols which senders can trigger in group chats/channels (falls back tochannels.msteams.allowFrom).- Set
groupPolicy: "open"to allow any member (still mention‑gated by default). - To allow no channels, set
channels.msteams.groupPolicy: "disabled".
Example:
{
channels: {
msteams: {
groupPolicy: "allowlist",
groupAllowFrom: ["user@org.com"],
},
},
}
Teams + channel allowlist
- Scope group/channel replies by listing teams and channels under
channels.msteams.teams. - Keys can be team IDs or names; channel keys can be conversation IDs or names.
- When
groupPolicy="allowlist"and a teams allowlist is present, only listed teams/channels are accepted (mention‑gated). - The configure wizard accepts
Team/Channelentries and stores them for you. - On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow) and logs the mapping; unresolved entries are kept as typed.
Example:
{
channels: {
msteams: {
groupPolicy: "allowlist",
teams: {
"My Team": {
channels: {
General: { requireMention: true },
},
},
},
},
},
}
How it works
- Install the Microsoft Teams plugin.
- Create an Azure Bot (App ID + secret + tenant ID).
- Build a Teams app package that references the bot and includes the RSC permissions below.
- Upload/install the Teams app into a team (or personal scope for DMs).
- Configure
msteamsin~/.openclaw/openclaw.json(or env vars) and start the gateway. - The gateway listens for Bot Framework webhook traffic on
/api/messagesby default.
Azure Bot Setup (Prerequisites)
Before configuring OpenClaw, you need to create an Azure Bot resource.
Step 1: Create Azure Bot
- Go to Create Azure Bot
- Fill in the Basics tab:
Field Value Bot handle Your bot name, e.g., openclaw-msteams(must be unique)Subscription Select your Azure subscription Resource group Create new or use existing Pricing tier Free for dev/testing Type of App Single Tenant (recommended - see note below) Creation type Create new Microsoft App ID
Deprecation notice: Creation of new multi-tenant bots was deprecated after 2025-07-31. Use Single Tenant for new bots.
- Click Review + create → Create (wait ~1-2 minutes)
Step 2: Get Credentials
- Go to your Azure Bot resource → Configuration
- Copy Microsoft App ID → this is your
appId - Click Manage Password → go to the App Registration
- Under Certificates & secrets → New client secret → copy the Value → this is your
appPassword - Go to Overview → copy Directory (tenant) ID → this is your
tenantId
Step 3: Configure Messaging Endpoint
- In Azure Bot → Configuration
- Set Messaging endpoint to your webhook URL:
- Production:
https://your-domain.com/api/messages - Local dev: Use a tunnel (see Local Development below)
- Production:
Step 4: Enable Teams Channel
- In Azure Bot → Channels
- Click Microsoft Teams → Configure → Save
- Accept the Terms of Service
Local Development (Tunneling)
Teams can't reach localhost. Use a tunnel for local development:
Option A: ngrok
ngrok http 3978
# Copy the https URL, e.g., https://abc123.ngrok.io
# Set messaging endpoint to: https://abc123.ngrok.io/api/messages
Option B: Tailscale Funnel
tailscale funnel 3978
# Use your Tailscale funnel URL as the messaging endpoint
Teams Developer Portal (Alternative)
Instead of manually creating a manifest ZIP, you can use the Teams Developer Portal:
- Click + New app
- Fill in basic info (name, description, developer info)
- Go to App features → Bot
- Select Enter a bot ID manually and paste your Azure Bot App ID
- Check scopes: Personal, Team, Group Chat
- Click Distribute → Download app package
- In Teams: Apps → Manage your apps → Upload a custom app → select the ZIP
This is often easier than hand-editing JSON manifests.
Testing the Bot
Option A: Azure Web Chat (verify webhook first)
- In Azure Portal → your Azure Bot resource → Test in Web Chat
- Send a message - you should see a response
- This confirms your webhook endpoint works before Teams setup
Option B: Teams (after app installation)
- Install the Teams app (sideload or org catalog)
- Find the bot in Teams and send a DM
- Check gateway logs for incoming activity
Setup (minimal text-only)
-
Install the Microsoft Teams plugin
- From npm:
openclaw plugins install @openclaw/msteams - From a local checkout:
openclaw plugins install ./extensions/msteams
- From npm:
-
Bot registration
- Create an Azure Bot (see above) and note:
- App ID
- Client secret (App password)
- Tenant ID (single-tenant)
- Create an Azure Bot (see above) and note:
-
Teams app manifest
- Include a
botentry withbotId = <App ID>. - Scopes:
personal,team,groupChat. supportsFiles: true(required for personal scope file handling).- Add RSC permissions (below).
- Create icons:
outline.png(32x32) andcolor.png(192x192). - Zip all three files together:
manifest.json,outline.png,color.png.
- Include a
-
Configure OpenClaw
{ "msteams": { "enabled": true, "appId": "<APP_ID>", "appPassword": "<APP_PASSWORD>", "tenantId": "<TENANT_ID>", "webhook": { "port": 3978, "path": "/api/messages" } } }You can also use environment variables instead of config keys:
MSTEAMS_APP_IDMSTEAMS_APP_PASSWORDMSTEAMS_TENANT_ID
-
Bot endpoint
- Set the Azure Bot Messaging Endpoint to:
https://<host>:3978/api/messages(or your chosen path/port).
- Set the Azure Bot Messaging Endpoint to:
-
Run the gateway
- The Teams channel starts automatically when the plugin is installed and
msteamsconfig exists with credentials.
- The Teams channel starts automatically when the plugin is installed and
History context
channels.msteams.historyLimitcontrols how many recent channel/group messages are wrapped into the prompt.- Falls back to
messages.groupChat.historyLimit. Set0to disable (default 50). - DM history can be limited with
channels.msteams.dmHistoryLimit(user turns). Per-user overrides:channels.msteams.dms["<user_id>"].historyLimit.
Current Teams RSC Permissions (Manifest)
These are the existing resourceSpecific permissions in our Teams app manifest. They only apply inside the team/chat where the app is installed.
For channels (team scope):
ChannelMessage.Read.Group(Application) - receive all channel messages without @mentionChannelMessage.Send.Group(Application)Member.Read.Group(Application)Owner.Read.Group(Application)ChannelSettings.Read.Group(Application)TeamMember.Read.Group(Application)TeamSettings.Read.Group(Application)
For group chats:
ChatMessage.Read.Chat(Application) - receive all group chat messages without @mention
Example Teams Manifest (redacted)
Minimal, valid example with the required fields. Replace IDs and URLs.
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
"manifestVersion": "1.23",
"version": "1.0.0",
"id": "00000000-0000-0000-0000-000000000000",
"name": { "short": "OpenClaw" },
"developer": {
"name": "Your Org",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"termsOfUseUrl": "https://example.com/terms"
},
"description": { "short": "OpenClaw in Teams", "full": "OpenClaw in Teams" },
"icons": { "outline": "outline.png", "color": "color.png" },
"accentColor": "#5B6DEF",
"bots": [
{
"botId": "11111111-1111-1111-1111-111111111111",
"scopes": ["personal", "team", "groupChat"],
"isNotificationOnly": false,
"supportsCalling": false,
"supportsVideo": false,
"supportsFiles": true
}
],
"webApplicationInfo": {
"id": "11111111-1111-1111-1111-111111111111"
},
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "ChannelMessage.Send.Group", "type": "Application" },
{ "name": "Member.Read.Group", "type": "Application" },
{ "name": "Owner.Read.Group", "type": "Application" },
{ "name": "ChannelSettings.Read.Group", "type": "Application" },
{ "name": "TeamMember.Read.Group", "type": "Application" },
{ "name": "TeamSettings.Read.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
]
}
}
}
Manifest caveats (must-have fields)
bots[].botIdmust match the Azure Bot App ID.webApplicationInfo.idmust match the Azure Bot App ID.bots[].scopesmust include the surfaces you plan to use (personal,team,groupChat).bots[].supportsFiles: trueis required for file handling in personal scope.authorization.permissions.resourceSpecificmust include channel read/send if you want channel traffic.
Updating an existing app
To update an already-installed Teams app (e.g., to add RSC permissions):
- Update your
manifest.jsonwith the new settings - Increment the
versionfield (e.g.,1.0.0→1.1.0) - Re-zip the manifest with icons (
manifest.json,outline.png,color.png) - Upload the new zip:
- Option A (Teams Admin Center): Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version
- Option B (Sideload): In Teams → Apps → Manage your apps → Upload a custom app
- For team channels: Reinstall the app in each team for new permissions to take effect
- Fully quit and relaunch Teams (not just close the window) to clear cached app metadata
Capabilities: RSC only vs Graph
With Teams RSC only (app installed, no Graph API permissions)
Works:
- Read channel message text content.
- Send channel message text content.
- Receive personal (DM) file attachments.
Does NOT work:
- Channel/group image or file contents (payload only includes HTML stub).
- Downloading attachments stored in SharePoint/OneDrive.
- Reading message history (beyond the live webhook event).
With Teams RSC + Microsoft Graph Application permissions
Adds:
- Downloading hosted contents (images pasted into messages).
- Downloading file attachments stored in SharePoint/OneDrive.
- Reading channel/chat message history via Graph.
RSC vs Graph API
| Capability | RSC Permissions | Graph API |
|---|---|---|
| Real-time messages | Yes (via webhook) | No (polling only) |
| Historical messages | No | Yes (can query history) |
| Setup complexity | App manifest only | Requires admin consent + token flow |
| Works offline | No (must be running) | Yes (query anytime) |
Bottom line: RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with ChannelMessage.Read.All (requires admin consent).
Graph-enabled media + history (required for channels)
If you need images/files in channels or want to fetch message history, you must enable Microsoft Graph permissions and grant admin consent.
- In Entra ID (Azure AD) App Registration, add Microsoft Graph Application permissions:
ChannelMessage.Read.All(channel attachments + history)Chat.Read.AllorChatMessage.Read.All(group chats)
- Grant admin consent for the tenant.
- Bump the Teams app manifest version, re-upload, and reinstall the app in Teams.
- Fully quit and relaunch Teams to clear cached app metadata.
Known Limitations
Webhook timeouts
Teams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see:
- Gateway timeouts
- Teams retrying the message (causing duplicates)
- Dropped replies
OpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues.
Formatting
Teams markdown is more limited than Slack or Discord:
- Basic formatting works: bold, italic,
code, links - Complex markdown (tables, nested lists) may not render correctly
- Adaptive Cards are supported for polls and arbitrary card sends (see below)
Configuration
Key settings (see /gateway/configuration for shared channel patterns):
channels.msteams.enabled: enable/disable the channel.channels.msteams.appId,channels.msteams.appPassword,channels.msteams.tenantId: bot credentials.channels.msteams.webhook.port(default3978)channels.msteams.webhook.path(default/api/messages)channels.msteams.dmPolicy:pairing | allowlist | open | disabled(default: pairing)channels.msteams.allowFrom: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.channels.msteams.textChunkLimit: outbound text chunk size.channels.msteams.chunkMode:length(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.msteams.mediaAllowHosts: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).channels.msteams.mediaAuthAllowHosts: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts).channels.msteams.requireMention: require @mention in channels/groups (default true).channels.msteams.replyStyle:thread | top-level(see Reply Style).channels.msteams.teams.<teamId>.replyStyle: per-team override.channels.msteams.teams.<teamId>.requireMention: per-team override.channels.msteams.teams.<teamId>.tools: default per-team tool policy overrides (allow/deny/alsoAllow) used when a channel override is missing.channels.msteams.teams.<teamId>.toolsBySender: default per-team per-sender tool policy overrides ("*"wildcard supported).channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle: per-channel override.channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention: per-channel override.channels.msteams.teams.<teamId>.channels.<conversationId>.tools: per-channel tool policy overrides (allow/deny/alsoAllow).channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender: per-channel per-sender tool policy overrides ("*"wildcard supported).channels.msteams.sharePointSiteId: SharePoint site ID for file uploads in group chats/channels (see Sending files in group chats).
Routing & Sessions
- Session keys follow the standard agent format (see /concepts/session):
- Direct messages share the main session (
agent:<agentId>:<mainKey>). - Channel/group messages use conversation id:
agent:<agentId>:msteams:channel:<conversationId>agent:<agentId>:msteams:group:<conversationId>
- Direct messages share the main session (
Reply Style: Threads vs Posts
Teams recently introduced two channel UI styles over the same underlying data model:
| Style | Description | Recommended replyStyle |
|---|---|---|
| Posts (classic) | Messages appear as cards with threaded replies underneath | thread (default) |
| Threads (Slack-like) | Messages flow linearly, more like Slack | top-level |
The problem: The Teams API does not expose which UI style a channel uses. If you use the wrong replyStyle:
threadin a Threads-style channel → replies appear nested awkwardlytop-levelin a Posts-style channel → replies appear as separate top-level posts instead of in-thread
Solution: Configure replyStyle per-channel based on how the channel is set up:
{
"msteams": {
"replyStyle": "thread",
"teams": {
"19:abc...@thread.tacv2": {
"channels": {
"19:xyz...@thread.tacv2": {
"replyStyle": "top-level"
}
}
}
}
}
}
Attachments & Images
Current limitations:
- DMs: Images and file attachments work via Teams bot file APIs.
- Channels/groups: Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. Graph API permissions are required to download channel attachments.
Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot).
By default, OpenClaw only downloads media from Microsoft/Teams hostnames. Override with channels.msteams.mediaAllowHosts (use ["*"] to allow any host).
Authorization headers are only attached for hosts in channels.msteams.mediaAuthAllowHosts (defaults to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes).
Sending files in group chats
Bots can send files in DMs using the FileConsentCard flow (built-in). However, sending files in group chats/channels requires additional setup:
| Context | How files are sent | Setup needed |
|---|---|---|
| DMs | FileConsentCard → user accepts → bot uploads | Works out of the box |
| Group chats/channels | Upload to SharePoint → share link | Requires sharePointSiteId + Graph permissions |
| Images (any context) | Base64-encoded inline | Works out of the box |
Why group chats need SharePoint
Bots don't have a personal OneDrive drive (the /me/drive Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a SharePoint site and creates a sharing link.
Setup
-
Add Graph API permissions in Entra ID (Azure AD) → App Registration:
Sites.ReadWrite.All(Application) - upload files to SharePointChat.Read.All(Application) - optional, enables per-user sharing links
-
Grant admin consent for the tenant.
-
Get your SharePoint site ID:
# Via Graph Explorer or curl with a valid token: curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}" # Example: for a site at "contoso.sharepoint.com/sites/BotFiles" curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles" # Response includes: "id": "contoso.sharepoint.com,guid1,guid2" -
Configure OpenClaw:
{ channels: { msteams: { // ... other config ... sharePointSiteId: "contoso.sharepoint.com,guid1,guid2", }, }, }
Sharing behavior
| Permission | Sharing behavior |
|---|---|
Sites.ReadWrite.All only |
Organization-wide sharing link (anyone in org can access) |
Sites.ReadWrite.All + Chat.Read.All |
Per-user sharing link (only chat members can access) |
Per-user sharing is more secure as only the chat participants can access the file. If Chat.Read.All permission is missing, the bot falls back to organization-wide sharing.
Fallback behavior
| Scenario | Result |
|---|---|
Group chat + file + sharePointSiteId configured |
Upload to SharePoint, send sharing link |
Group chat + file + no sharePointSiteId |
Attempt OneDrive upload (may fail), send text only |
| Personal chat + file | FileConsentCard flow (works without SharePoint) |
| Any context + image | Base64-encoded inline (works without SharePoint) |
Files stored location
Uploaded files are stored in a /OpenClawShared/ folder in the configured SharePoint site's default document library.
Polls (Adaptive Cards)
OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).
- CLI:
openclaw message poll --channel msteams --target conversation:<id> ... - Votes are recorded by the gateway in
~/.openclaw/msteams-polls.json. - The gateway must stay online to record votes.
- Polls do not auto-post result summaries yet (inspect the store file if needed).
Adaptive Cards (arbitrary)
Send any Adaptive Card JSON to Teams users or conversations using the message tool or CLI.
The card parameter accepts an Adaptive Card JSON object. When card is provided, the message text is optional.
Agent tool:
{
"action": "send",
"channel": "msteams",
"target": "user:<id>",
"card": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [{ "type": "TextBlock", "text": "Hello!" }]
}
}
CLI:
openclaw message send --channel msteams \
--target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'
See Adaptive Cards documentation for card schema and examples. For target format details, see Target formats below.
Target formats
MSTeams targets use prefixes to distinguish between users and conversations:
| Target type | Format | Example |
|---|---|---|
| User (by ID) | user:<aad-object-id> |
user:40a1a0ed-4ff2-4164-a219-55518990c197 |
| User (by name) | user:<display-name> |
user:John Smith (requires Graph API) |
| Group/channel | conversation:<conversation-id> |
conversation:19:abc123...@thread.tacv2 |
| Group/channel (raw) | <conversation-id> |
19:abc123...@thread.tacv2 (if contains @thread) |
CLI examples:
# Send to a user by ID
openclaw message send --channel msteams --target "user:40a1a0ed-..." --message "Hello"
# Send to a user by display name (triggers Graph API lookup)
openclaw message send --channel msteams --target "user:John Smith" --message "Hello"
# Send to a group chat or channel
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"
# Send an Adaptive Card to a conversation
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'
Agent tool examples:
{
"action": "send",
"channel": "msteams",
"target": "user:John Smith",
"message": "Hello!"
}
{
"action": "send",
"channel": "msteams",
"target": "conversation:19:abc...@thread.tacv2",
"card": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [{ "type": "TextBlock", "text": "Hello" }]
}
}
Note: Without the user: prefix, names default to group/team resolution. Always use user: when targeting people by display name.
Proactive messaging
- Proactive messages are only possible after a user has interacted, because we store conversation references at that point.
- See
/gateway/configurationfordmPolicyand allowlist gating.
Team and Channel IDs (Common Gotcha)
The groupId query parameter in Teams URLs is NOT the team ID used for configuration. Extract IDs from the URL path instead:
Team URL:
https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
└────────────────────────────┘
Team ID (URL-decode this)
Channel URL:
https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=...
└─────────────────────────┘
Channel ID (URL-decode this)
For config:
- Team ID = path segment after
/team/(URL-decoded, e.g.,19:Bk4j...@thread.tacv2) - Channel ID = path segment after
/channel/(URL-decoded) - Ignore the
groupIdquery parameter
Private Channels
Bots have limited support in private channels:
| Feature | Standard Channels | Private Channels |
|---|---|---|
| Bot installation | Yes | Limited |
| Real-time messages (webhook) | Yes | May not work |
| RSC permissions | Yes | May behave differently |
| @mentions | Yes | If bot is accessible |
| Graph API history | Yes | Yes (with permissions) |
Workarounds if private channels don't work:
- Use standard channels for bot interactions
- Use DMs - users can always message the bot directly
- Use Graph API for historical access (requires
ChannelMessage.Read.All)
Troubleshooting
Common issues
- Images not showing in channels: Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
- No responses in channel: mentions are required by default; set
channels.msteams.requireMention=falseor configure per team/channel. - Version mismatch (Teams still shows old manifest): remove + re-add the app and fully quit Teams to refresh.
- 401 Unauthorized from webhook: Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly.
Manifest upload errors
- "Icon file cannot be empty": The manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for
outline.png, 192x192 forcolor.png). - "webApplicationInfo.Id already in use": The app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation.
- "Something went wrong" on upload: Upload via https://admin.teams.microsoft.com instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error.
- Sideload failing: Try "Upload an app to your org's app catalog" instead of "Upload a custom app" - this often bypasses sideload restrictions.
RSC permissions not working
- Verify
webApplicationInfo.idmatches your bot's App ID exactly - Re-upload the app and reinstall in the team/chat
- Check if your org admin has blocked RSC permissions
- Confirm you're using the right scope:
ChannelMessage.Read.Groupfor teams,ChatMessage.Read.Chatfor group chats
References
- Create Azure Bot - Azure Bot setup guide
- Teams Developer Portal - create/manage Teams apps
- Teams app manifest schema
- Receive channel messages with RSC
- RSC permissions reference
- Teams bot file handling (channel/group requires Graph)
- Proactive messaging
Signal
Source: https://docs.openclaw.ai/channels/signal
Signal (signal-cli)
Status: external CLI integration. Gateway talks to signal-cli over HTTP JSON-RPC + SSE.
Quick setup (beginner)
- Use a separate Signal number for the bot (recommended).
- Install
signal-cli(Java required). - Link the bot device and start the daemon:
signal-cli link -n "OpenClaw"
- Configure OpenClaw and start the gateway.
Minimal config:
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"],
},
},
}
What it is
- Signal channel via
signal-cli(not embedded libsignal). - Deterministic routing: replies always go back to Signal.
- DMs share the agent's main session; groups are isolated (
agent:<agentId>:signal:group:<groupId>).
Config writes
By default, Signal is allowed to write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { signal: { configWrites: false } },
}
The number model (important)
- The gateway connects to a Signal device (the
signal-cliaccount). - If you run the bot on your personal Signal account, it will ignore your own messages (loop protection).
- For "I text the bot and it replies," use a separate bot number.
Setup (fast path)
- Install
signal-cli(Java required). - Link a bot account:
signal-cli link -n "OpenClaw"then scan the QR in Signal.
- Configure Signal and start the gateway.
Example:
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"],
},
},
}
Multi-account support: use channels.signal.accounts with per-account config and optional name. See gateway/configuration for the shared pattern.
External daemon mode (httpUrl)
If you want to manage signal-cli yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point OpenClaw at it:
{
channels: {
signal: {
httpUrl: "http://127.0.0.1:8080",
autoStart: false,
},
},
}
This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set channels.signal.startupTimeoutMs.
Access control (DMs + groups)
DMs:
- Default:
channels.signal.dmPolicy = "pairing". - Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
openclaw pairing list signalopenclaw pairing approve signal <CODE>
- Pairing is the default token exchange for Signal DMs. Details: Pairing
- UUID-only senders (from
sourceUuid) are stored asuuid:<id>inchannels.signal.allowFrom.
Groups:
channels.signal.groupPolicy = open | allowlist | disabled.channels.signal.groupAllowFromcontrols who can trigger in groups whenallowlistis set.
How it works (behavior)
signal-cliruns as a daemon; the gateway reads events via SSE.- Inbound messages are normalized into the shared channel envelope.
- Replies always route back to the same number or group.
Media + limits
- Outbound text is chunked to
channels.signal.textChunkLimit(default 4000). - Optional newline chunking: set
channels.signal.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - Attachments supported (base64 fetched from
signal-cli). - Default media cap:
channels.signal.mediaMaxMb(default 8). - Use
channels.signal.ignoreAttachmentsto skip downloading media. - Group history context uses
channels.signal.historyLimit(orchannels.signal.accounts.*.historyLimit), falling back tomessages.groupChat.historyLimit. Set0to disable (default 50).
Typing + read receipts
- Typing indicators: OpenClaw sends typing signals via
signal-cli sendTypingand refreshes them while a reply is running. - Read receipts: when
channels.signal.sendReadReceiptsis true, OpenClaw forwards read receipts for allowed DMs. - Signal-cli does not expose read receipts for groups.
Reactions (message tool)
- Use
message action=reactwithchannel=signal. - Targets: sender E.164 or UUID (use
uuid:<id>from pairing output; bare UUID works too). messageIdis the Signal timestamp for the message you’re reacting to.- Group reactions require
targetAuthorortargetAuthorUuid.
Examples:
message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥
message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true
message action=react channel=signal target=signal:group:<groupId> targetAuthor=uuid:<sender-uuid> messageId=1737630212345 emoji=✅
Config:
channels.signal.actions.reactions: enable/disable reaction actions (default true).channels.signal.reactionLevel:off | ack | minimal | extensive.off/ackdisables agent reactions (message toolreactwill error).minimal/extensiveenables agent reactions and sets the guidance level.
- Per-account overrides:
channels.signal.accounts.<id>.actions.reactions,channels.signal.accounts.<id>.reactionLevel.
Delivery targets (CLI/cron)
- DMs:
signal:+15551234567(or plain E.164). - UUID DMs:
uuid:<id>(or bare UUID). - Groups:
signal:group:<groupId>. - Usernames:
username:<name>(if supported by your Signal account).
Configuration reference (Signal)
Full configuration: Configuration
Provider options:
channels.signal.enabled: enable/disable channel startup.channels.signal.account: E.164 for the bot account.channels.signal.cliPath: path tosignal-cli.channels.signal.httpUrl: full daemon URL (overrides host/port).channels.signal.httpHost,channels.signal.httpPort: daemon bind (default 127.0.0.1:8080).channels.signal.autoStart: auto-spawn daemon (default true ifhttpUrlunset).channels.signal.startupTimeoutMs: startup wait timeout in ms (cap 120000).channels.signal.receiveMode:on-start | manual.channels.signal.ignoreAttachments: skip attachment downloads.channels.signal.ignoreStories: ignore stories from the daemon.channels.signal.sendReadReceipts: forward read receipts.channels.signal.dmPolicy:pairing | allowlist | open | disabled(default: pairing).channels.signal.allowFrom: DM allowlist (E.164 oruuid:<id>).openrequires"*". Signal has no usernames; use phone/UUID ids.channels.signal.groupPolicy:open | allowlist | disabled(default: allowlist).channels.signal.groupAllowFrom: group sender allowlist.channels.signal.historyLimit: max group messages to include as context (0 disables).channels.signal.dmHistoryLimit: DM history limit in user turns. Per-user overrides:channels.signal.dms["<phone_or_uuid>"].historyLimit.channels.signal.textChunkLimit: outbound chunk size (chars).channels.signal.chunkMode:length(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.signal.mediaMaxMb: inbound/outbound media cap (MB).
Related global options:
agents.list[].groupChat.mentionPatterns(Signal does not support native mentions).messages.groupChat.mentionPatterns(global fallback).messages.responsePrefix.
Slack
Source: https://docs.openclaw.ai/channels/slack
Slack
Socket mode (default)
Quick setup (beginner)
- Create a Slack app and enable Socket Mode.
- Create an App Token (
xapp-...) and Bot Token (xoxb-...). - Set tokens for OpenClaw and start the gateway.
Minimal config:
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
},
},
}
Setup
- Create a Slack app (From scratch) in https://api.slack.com/apps.
- Socket Mode → toggle on. Then go to Basic Information → App-Level Tokens → Generate Token and Scopes with scope
connections:write. Copy the App Token (xapp-...). - OAuth & Permissions → add bot token scopes (use the manifest below). Click Install to Workspace. Copy the Bot User OAuth Token (
xoxb-...). - Optional: OAuth & Permissions → add User Token Scopes (see the read-only list below). Reinstall the app and copy the User OAuth Token (
xoxp-...). - Event Subscriptions → enable events and subscribe to:
message.*(includes edits/deletes/thread broadcasts)app_mentionreaction_added,reaction_removedmember_joined_channel,member_left_channelchannel_renamepin_added,pin_removed
- Invite the bot to channels you want it to read.
- Slash Commands → create
/openclawif you usechannels.slack.slashCommand. If you enable native commands, add one slash command per built-in command (same names as/help). Native defaults to off for Slack unless you setchannels.slack.commands.native: true(globalcommands.nativeis"auto"which leaves Slack off). - App Home → enable the Messages Tab so users can DM the bot.
Use the manifest below so scopes and events stay in sync.
Multi-account support: use channels.slack.accounts with per-account tokens and optional name. See gateway/configuration for the shared pattern.
OpenClaw config (minimal)
Set tokens via env vars (recommended):
SLACK_APP_TOKEN=xapp-...SLACK_BOT_TOKEN=xoxb-...
Or via config:
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
},
},
}
User token (optional)
OpenClaw can use a Slack user token (xoxp-...) for read operations (history,
pins, reactions, emoji, member info). By default this stays read-only: reads
prefer the user token when present, and writes still use the bot token unless
you explicitly opt in. Even with userTokenReadOnly: false, the bot token stays
preferred for writes when it is available.
User tokens are configured in the config file (no env var support). For
multi-account, set channels.slack.accounts.<id>.userToken.
Example with bot + app + user tokens:
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
userToken: "xoxp-...",
},
},
}
Example with userTokenReadOnly explicitly set (allow user token writes):
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
userToken: "xoxp-...",
userTokenReadOnly: false,
},
},
}
Token usage
- Read operations (history, reactions list, pins list, emoji list, member info, search) prefer the user token when configured, otherwise the bot token.
- Write operations (send/edit/delete messages, add/remove reactions, pin/unpin,
file uploads) use the bot token by default. If
userTokenReadOnly: falseand no bot token is available, OpenClaw falls back to the user token.
History context
channels.slack.historyLimit(orchannels.slack.accounts.*.historyLimit) controls how many recent channel/group messages are wrapped into the prompt.- Falls back to
messages.groupChat.historyLimit. Set0to disable (default 50).
HTTP mode (Events API)
Use HTTP webhook mode when your Gateway is reachable by Slack over HTTPS (typical for server deployments). HTTP mode uses the Events API + Interactivity + Slash Commands with a shared request URL.
Setup
- Create a Slack app and disable Socket Mode (optional if you only use HTTP).
- Basic Information → copy the Signing Secret.
- OAuth & Permissions → install the app and copy the Bot User OAuth Token (
xoxb-...). - Event Subscriptions → enable events and set the Request URL to your gateway webhook path (default
/slack/events). - Interactivity & Shortcuts → enable and set the same Request URL.
- Slash Commands → set the same Request URL for your command(s).
Example request URL:
https://gateway-host/slack/events
OpenClaw config (minimal)
{
channels: {
slack: {
enabled: true,
mode: "http",
botToken: "xoxb-...",
signingSecret: "your-signing-secret",
webhookPath: "/slack/events",
},
},
}
Multi-account HTTP mode: set channels.slack.accounts.<id>.mode = "http" and provide a unique
webhookPath per account so each Slack app can point to its own URL.
Manifest (optional)
Use this Slack app manifest to create the app quickly (adjust the name/command if you want). Include the user scopes if you plan to configure a user token.
{
"display_information": {
"name": "OpenClaw",
"description": "Slack connector for OpenClaw"
},
"features": {
"bot_user": {
"display_name": "OpenClaw",
"always_online": false
},
"app_home": {
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"slash_commands": [
{
"command": "/openclaw",
"description": "Send a message to OpenClaw",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"chat:write",
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"groups:write",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"mpim:write",
"users:read",
"app_mentions:read",
"reactions:read",
"reactions:write",
"pins:read",
"pins:write",
"emoji:read",
"commands",
"files:read",
"files:write"
],
"user": [
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"im:history",
"im:read",
"mpim:history",
"mpim:read",
"users:read",
"reactions:read",
"pins:read",
"emoji:read",
"search:read"
]
}
},
"settings": {
"socket_mode_enabled": true,
"event_subscriptions": {
"bot_events": [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"reaction_added",
"reaction_removed",
"member_joined_channel",
"member_left_channel",
"channel_rename",
"pin_added",
"pin_removed"
]
}
}
}
If you enable native commands, add one slash_commands entry per command you want to expose (matching the /help list). Override with channels.slack.commands.native.
Scopes (current vs optional)
Slack's Conversations API is type-scoped: you only need the scopes for the conversation types you actually touch (channels, groups, im, mpim). See https://docs.slack.dev/apis/web-api/using-the-conversations-api/ for the overview.
Bot token scopes (required)
chat:write(send/update/delete messages viachat.postMessage) https://docs.slack.dev/reference/methods/chat.postMessageim:write(open DMs viaconversations.openfor user DMs) https://docs.slack.dev/reference/methods/conversations.openchannels:history,groups:history,im:history,mpim:historyhttps://docs.slack.dev/reference/methods/conversations.historychannels:read,groups:read,im:read,mpim:readhttps://docs.slack.dev/reference/methods/conversations.infousers:read(user lookup) https://docs.slack.dev/reference/methods/users.inforeactions:read,reactions:write(reactions.get/reactions.add) https://docs.slack.dev/reference/methods/reactions.get https://docs.slack.dev/reference/methods/reactions.addpins:read,pins:write(pins.list/pins.add/pins.remove) https://docs.slack.dev/reference/scopes/pins.read https://docs.slack.dev/reference/scopes/pins.writeemoji:read(emoji.list) https://docs.slack.dev/reference/scopes/emoji.readfiles:write(uploads viafiles.uploadV2) https://docs.slack.dev/messaging/working-with-files/#upload
User token scopes (optional, read-only by default)
Add these under User Token Scopes if you configure channels.slack.userToken.
channels:history,groups:history,im:history,mpim:historychannels:read,groups:read,im:read,mpim:readusers:readreactions:readpins:reademoji:readsearch:read
Not needed today (but likely future)
mpim:write(only if we add group-DM open/DM start viaconversations.open)groups:write(only if we add private-channel management: create/rename/invite/archive)chat:write.public(only if we want to post to channels the bot isn't in) https://docs.slack.dev/reference/scopes/chat.write.publicusers:read.email(only if we need email fields fromusers.info) https://docs.slack.dev/changelog/2017-04-narrowing-email-accessfiles:read(only if we start listing/reading file metadata)
Config
Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
{
"slack": {
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-...",
"groupPolicy": "allowlist",
"dm": {
"enabled": true,
"policy": "pairing",
"allowFrom": ["U123", "U456", "*"],
"groupEnabled": false,
"groupChannels": ["G123"],
"replyToMode": "all"
},
"channels": {
"C123": { "allow": true, "requireMention": true },
"#general": {
"allow": true,
"requireMention": true,
"users": ["U123"],
"skills": ["search", "docs"],
"systemPrompt": "Keep answers short."
}
},
"reactionNotifications": "own",
"reactionAllowlist": ["U123"],
"replyToMode": "off",
"actions": {
"reactions": true,
"messages": true,
"pins": true,
"memberInfo": true,
"emojiList": true
},
"slashCommand": {
"enabled": true,
"name": "openclaw",
"sessionPrefix": "slack:slash",
"ephemeral": true
},
"textChunkLimit": 4000,
"mediaMaxMb": 20
}
}
Tokens can also be supplied via env vars:
SLACK_BOT_TOKENSLACK_APP_TOKEN
Ack reactions are controlled globally via messages.ackReaction +
messages.ackReactionScope. Use messages.removeAckAfterReply to clear the
ack reaction after the bot replies.
Limits
- Outbound text is chunked to
channels.slack.textChunkLimit(default 4000). - Optional newline chunking: set
channels.slack.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - Media uploads are capped by
channels.slack.mediaMaxMb(default 20).
Reply threading
By default, OpenClaw replies in the main channel. Use channels.slack.replyToMode to control automatic threading:
| Mode | Behavior |
|---|---|
off |
Default. Reply in main channel. Only thread if the triggering message was already in a thread. |
first |
First reply goes to thread (under the triggering message), subsequent replies go to main channel. Useful for keeping context visible while avoiding thread clutter. |
all |
All replies go to thread. Keeps conversations contained but may reduce visibility. |
The mode applies to both auto-replies and agent tool calls (slack sendMessage).
Per-chat-type threading
You can configure different threading behavior per chat type by setting channels.slack.replyToModeByChatType:
{
channels: {
slack: {
replyToMode: "off", // default for channels
replyToModeByChatType: {
direct: "all", // DMs always thread
group: "first", // group DMs/MPIM thread first reply
},
},
},
}
Supported chat types:
direct: 1:1 DMs (Slackim)group: group DMs / MPIMs (Slackmpim)channel: standard channels (public/private)
Precedence:
replyToModeByChatType.<chatType>replyToMode- Provider default (
off)
Legacy channels.slack.dm.replyToMode is still accepted as a fallback for direct when no chat-type override is set.
Examples:
Thread DMs only:
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { direct: "all" },
},
},
}
Thread group DMs but keep channels in the root:
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { group: "first" },
},
},
}
Make channels thread, keep DMs in the root:
{
channels: {
slack: {
replyToMode: "first",
replyToModeByChatType: { direct: "off", group: "off" },
},
},
}
Manual threading tags
For fine-grained control, use these tags in agent responses:
[[reply_to_current]]— reply to the triggering message (start/continue thread).[[reply_to:<id>]]— reply to a specific message id.
Sessions + routing
- DMs share the
mainsession (like WhatsApp/Telegram). - Channels map to
agent:<agentId>:slack:channel:<channelId>sessions. - Slash commands use
agent:<agentId>:slack:slash:<userId>sessions (prefix configurable viachannels.slack.slashCommand.sessionPrefix). - If Slack doesn’t provide
channel_type, OpenClaw infers it from the channel ID prefix (D,C,G) and defaults tochannelto keep session keys stable. - Native command registration uses
commands.native(global default"auto"→ Slack off) and can be overridden per-workspace withchannels.slack.commands.native. Text commands require standalone/...messages and can be disabled withcommands.text: false. Slack slash commands are managed in the Slack app and are not removed automatically. Usecommands.useAccessGroups: falseto bypass access-group checks for commands. - Full command list + config: Slash commands
DM security (pairing)
- Default:
channels.slack.dm.policy="pairing"— unknown DM senders get a pairing code (expires after 1 hour). - Approve via:
openclaw pairing approve slack <code>. - To allow anyone: set
channels.slack.dm.policy="open"andchannels.slack.dm.allowFrom=["*"]. channels.slack.dm.allowFromaccepts user IDs, @handles, or emails (resolved at startup when tokens allow). The wizard accepts usernames and resolves them to ids during setup when tokens allow.
Group policy
channels.slack.groupPolicycontrols channel handling (open|disabled|allowlist).allowlistrequires channels to be listed inchannels.slack.channels.- If you only set
SLACK_BOT_TOKEN/SLACK_APP_TOKENand never create achannels.slacksection, the runtime defaultsgroupPolicytoopen. Addchannels.slack.groupPolicy,channels.defaults.groupPolicy, or a channel allowlist to lock it down. - The configure wizard accepts
#channelnames and resolves them to IDs when possible (public + private); if multiple matches exist, it prefers the active channel. - On startup, OpenClaw resolves channel/user names in allowlists to IDs (when tokens allow) and logs the mapping; unresolved entries are kept as typed.
- To allow no channels, set
channels.slack.groupPolicy: "disabled"(or keep an empty allowlist).
Channel options (channels.slack.channels.<id> or channels.slack.channels.<name>):
allow: allow/deny the channel whengroupPolicy="allowlist".requireMention: mention gating for the channel.tools: optional per-channel tool policy overrides (allow/deny/alsoAllow).toolsBySender: optional per-sender tool policy overrides within the channel (keys are sender ids/@handles/emails;"*"wildcard supported).allowBots: allow bot-authored messages in this channel (default: false).users: optional per-channel user allowlist.skills: skill filter (omit = all skills, empty = none).systemPrompt: extra system prompt for the channel (combined with topic/purpose).enabled: setfalseto disable the channel.
Delivery targets
Use these with cron/CLI sends:
user:<id>for DMschannel:<id>for channels
Tool actions
Slack tool actions can be gated with channels.slack.actions.*:
| Action group | Default | Notes |
|---|---|---|
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
Security notes
- Writes default to the bot token so state-changing actions stay scoped to the app's bot permissions and identity.
- Setting
userTokenReadOnly: falseallows the user token to be used for write operations when a bot token is unavailable, which means actions run with the installing user's access. Treat the user token as highly privileged and keep action gates and allowlists tight. - If you enable user-token writes, make sure the user token includes the write
scopes you expect (
chat:write,reactions:write,pins:write,files:write) or those operations will fail.
Notes
- Mention gating is controlled via
channels.slack.channels(setrequireMentiontotrue);agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns) also count as mentions. - Multi-agent override: set per-agent patterns on
agents.list[].groupChat.mentionPatterns. - Reaction notifications follow
channels.slack.reactionNotifications(usereactionAllowlistwith modeallowlist). - Bot-authored messages are ignored by default; enable via
channels.slack.allowBotsorchannels.slack.channels.<id>.allowBots. - Warning: If you allow replies to other bots (
channels.slack.allowBots=trueorchannels.slack.channels.<id>.allowBots=true), prevent bot-to-bot reply loops withrequireMention,channels.slack.channels.<id>.usersallowlists, and/or clear guardrails inAGENTS.mdandSOUL.md. - For the Slack tool, reaction removal semantics are in /tools/reactions.
- Attachments are downloaded to the media store when permitted and under the size limit.
Telegram
Source: https://docs.openclaw.ai/channels/telegram
Telegram (Bot API)
Status: production-ready for bot DMs + groups via grammY. Long-polling by default; webhook optional.
Quick setup (beginner)
- Create a bot with @BotFather (direct link). Confirm the handle is exactly
@BotFather, then copy the token. - Set the token:
- Env:
TELEGRAM_BOT_TOKEN=... - Or config:
channels.telegram.botToken: "...". - If both are set, config takes precedence (env fallback is default-account only).
- Env:
- Start the gateway.
- DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing",
},
},
}
What it is
- A Telegram Bot API channel owned by the Gateway.
- Deterministic routing: replies go back to Telegram; the model never chooses channels.
- DMs share the agent's main session; groups stay isolated (
agent:<agentId>:telegram:group:<chatId>).
Setup (fast path)
1) Create a bot token (BotFather)
- Open Telegram and chat with @BotFather (direct link). Confirm the handle is exactly
@BotFather. - Run
/newbot, then follow the prompts (name + username ending inbot). - Copy the token and store it safely.
Optional BotFather settings:
/setjoingroups— allow/deny adding the bot to groups./setprivacy— control whether the bot sees all group messages.
2) Configure the token (env or config)
Example:
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing",
groups: { "*": { requireMention: true } },
},
},
}
Env option: TELEGRAM_BOT_TOKEN=... (works for the default account).
If both env and config are set, config takes precedence.
Multi-account support: use channels.telegram.accounts with per-account tokens and optional name. See gateway/configuration for the shared pattern.
- Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
- DM access defaults to pairing. Approve the code when the bot is first contacted.
- For groups: add the bot, decide privacy/admin behavior (below), then set
channels.telegram.groupsto control mention gating + allowlists.
Token + privacy + permissions (Telegram side)
Token creation (BotFather)
/newbotcreates the bot and returns the token (keep it secret).- If a token leaks, revoke/regenerate it via @BotFather and update your config.
Group message visibility (Privacy Mode)
Telegram bots default to Privacy Mode, which limits which group messages they receive. If your bot must see all group messages, you have two options:
- Disable privacy mode with
/setprivacyor - Add the bot as a group admin (admin bots receive all messages).
Note: When you toggle privacy mode, Telegram requires removing + re‑adding the bot to each group for the change to take effect.
Group permissions (admin rights)
Admin status is set inside the group (Telegram UI). Admin bots always receive all group messages, so use admin if you need full visibility.
How it works (behavior)
- Inbound messages are normalized into the shared channel envelope with reply context and media placeholders.
- Group replies require a mention by default (native @mention or
agents.list[].groupChat.mentionPatterns/messages.groupChat.mentionPatterns). - Multi-agent override: set per-agent patterns on
agents.list[].groupChat.mentionPatterns. - Replies always route back to the same Telegram chat.
- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by
agents.defaults.maxConcurrent. - Telegram Bot API does not support read receipts; there is no
sendReadReceiptsoption.
Draft streaming
OpenClaw can stream partial replies in Telegram DMs using sendMessageDraft.
Requirements:
- Threaded Mode enabled for the bot in @BotFather (forum topic mode).
- Private chat threads only (Telegram includes
message_thread_idon inbound messages). channels.telegram.streamModenot set to"off"(default:"partial","block"enables chunked draft updates).
Draft streaming is DM-only; Telegram does not support it in groups or channels.
Formatting (Telegram HTML)
- Outbound Telegram text uses
parse_mode: "HTML"(Telegram’s supported tag subset). - Markdown-ish input is rendered into Telegram-safe HTML (bold/italic/strike/code/links); block elements are flattened to text with newlines/bullets.
- Raw HTML from models is escaped to avoid Telegram parse errors.
- If Telegram rejects the HTML payload, OpenClaw retries the same message as plain text.
Commands (native + custom)
OpenClaw registers native commands (like /status, /reset, /model) with Telegram’s bot menu on startup.
You can add custom commands to the menu via config:
{
channels: {
telegram: {
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" },
],
},
},
}
Troubleshooting
setMyCommands failedin logs usually means outbound HTTPS/DNS is blocked toapi.telegram.org.- If you see
sendMessageorsendChatActionfailures, check IPv6 routing and DNS.
More help: Channel troubleshooting.
Notes:
- Custom commands are menu entries only; OpenClaw does not implement them unless you handle them elsewhere.
- Command names are normalized (leading
/stripped, lowercased) and must matcha-z,0-9,_(1–32 chars). - Custom commands cannot override native commands. Conflicts are ignored and logged.
- If
commands.nativeis disabled, only custom commands are registered (or cleared if none).
Limits
- Outbound text is chunked to
channels.telegram.textChunkLimit(default 4000). - Optional newline chunking: set
channels.telegram.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - Media downloads/uploads are capped by
channels.telegram.mediaMaxMb(default 5). - Telegram Bot API requests time out after
channels.telegram.timeoutSeconds(default 500 via grammY). Set lower to avoid long hangs. - Group history context uses
channels.telegram.historyLimit(orchannels.telegram.accounts.*.historyLimit), falling back tomessages.groupChat.historyLimit. Set0to disable (default 50). - DM history can be limited with
channels.telegram.dmHistoryLimit(user turns). Per-user overrides:channels.telegram.dms["<user_id>"].historyLimit.
Group activation modes
By default, the bot only responds to mentions in groups (@botname or patterns in agents.list[].groupChat.mentionPatterns). To change this behavior:
Via config (recommended)
{
channels: {
telegram: {
groups: {
"-1001234567890": { requireMention: false }, // always respond in this group
},
},
},
}
Important: Setting channels.telegram.groups creates an allowlist - only listed groups (or "*") will be accepted.
Forum topics inherit their parent group config (allowFrom, requireMention, skills, prompts) unless you add per-topic overrides under channels.telegram.groups.<groupId>.topics.<topicId>.
To allow all groups with always-respond:
{
channels: {
telegram: {
groups: {
"*": { requireMention: false }, // all groups, always respond
},
},
},
}
To keep mention-only for all groups (default behavior):
{
channels: {
telegram: {
groups: {
"*": { requireMention: true }, // or omit groups entirely
},
},
},
}
Via command (session-level)
Send in the group:
/activation always- respond to all messages/activation mention- require mentions (default)
Note: Commands update session state only. For persistent behavior across restarts, use config.
Getting the group chat ID
Forward any message from the group to @userinfobot or @getidsbot on Telegram to see the chat ID (negative number like -1001234567890).
Tip: For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use /whoami once commands are enabled.
Privacy note: @userinfobot is a third-party bot. If you prefer, add the bot to the group, send a message, and use openclaw logs --follow to read chat.id, or use the Bot API getUpdates.
Config writes
By default, Telegram is allowed to write config updates triggered by channel events or /config set|unset.
This happens when:
- A group is upgraded to a supergroup and Telegram emits
migrate_to_chat_id(chat ID changes). OpenClaw can migratechannels.telegram.groupsautomatically. - You run
/config setor/config unsetin a Telegram chat (requirescommands.config: true).
Disable with:
{
channels: { telegram: { configWrites: false } },
}
Topics (forum supergroups)
Telegram forum topics include a message_thread_id per message. OpenClaw:
- Appends
:topic:<threadId>to the Telegram group session key so each topic is isolated. - Sends typing indicators and replies with
message_thread_idso responses stay in the topic. - General topic (thread id
1) is special: message sends omitmessage_thread_id(Telegram rejects it), but typing indicators still include it. - Exposes
MessageThreadId+IsForumin template context for routing/templating. - Topic-specific configuration is available under
channels.telegram.groups.<chatId>.topics.<threadId>(skills, allowlists, auto-reply, system prompts, disable). - Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.
Private chats can include message_thread_id in some edge cases. OpenClaw keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
Inline Buttons
Telegram supports inline keyboards with callback buttons.
{
channels: {
telegram: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
}
For per-account configuration:
{
channels: {
telegram: {
accounts: {
main: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
},
},
}
Scopes:
off— inline buttons disableddm— only DMs (group targets blocked)group— only groups (DM targets blocked)all— DMs + groupsallowlist— DMs + groups, but only senders allowed byallowFrom/groupAllowFrom(same rules as control commands)
Default: allowlist.
Legacy: capabilities: ["inlineButtons"] = inlineButtons: "all".
Sending buttons
Use the message tool with the buttons parameter:
{
action: "send",
channel: "telegram",
to: "123456789",
message: "Choose an option:",
buttons: [
[
{ text: "Yes", callback_data: "yes" },
{ text: "No", callback_data: "no" },
],
[{ text: "Cancel", callback_data: "cancel" }],
],
}
When a user clicks a button, the callback data is sent back to the agent as a message with the format:
callback_data: value
Configuration options
Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported):
channels.telegram.capabilities: Global default capability config applied to all Telegram accounts unless overridden.channels.telegram.accounts.<account>.capabilities: Per-account capabilities that override the global defaults for that specific account.
Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups).
Access control (DMs + groups)
DM access
- Default:
channels.telegram.dmPolicy = "pairing". Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). - Approve via:
openclaw pairing list telegramopenclaw pairing approve telegram <CODE>
- Pairing is the default token exchange used for Telegram DMs. Details: Pairing
channels.telegram.allowFromaccepts numeric user IDs (recommended) or@usernameentries. It is not the bot username; use the human sender’s ID. The wizard accepts@usernameand resolves it to the numeric ID when possible.
Finding your Telegram user ID
Safer (no third-party bot):
- Start the gateway and DM your bot.
- Run
openclaw logs --followand look forfrom.id.
Alternate (official Bot API):
- DM your bot.
- Fetch updates with your bot token and read
message.from.id:curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Third-party (less private):
- DM
@userinfobotor@getidsbotand use the returned user id.
Group access
Two independent controls:
1. Which groups are allowed (group allowlist via channels.telegram.groups):
- No
groupsconfig = all groups allowed - With
groupsconfig = only listed groups or"*"are allowed - Example:
"groups": { "-1001234567890": {}, "*": {} }allows all groups
2. Which senders are allowed (sender filtering via channels.telegram.groupPolicy):
"open"= all senders in allowed groups can message"allowlist"= only senders inchannels.telegram.groupAllowFromcan message"disabled"= no group messages accepted at all Default isgroupPolicy: "allowlist"(blocked unless you addgroupAllowFrom).
Most users want: groupPolicy: "allowlist" + groupAllowFrom + specific groups listed in channels.telegram.groups
Long-polling vs webhook
- Default: long-polling (no public URL required).
- Webhook mode: set
channels.telegram.webhookUrlandchannels.telegram.webhookSecret(optionallychannels.telegram.webhookPath).- The local listener binds to
0.0.0.0:8787and servesPOST /telegram-webhookby default. - If your public URL is different, use a reverse proxy and point
channels.telegram.webhookUrlat the public endpoint.
- The local listener binds to
Reply threading
Telegram supports optional threaded replies via tags:
[[reply_to_current]]-- reply to the triggering message.[[reply_to:<id>]]-- reply to a specific message id.
Controlled by channels.telegram.replyToMode:
first(default),all,off.
Audio messages (voice vs file)
Telegram distinguishes voice notes (round bubble) from audio files (metadata card). OpenClaw defaults to audio files for backward compatibility.
To force a voice note bubble in agent replies, include this tag anywhere in the reply:
[[audio_as_voice]]— send audio as a voice note instead of a file.
The tag is stripped from the delivered text. Other channels ignore this tag.
For message tool sends, set asVoice: true with a voice-compatible audio media URL
(message is optional when media is present):
{
action: "send",
channel: "telegram",
to: "123456789",
media: "https://example.com/voice.ogg",
asVoice: true,
}
Stickers
OpenClaw supports receiving and sending Telegram stickers with intelligent caching.
Receiving stickers
When a user sends a sticker, OpenClaw handles it based on the sticker type:
- Static stickers (WEBP): Downloaded and processed through vision. The sticker appears as a
<media:sticker>placeholder in the message content. - Animated stickers (TGS): Skipped (Lottie format not supported for processing).
- Video stickers (WEBM): Skipped (video format not supported for processing).
Template context field available when receiving stickers:
Sticker— object with:emoji— emoji associated with the stickersetName— name of the sticker setfileId— Telegram file ID (send the same sticker back)fileUniqueId— stable ID for cache lookupcachedDescription— cached vision description when available
Sticker cache
Stickers are processed through the AI's vision capabilities to generate descriptions. Since the same stickers are often sent repeatedly, OpenClaw caches these descriptions to avoid redundant API calls.
How it works:
- First encounter: The sticker image is sent to the AI for vision analysis. The AI generates a description (e.g., "A cartoon cat waving enthusiastically").
- Cache storage: The description is saved along with the sticker's file ID, emoji, and set name.
- Subsequent encounters: When the same sticker is seen again, the cached description is used directly. The image is not sent to the AI.
Cache location: ~/.openclaw/telegram/sticker-cache.json
Cache entry format:
{
"fileId": "CAACAgIAAxkBAAI...",
"fileUniqueId": "AgADBAADb6cxG2Y",
"emoji": "👋",
"setName": "CoolCats",
"description": "A cartoon cat waving enthusiastically",
"cachedAt": "2026-01-15T10:30:00.000Z"
}
Benefits:
- Reduces API costs by avoiding repeated vision calls for the same sticker
- Faster response times for cached stickers (no vision processing delay)
- Enables sticker search functionality based on cached descriptions
The cache is populated automatically as stickers are received. There is no manual cache management required.
Sending stickers
The agent can send and search stickers using the sticker and sticker-search actions. These are disabled by default and must be enabled in config:
{
channels: {
telegram: {
actions: {
sticker: true,
},
},
},
}
Send a sticker:
{
action: "sticker",
channel: "telegram",
to: "123456789",
fileId: "CAACAgIAAxkBAAI...",
}
Parameters:
fileId(required) — the Telegram file ID of the sticker. Obtain this fromSticker.fileIdwhen receiving a sticker, or from asticker-searchresult.replyTo(optional) — message ID to reply to.threadId(optional) — message thread ID for forum topics.
Search for stickers:
The agent can search cached stickers by description, emoji, or set name:
{
action: "sticker-search",
channel: "telegram",
query: "cat waving",
limit: 5,
}
Returns matching stickers from the cache:
{
ok: true,
count: 2,
stickers: [
{
fileId: "CAACAgIAAxkBAAI...",
emoji: "👋",
description: "A cartoon cat waving enthusiastically",
setName: "CoolCats",
},
],
}
The search uses fuzzy matching across description text, emoji characters, and set names.
Example with threading:
{
action: "sticker",
channel: "telegram",
to: "-1001234567890",
fileId: "CAACAgIAAxkBAAI...",
replyTo: 42,
threadId: 123,
}
Streaming (drafts)
Telegram can stream draft bubbles while the agent is generating a response.
OpenClaw uses Bot API sendMessageDraft (not real messages) and then sends the
final reply as a normal message.
Requirements (Telegram Bot API 9.3+):
- Private chats with topics enabled (forum topic mode for the bot).
- Incoming messages must include
message_thread_id(private topic thread). - Streaming is ignored for groups/supergroups/channels.
Config:
channels.telegram.streamMode: "off" | "partial" | "block"(default:partial)partial: update the draft bubble with the latest streaming text.block: update the draft bubble in larger blocks (chunked).off: disable draft streaming.
- Optional (only for
streamMode: "block"):channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }- defaults:
minChars: 200,maxChars: 800,breakPreference: "paragraph"(clamped tochannels.telegram.textChunkLimit).
- defaults:
Note: draft streaming is separate from block streaming (channel messages).
Block streaming is off by default and requires channels.telegram.blockStreaming: true
if you want early Telegram messages instead of draft updates.
Reasoning stream (Telegram only):
/reasoning streamstreams reasoning into the draft bubble while the reply is generating, then sends the final answer without reasoning.- If
channels.telegram.streamModeisoff, reasoning stream is disabled. More context: Streaming + chunking.
Retry policy
Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via channels.telegram.retry. See Retry policy.
Agent tool (messages + reactions)
- Tool:
telegramwithsendMessageaction (to,content, optionalmediaUrl,replyToMessageId,messageThreadId). - Tool:
telegramwithreactaction (chatId,messageId,emoji). - Tool:
telegramwithdeleteMessageaction (chatId,messageId). - Reaction removal semantics: see /tools/reactions.
- Tool gating:
channels.telegram.actions.reactions,channels.telegram.actions.sendMessage,channels.telegram.actions.deleteMessage(default: enabled), andchannels.telegram.actions.sticker(default: disabled).
Reaction notifications
How reactions work:
Telegram reactions arrive as separate message_reaction events, not as properties in message payloads. When a user adds a reaction, OpenClaw:
- Receives the
message_reactionupdate from Telegram API - Converts it to a system event with format:
"Telegram reaction added: {emoji} by {user} on msg {id}" - Enqueues the system event using the same session key as regular messages
- When the next message arrives in that conversation, system events are drained and prepended to the agent's context
The agent sees reactions as system notifications in the conversation history, not as message metadata.
Configuration:
-
channels.telegram.reactionNotifications: Controls which reactions trigger notifications"off"— ignore all reactions"own"— notify when users react to bot messages (best-effort; in-memory) (default)"all"— notify for all reactions
-
channels.telegram.reactionLevel: Controls agent's reaction capability"off"— agent cannot react to messages"ack"— bot sends acknowledgment reactions (👀 while processing) (default)"minimal"— agent can react sparingly (guideline: 1 per 5-10 exchanges)"extensive"— agent can react liberally when appropriate
Forum groups: Reactions in forum groups include message_thread_id and use session keys like agent:main:telegram:group:{chatId}:topic:{threadId}. This ensures reactions and messages in the same topic stay together.
Example config:
{
channels: {
telegram: {
reactionNotifications: "all", // See all reactions
reactionLevel: "minimal", // Agent can react sparingly
},
},
}
Requirements:
- Telegram bots must explicitly request
message_reactioninallowed_updates(configured automatically by OpenClaw) - For webhook mode, reactions are included in the webhook
allowed_updates - For polling mode, reactions are included in the
getUpdatesallowed_updates
Delivery targets (CLI/cron)
- Use a chat id (
123456789) or a username (@name) as the target. - Example:
openclaw message send --channel telegram --target 123456789 --message "hi".
Troubleshooting
Bot doesn’t respond to non-mention messages in a group:
- If you set
channels.telegram.groups.*.requireMention=false, Telegram’s Bot API privacy mode must be disabled.- BotFather:
/setprivacy→ Disable (then remove + re-add the bot to the group)
- BotFather:
openclaw channels statusshows a warning when config expects unmentioned group messages.openclaw channels status --probecan additionally check membership for explicit numeric group IDs (it can’t audit wildcard"*"rules).- Quick test:
/activation always(session-only; use config for persistence)
Bot not seeing group messages at all:
- If
channels.telegram.groupsis set, the group must be listed or use"*" - Check Privacy Settings in @BotFather → "Group Privacy" should be OFF
- Verify bot is actually a member (not just an admin with no read access)
- Check gateway logs:
openclaw logs --follow(look for "skipping group message")
Bot responds to mentions but not /activation always:
- The
/activationcommand updates session state but doesn't persist to config - For persistent behavior, add group to
channels.telegram.groupswithrequireMention: false
Commands like /status don't work:
- Make sure your Telegram user ID is authorized (via pairing or
channels.telegram.allowFrom) - Commands require authorization even in groups with
groupPolicy: "open"
Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):
- Node 22+ is stricter about
AbortSignalinstances; foreign signals can abortfetchcalls right away. - Upgrade to a OpenClaw build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade.
Bot starts, then silently stops responding (or logs HttpError: Network request ... failed):
- Some hosts resolve
api.telegram.orgto IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests. - Fix by enabling IPv6 egress or forcing IPv4 resolution for
api.telegram.org(for example, add an/etc/hostsentry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway. - Quick check:
dig +short api.telegram.org Aanddig +short api.telegram.org AAAAto confirm what DNS returns.
Configuration reference (Telegram)
Full configuration: Configuration
Provider options:
channels.telegram.enabled: enable/disable channel startup.channels.telegram.botToken: bot token (BotFather).channels.telegram.tokenFile: read token from file path.channels.telegram.dmPolicy:pairing | allowlist | open | disabled(default: pairing).channels.telegram.allowFrom: DM allowlist (ids/usernames).openrequires"*".channels.telegram.groupPolicy:open | allowlist | disabled(default: allowlist).channels.telegram.groupAllowFrom: group sender allowlist (ids/usernames).channels.telegram.groups: per-group defaults + allowlist (use"*"for global defaults).channels.telegram.groups.<id>.requireMention: mention gating default.channels.telegram.groups.<id>.skills: skill filter (omit = all skills, empty = none).channels.telegram.groups.<id>.allowFrom: per-group sender allowlist override.channels.telegram.groups.<id>.systemPrompt: extra system prompt for the group.channels.telegram.groups.<id>.enabled: disable the group whenfalse.channels.telegram.groups.<id>.topics.<threadId>.*: per-topic overrides (same fields as group).channels.telegram.groups.<id>.topics.<threadId>.requireMention: per-topic mention gating override.
channels.telegram.capabilities.inlineButtons:off | dm | group | all | allowlist(default: allowlist).channels.telegram.accounts.<account>.capabilities.inlineButtons: per-account override.channels.telegram.replyToMode:off | first | all(default:first).channels.telegram.textChunkLimit: outbound chunk size (chars).channels.telegram.chunkMode:length(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.telegram.linkPreview: toggle link previews for outbound messages (default: true).channels.telegram.streamMode:off | partial | block(draft streaming).channels.telegram.mediaMaxMb: inbound/outbound media cap (MB).channels.telegram.retry: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).channels.telegram.network.autoSelectFamily: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.channels.telegram.proxy: proxy URL for Bot API calls (SOCKS/HTTP).channels.telegram.webhookUrl: enable webhook mode (requireschannels.telegram.webhookSecret).channels.telegram.webhookSecret: webhook secret (required when webhookUrl is set).channels.telegram.webhookPath: local webhook path (default/telegram-webhook).channels.telegram.actions.reactions: gate Telegram tool reactions.channels.telegram.actions.sendMessage: gate Telegram tool message sends.channels.telegram.actions.deleteMessage: gate Telegram tool message deletes.channels.telegram.actions.sticker: gate Telegram sticker actions — send and search (default: false).channels.telegram.reactionNotifications:off | own | all— control which reactions trigger system events (default:ownwhen not set).channels.telegram.reactionLevel:off | ack | minimal | extensive— control agent's reaction capability (default:minimalwhen not set).
Related global options:
agents.list[].groupChat.mentionPatterns(mention gating patterns).messages.groupChat.mentionPatterns(global fallback).commands.native(defaults to"auto"→ on for Telegram/Discord, off for Slack),commands.text,commands.useAccessGroups(command behavior). Override withchannels.telegram.commands.native.messages.responsePrefix,messages.ackReaction,messages.ackReactionScope,messages.removeAckAfterReply.
Channel Troubleshooting
Source: https://docs.openclaw.ai/channels/troubleshooting
Channel troubleshooting
Start with:
openclaw doctor
openclaw channels status --probe
channels status --probe prints warnings when it can detect common channel misconfigurations, and includes small live checks (credentials, some permissions/membership).
Channels
- Discord: /channels/discord#troubleshooting
- Telegram: /channels/telegram#troubleshooting
- WhatsApp: /channels/whatsapp#troubleshooting-quick
Telegram quick fixes
- Logs show
HttpError: Network request for 'sendMessage' failedorsendChatAction→ check IPv6 DNS. Ifapi.telegram.orgresolves to IPv6 first and the host lacks IPv6 egress, force IPv4 or enable IPv6. See /channels/telegram#troubleshooting. - Logs show
setMyCommands failed→ check outbound HTTPS and DNS reachability toapi.telegram.org(common on locked-down VPS or proxies).
Source: https://docs.openclaw.ai/channels/whatsapp
WhatsApp (web channel)
Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
Quick setup (beginner)
- Use a separate phone number if possible (recommended).
- Configure WhatsApp in
~/.openclaw/openclaw.json. - Run
openclaw channels loginto scan the QR code (Linked Devices). - Start the gateway.
Minimal config:
{
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"],
},
},
}
Goals
- Multiple WhatsApp accounts (multi-account) in one Gateway process.
- Deterministic routing: replies return to WhatsApp, no model routing.
- Model sees enough context to understand quoted replies.
Config writes
By default, WhatsApp is allowed to write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { whatsapp: { configWrites: false } },
}
Architecture (who owns what)
- Gateway owns the Baileys socket and inbox loop.
- CLI / macOS app talk to the gateway; no direct Baileys use.
- Active listener is required for outbound sends; otherwise send fails fast.
Getting a phone number (two modes)
WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run OpenClaw on WhatsApp:
Dedicated number (recommended)
Use a separate phone number for OpenClaw. Best UX, clean routing, no self-chat quirks. Ideal setup: spare/old Android phone + eSIM. Leave it on Wi‑Fi and power, and link it via QR.
WhatsApp Business: You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the OpenClaw number there.
Sample config (dedicated number, single-user allowlist):
{
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"],
},
},
}
Pairing mode (optional):
If you want pairing instead of allowlist, set channels.whatsapp.dmPolicy to pairing. Unknown senders get a pairing code; approve with:
openclaw pairing approve whatsapp <code>
Personal number (fallback)
Quick fallback: run OpenClaw on your own number. Message yourself (WhatsApp “Message yourself”) for testing so you don’t spam contacts. Expect to read verification codes on your main phone during setup and experiments. Must enable self-chat mode. When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.
Sample config (personal number, self-chat):
{
"whatsapp": {
"selfChatMode": true,
"dmPolicy": "allowlist",
"allowFrom": ["+15551234567"]
}
}
Self-chat replies default to [{identity.name}] when set (otherwise [openclaw])
if messages.responsePrefix is unset. Set it explicitly to customize or disable
the prefix (use "" to remove it).
Number sourcing tips
- Local eSIM from your country's mobile carrier (most reliable)
- Prepaid SIM — cheap, just needs to receive one SMS for verification
Avoid: TextNow, Google Voice, most "free SMS" services — WhatsApp blocks these aggressively.
Tip: The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via creds.json.
Why Not Twilio?
- Early OpenClaw builds supported Twilio’s WhatsApp Business integration.
- WhatsApp Business numbers are a poor fit for a personal assistant.
- Meta enforces a 24‑hour reply window; if you haven’t responded in the last 24 hours, the business number can’t initiate new messages.
- High-volume or “chatty” usage triggers aggressive blocking, because business accounts aren’t meant to send dozens of personal assistant messages.
- Result: unreliable delivery and frequent blocks, so support was removed.
Login + credentials
- Login command:
openclaw channels login(QR via Linked Devices). - Multi-account login:
openclaw channels login --account <id>(<id>=accountId). - Default account (when
--accountis omitted):defaultif present, otherwise the first configured account id (sorted). - Credentials stored in
~/.openclaw/credentials/whatsapp/<accountId>/creds.json. - Backup copy at
creds.json.bak(restored on corruption). - Legacy compatibility: older installs stored Baileys files directly in
~/.openclaw/credentials/. - Logout:
openclaw channels logout(or--account <id>) deletes WhatsApp auth state (but keeps sharedoauth.json). - Logged-out socket => error instructs re-link.
Inbound flow (DM + group)
- WhatsApp events come from
messages.upsert(Baileys). - Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
- Status/broadcast chats are ignored.
- Direct chats use E.164; groups use group JID.
- DM policy:
channels.whatsapp.dmPolicycontrols direct chat access (default:pairing).- Pairing: unknown senders get a pairing code (approve via
openclaw pairing approve whatsapp <code>; codes expire after 1 hour). - Open: requires
channels.whatsapp.allowFromto include"*". - Your linked WhatsApp number is implicitly trusted, so self messages skip
channels.whatsapp.dmPolicyandchannels.whatsapp.allowFromchecks.
- Pairing: unknown senders get a pairing code (approve via
Personal-number mode (fallback)
If you run OpenClaw on your personal WhatsApp number, enable channels.whatsapp.selfChatMode (see sample above).
Behavior:
- Outbound DMs never trigger pairing replies (prevents spamming contacts).
- Inbound unknown senders still follow
channels.whatsapp.dmPolicy. - Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
- Read receipts sent for non-self-chat DMs.
Read receipts
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
Disable globally:
{
channels: { whatsapp: { sendReadReceipts: false } },
}
Disable per account:
{
channels: {
whatsapp: {
accounts: {
personal: { sendReadReceipts: false },
},
},
},
}
Notes:
- Self-chat mode always skips read receipts.
WhatsApp FAQ: sending messages + pairing
Will OpenClaw message random contacts when I link WhatsApp?
No. Default DM policy is pairing, so unknown senders only get a pairing code and their message is not processed. OpenClaw only replies to chats it receives, or to sends you explicitly trigger (agent/CLI).
How does pairing work on WhatsApp?
Pairing is a DM gate for unknown senders:
- First DM from a new sender returns a short code (message is not processed).
- Approve with:
openclaw pairing approve whatsapp <code>(list withopenclaw pairing list whatsapp). - Codes expire after 1 hour; pending requests are capped at 3 per channel.
Can multiple people use different OpenClaw instances on one WhatsApp number?
Yes, by routing each sender to a different agent via bindings (peer kind: "dm", sender E.164 like +15551234567). Replies still come from the same WhatsApp account, and direct chats collapse to each agent’s main session, so use one agent per person. DM access control (dmPolicy/allowFrom) is global per WhatsApp account. See Multi-Agent Routing.
Why do you ask for my phone number in the wizard?
The wizard uses it to set your allowlist/owner so your own DMs are permitted. It’s not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable channels.whatsapp.selfChatMode.
Message normalization (what the model sees)
Bodyis the current message body with envelope.- Quoted reply context is always appended:
[Replying to +1555 id:ABC123] <quoted text or <media:...>> [/Replying] - Reply metadata also set:
ReplyToId= stanzaIdReplyToBody= quoted body or media placeholderReplyToSender= E.164 when known
- Media-only inbound messages use placeholders:
<media:image|video|audio|document|sticker>
Groups
- Groups map to
agent:<agentId>:whatsapp:group:<jid>sessions. - Group policy:
channels.whatsapp.groupPolicy = open|disabled|allowlist(defaultallowlist). - Activation modes:
mention(default): requires @mention or regex match.always: always triggers.
/activation mention|alwaysis owner-only and must be sent as a standalone message.- Owner =
channels.whatsapp.allowFrom(or self E.164 if unset). - History injection (pending-only):
- Recent unprocessed messages (default 50) inserted under:
[Chat messages since your last reply - for context](messages already in the session are not re-injected) - Current message under:
[Current message - respond to this] - Sender suffix appended:
[from: Name (+E164)]
- Recent unprocessed messages (default 50) inserted under:
- Group metadata cached 5 min (subject + participants).
Reply delivery (threading)
- WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
- Reply tags are ignored on this channel.
Acknowledgment reactions (auto-react on receipt)
WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received.
Configuration:
{
"whatsapp": {
"ackReaction": {
"emoji": "👀",
"direct": true,
"group": "mentions"
}
}
}
Options:
emoji(string): Emoji to use for acknowledgment (e.g., "👀", "✅", "📨"). Empty or omitted = feature disabled.direct(boolean, default:true): Send reactions in direct/DM chats.group(string, default:"mentions"): Group chat behavior:"always": React to all group messages (even without @mention)"mentions": React only when bot is @mentioned"never": Never react in groups
Per-account override:
{
"whatsapp": {
"accounts": {
"work": {
"ackReaction": {
"emoji": "✅",
"direct": false,
"group": "always"
}
}
}
}
}
Behavior notes:
- Reactions are sent immediately upon message receipt, before typing indicators or bot replies.
- In groups with
requireMention: false(activation: always),group: "mentions"will react to all messages (not just @mentions). - Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
- Participant JID is automatically included for group reactions.
- WhatsApp ignores
messages.ackReaction; usechannels.whatsapp.ackReactioninstead.
Agent tool (reactions)
- Tool:
whatsappwithreactaction (chatJid,messageId,emoji, optionalremove). - Optional:
participant(group sender),fromMe(reacting to your own message),accountId(multi-account). - Reaction removal semantics: see /tools/reactions.
- Tool gating:
channels.whatsapp.actions.reactions(default: enabled).
Limits
- Outbound text is chunked to
channels.whatsapp.textChunkLimit(default 4000). - Optional newline chunking: set
channels.whatsapp.chunkMode="newline"to split on blank lines (paragraph boundaries) before length chunking. - Inbound media saves are capped by
channels.whatsapp.mediaMaxMb(default 50 MB). - Outbound media items are capped by
agents.defaults.mediaMaxMb(default 5 MB).
Outbound send (text + media)
- Uses active web listener; error if gateway not running.
- Text chunking: 4k max per message (configurable via
channels.whatsapp.textChunkLimit, optionalchannels.whatsapp.chunkMode). - Media:
- Image/video/audio/document supported.
- Audio sent as PTT;
audio/ogg=>audio/ogg; codecs=opus. - Caption only on first media item.
- Media fetch supports HTTP(S) and local paths.
- Animated GIFs: WhatsApp expects MP4 with
gifPlayback: truefor inline looping.- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
- CLI:
Voice notes (PTT audio)
WhatsApp sends audio as voice notes (PTT bubble).
- Best results: OGG/Opus. OpenClaw rewrites
audio/oggtoaudio/ogg; codecs=opus. [[audio_as_voice]]is ignored for WhatsApp (audio already ships as voice note).
Media limits + optimization
- Default outbound cap: 5 MB (per media item).
- Override:
agents.defaults.mediaMaxMb. - Images are auto-optimized to JPEG under cap (resize + quality sweep).
- Oversize media => error; media reply falls back to text warning.
Heartbeats
- Gateway heartbeat logs connection health (
web.heartbeatSeconds, default 60s). - Agent heartbeat can be configured per agent (
agents.list[].heartbeat) or globally viaagents.defaults.heartbeat(fallback when no per-agent entries are set).- Uses the configured heartbeat prompt (default:
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.) +HEARTBEAT_OKskip behavior. - Delivery defaults to the last used channel (or configured target).
- Uses the configured heartbeat prompt (default:
Reconnect behavior
- Backoff policy:
web.reconnect:initialMs,maxMs,factor,jitter,maxAttempts.
- If maxAttempts reached, web monitoring stops (degraded).
- Logged-out => stop and require re-link.
Config quick map
channels.whatsapp.dmPolicy(DM policy: pairing/allowlist/open/disabled).channels.whatsapp.selfChatMode(same-phone setup; bot uses your personal WhatsApp number).channels.whatsapp.allowFrom(DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).channels.whatsapp.mediaMaxMb(inbound media save cap).channels.whatsapp.ackReaction(auto-reaction on message receipt:{emoji, direct, group}).channels.whatsapp.accounts.<accountId>.*(per-account settings + optionalauthDir).channels.whatsapp.accounts.<accountId>.mediaMaxMb(per-account inbound media cap).channels.whatsapp.accounts.<accountId>.ackReaction(per-account ack reaction override).channels.whatsapp.groupAllowFrom(group sender allowlist).channels.whatsapp.groupPolicy(group policy).channels.whatsapp.historyLimit/channels.whatsapp.accounts.<accountId>.historyLimit(group history context;0disables).channels.whatsapp.dmHistoryLimit(DM history limit in user turns). Per-user overrides:channels.whatsapp.dms["<phone>"].historyLimit.channels.whatsapp.groups(group allowlist + mention gating defaults; use"*"to allow all)channels.whatsapp.actions.reactions(gate WhatsApp tool reactions).agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns)messages.groupChat.historyLimitchannels.whatsapp.messagePrefix(inbound prefix; per-account:channels.whatsapp.accounts.<accountId>.messagePrefix; deprecated:messages.messagePrefix)messages.responsePrefix(outbound prefix)agents.defaults.mediaMaxMbagents.defaults.heartbeat.everyagents.defaults.heartbeat.model(optional override)agents.defaults.heartbeat.targetagents.defaults.heartbeat.toagents.defaults.heartbeat.sessionagents.list[].heartbeat.*(per-agent overrides)session.*(scope, idle, store, mainKey)web.enabled(disable channel startup when false)web.heartbeatSecondsweb.reconnect.*
Logs + troubleshooting
- Subsystems:
whatsapp/inbound,whatsapp/outbound,web-heartbeat,web-reconnect. - Log file:
/tmp/openclaw/openclaw-YYYY-MM-DD.log(configurable). - Troubleshooting guide: Gateway troubleshooting.
Troubleshooting (quick)
Not linked / QR login required
- Symptom:
channels statusshowslinked: falseor warns “Not linked”. - Fix: run
openclaw channels loginon the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
Linked but disconnected / reconnect loop
- Symptom:
channels statusshowsrunning, disconnectedor warns “Linked but disconnected”. - Fix:
openclaw doctor(or restart the gateway). If it persists, relink viachannels loginand inspectopenclaw logs --follow.
Bun runtime
- Bun is not recommended. WhatsApp (Baileys) and Telegram are unreliable on Bun. Run the gateway with Node. (See Getting Started runtime note.)
Zalo
Source: https://docs.openclaw.ai/channels/zalo
Zalo (Bot API)
Status: experimental. Direct messages only; groups coming soon per Zalo docs.
Plugin required
Zalo ships as a plugin and is not bundled with the core install.
- Install via CLI:
openclaw plugins install @openclaw/zalo - Or select Zalo during onboarding and confirm the install prompt
- Details: Plugins
Quick setup (beginner)
- Install the Zalo plugin:
- From a source checkout:
openclaw plugins install ./extensions/zalo - From npm (if published):
openclaw plugins install @openclaw/zalo - Or pick Zalo in onboarding and confirm the install prompt
- From a source checkout:
- Set the token:
- Env:
ZALO_BOT_TOKEN=... - Or config:
channels.zalo.botToken: "...".
- Env:
- Restart the gateway (or finish onboarding).
- DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
{
channels: {
zalo: {
enabled: true,
botToken: "12345689:abc-xyz",
dmPolicy: "pairing",
},
},
}
What it is
Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations. It is a good fit for support or notifications where you want deterministic routing back to Zalo.
- A Zalo Bot API channel owned by the Gateway.
- Deterministic routing: replies go back to Zalo; the model never chooses channels.
- DMs share the agent's main session.
- Groups are not yet supported (Zalo docs state "coming soon").
Setup (fast path)
1) Create a bot token (Zalo Bot Platform)
- Go to https://bot.zaloplatforms.com and sign in.
- Create a new bot and configure its settings.
- Copy the bot token (format:
12345689:abc-xyz).
2) Configure the token (env or config)
Example:
{
channels: {
zalo: {
enabled: true,
botToken: "12345689:abc-xyz",
dmPolicy: "pairing",
},
},
}
Env option: ZALO_BOT_TOKEN=... (works for the default account only).
Multi-account support: use channels.zalo.accounts with per-account tokens and optional name.
- Restart the gateway. Zalo starts when a token is resolved (env or config).
- DM access defaults to pairing. Approve the code when the bot is first contacted.
How it works (behavior)
- Inbound messages are normalized into the shared channel envelope with media placeholders.
- Replies always route back to the same Zalo chat.
- Long-polling by default; webhook mode available with
channels.zalo.webhookUrl.
Limits
- Outbound text is chunked to 2000 characters (Zalo API limit).
- Media downloads/uploads are capped by
channels.zalo.mediaMaxMb(default 5). - Streaming is blocked by default due to the 2000 char limit making streaming less useful.
Access control (DMs)
DM access
- Default:
channels.zalo.dmPolicy = "pairing". Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). - Approve via:
openclaw pairing list zaloopenclaw pairing approve zalo <CODE>
- Pairing is the default token exchange. Details: Pairing
channels.zalo.allowFromaccepts numeric user IDs (no username lookup available).
Long-polling vs webhook
- Default: long-polling (no public URL required).
- Webhook mode: set
channels.zalo.webhookUrlandchannels.zalo.webhookSecret.- The webhook secret must be 8-256 characters.
- Webhook URL must use HTTPS.
- Zalo sends events with
X-Bot-Api-Secret-Tokenheader for verification. - Gateway HTTP handles webhook requests at
channels.zalo.webhookPath(defaults to the webhook URL path).
Note: getUpdates (polling) and webhook are mutually exclusive per Zalo API docs.
Supported message types
- Text messages: Full support with 2000 character chunking.
- Image messages: Download and process inbound images; send images via
sendPhoto. - Stickers: Logged but not fully processed (no agent response).
- Unsupported types: Logged (e.g., messages from protected users).
Capabilities
| Feature | Status |
|---|---|
| Direct messages | ✅ Supported |
| Groups | ❌ Coming soon (per Zalo docs) |
| Media (images) | ✅ Supported |
| Reactions | ❌ Not supported |
| Threads | ❌ Not supported |
| Polls | ❌ Not supported |
| Native commands | ❌ Not supported |
| Streaming | ⚠️ Blocked (2000 char limit) |
Delivery targets (CLI/cron)
- Use a chat id as the target.
- Example:
openclaw message send --channel zalo --target 123456789 --message "hi".
Troubleshooting
Bot doesn't respond:
- Check that the token is valid:
openclaw channels status --probe - Verify the sender is approved (pairing or allowFrom)
- Check gateway logs:
openclaw logs --follow
Webhook not receiving events:
- Ensure webhook URL uses HTTPS
- Verify secret token is 8-256 characters
- Confirm the gateway HTTP endpoint is reachable on the configured path
- Check that getUpdates polling is not running (they're mutually exclusive)
Configuration reference (Zalo)
Full configuration: Configuration
Provider options:
channels.zalo.enabled: enable/disable channel startup.channels.zalo.botToken: bot token from Zalo Bot Platform.channels.zalo.tokenFile: read token from file path.channels.zalo.dmPolicy:pairing | allowlist | open | disabled(default: pairing).channels.zalo.allowFrom: DM allowlist (user IDs).openrequires"*". The wizard will ask for numeric IDs.channels.zalo.mediaMaxMb: inbound/outbound media cap (MB, default 5).channels.zalo.webhookUrl: enable webhook mode (HTTPS required).channels.zalo.webhookSecret: webhook secret (8-256 chars).channels.zalo.webhookPath: webhook path on the gateway HTTP server.channels.zalo.proxy: proxy URL for API requests.
Multi-account options:
channels.zalo.accounts.<id>.botToken: per-account token.channels.zalo.accounts.<id>.tokenFile: per-account token file.channels.zalo.accounts.<id>.name: display name.channels.zalo.accounts.<id>.enabled: enable/disable account.channels.zalo.accounts.<id>.dmPolicy: per-account DM policy.channels.zalo.accounts.<id>.allowFrom: per-account allowlist.channels.zalo.accounts.<id>.webhookUrl: per-account webhook URL.channels.zalo.accounts.<id>.webhookSecret: per-account webhook secret.channels.zalo.accounts.<id>.webhookPath: per-account webhook path.channels.zalo.accounts.<id>.proxy: per-account proxy URL.
Zalo Personal
Source: https://docs.openclaw.ai/channels/zalouser
Zalo Personal (unofficial)
Status: experimental. This integration automates a personal Zalo account via zca-cli.
Warning: This is an unofficial integration and may result in account suspension/ban. Use at your own risk.
Plugin required
Zalo Personal ships as a plugin and is not bundled with the core install.
- Install via CLI:
openclaw plugins install @openclaw/zalouser - Or from a source checkout:
openclaw plugins install ./extensions/zalouser - Details: Plugins
Prerequisite: zca-cli
The Gateway machine must have the zca binary available in PATH.
- Verify:
zca --version - If missing, install zca-cli (see
extensions/zalouser/README.mdor the upstream zca-cli docs).
Quick setup (beginner)
- Install the plugin (see above).
- Login (QR, on the Gateway machine):
openclaw channels login --channel zalouser- Scan the QR code in the terminal with the Zalo mobile app.
- Enable the channel:
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing",
},
},
}
- Restart the Gateway (or finish onboarding).
- DM access defaults to pairing; approve the pairing code on first contact.
What it is
- Uses
zca listento receive inbound messages. - Uses
zca msg ...to send replies (text/media/link). - Designed for “personal account” use cases where Zalo Bot API is not available.
Naming
Channel id is zalouser to make it explicit this automates a personal Zalo user account (unofficial). We keep zalo reserved for a potential future official Zalo API integration.
Finding IDs (directory)
Use the directory CLI to discover peers/groups and their IDs:
openclaw directory self --channel zalouser
openclaw directory peers list --channel zalouser --query "name"
openclaw directory groups list --channel zalouser --query "work"
Limits
- Outbound text is chunked to ~2000 characters (Zalo client limits).
- Streaming is blocked by default.
Access control (DMs)
channels.zalouser.dmPolicy supports: pairing | allowlist | open | disabled (default: pairing).
channels.zalouser.allowFrom accepts user IDs or names. The wizard resolves names to IDs via zca friend find when available.
Approve via:
openclaw pairing list zalouseropenclaw pairing approve zalouser <code>
Group access (optional)
- Default:
channels.zalouser.groupPolicy = "open"(groups allowed). Usechannels.defaults.groupPolicyto override the default when unset. - Restrict to an allowlist with:
channels.zalouser.groupPolicy = "allowlist"channels.zalouser.groups(keys are group IDs or names)
- Block all groups:
channels.zalouser.groupPolicy = "disabled". - The configure wizard can prompt for group allowlists.
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
Example:
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groups: {
"123456789": { allow: true },
"Work Chat": { allow: true },
},
},
},
}
Multi-account
Accounts map to zca profiles. Example:
{
channels: {
zalouser: {
enabled: true,
defaultAccount: "default",
accounts: {
work: { enabled: true, profile: "work" },
},
},
},
}
Troubleshooting
zca not found:
- Install zca-cli and ensure it’s on
PATHfor the Gateway process.
Login doesn’t stick:
openclaw channels status --probe- Re-login:
openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser
Agent Runtime
Source: https://docs.openclaw.ai/concepts/agent
Agent Runtime 🤖
OpenClaw runs a single embedded agent runtime derived from pi-mono.
Workspace (required)
OpenClaw uses a single agent workspace directory (agents.defaults.workspace) as the agent’s only working directory (cwd) for tools and context.
Recommended: use openclaw setup to create ~/.openclaw/openclaw.json if missing and initialize the workspace files.
Full workspace layout + backup guide: Agent workspace
If agents.defaults.sandbox is enabled, non-main sessions can override this with
per-session workspaces under agents.defaults.sandbox.workspaceRoot (see
Gateway configuration).
Bootstrap files (injected)
Inside agents.defaults.workspace, OpenClaw expects these user-editable files:
AGENTS.md— operating instructions + “memory”SOUL.md— persona, boundaries, toneTOOLS.md— user-maintained tool notes (e.g.imsg,sag, conventions)BOOTSTRAP.md— one-time first-run ritual (deleted after completion)IDENTITY.md— agent name/vibe/emojiUSER.md— user profile + preferred address
On the first turn of a new session, OpenClaw injects the contents of these files directly into the agent context.
Blank files are skipped. Large files are trimmed and truncated with a marker so prompts stay lean (read the file for full content).
If a file is missing, OpenClaw injects a single “missing file” marker line (and openclaw setup will create a safe default template).
BOOTSTRAP.md is only created for a brand new workspace (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts.
To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
{ agent: { skipBootstrap: true } }
Built-in tools
Core tools (read/exec/edit/write and related system tools) are always available,
subject to tool policy. apply_patch is optional and gated by
tools.exec.applyPatch. TOOLS.md does not control which tools exist; it’s
guidance for how you want them used.
Skills
OpenClaw loads skills from three locations (workspace wins on name conflict):
- Bundled (shipped with the install)
- Managed/local:
~/.openclaw/skills - Workspace:
<workspace>/skills
Skills can be gated by config/env (see skills in Gateway configuration).
pi-mono integration
OpenClaw reuses pieces of the pi-mono codebase (models/tools), but session management, discovery, and tool wiring are OpenClaw-owned.
- No pi-coding agent runtime.
- No
~/.pi/agentor<workspace>/.pisettings are consulted.
Sessions
Session transcripts are stored as JSONL at:
~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl
The session ID is stable and chosen by OpenClaw. Legacy Pi/Tau session folders are not read.
Steering while streaming
When queue mode is steer, inbound messages are injected into the current run.
The queue is checked after each tool call; if a queued message is present,
remaining tool calls from the current assistant message are skipped (error tool
results with "Skipped due to queued user message."), then the queued user
message is injected before the next assistant response.
When queue mode is followup or collect, inbound messages are held until the
current turn ends, then a new agent turn starts with the queued payloads. See
Queue for mode + debounce/cap behavior.
Block streaming sends completed assistant blocks as soon as they finish; it is
off by default (agents.defaults.blockStreamingDefault: "off").
Tune the boundary via agents.defaults.blockStreamingBreak (text_end vs message_end; defaults to text_end).
Control soft block chunking with agents.defaults.blockStreamingChunk (defaults to
800–1200 chars; prefers paragraph breaks, then newlines; sentences last).
Coalesce streamed chunks with agents.defaults.blockStreamingCoalesce to reduce
single-line spam (idle-based merging before send). Non-Telegram channels require
explicit *.blockStreaming: true to enable block replies.
Verbose tool summaries are emitted at tool start (no debounce); Control UI
streams tool output via agent events when available.
More details: Streaming + chunking.
Model refs
Model refs in config (for example agents.defaults.model and agents.defaults.models) are parsed by splitting on the first /.
- Use
provider/modelwhen configuring models. - If the model ID itself contains
/(OpenRouter-style), include the provider prefix (example:openrouter/moonshotai/kimi-k2). - If you omit the provider, OpenClaw treats the input as an alias or a model for the default provider (only works when there is no
/in the model ID).
Configuration (minimal)
At minimum, set:
agents.defaults.workspacechannels.whatsapp.allowFrom(strongly recommended)
Next: Group Chats 🦞
Agent Loop
Source: https://docs.openclaw.ai/concepts/agent-loop
Agent Loop (OpenClaw)
An agentic loop is the full “real” run of an agent: intake → context assembly → model inference → tool execution → streaming replies → persistence. It’s the authoritative path that turns a message into actions and a final reply, while keeping session state consistent.
In OpenClaw, a loop is a single, serialized run per session that emits lifecycle and stream events as the model thinks, calls tools, and streams output. This doc explains how that authentic loop is wired end-to-end.
Entry points
- Gateway RPC:
agentandagent.wait. - CLI:
agentcommand.
How it works (high-level)
agentRPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns{ runId, acceptedAt }immediately.agentCommandruns the agent:- resolves model + thinking/verbose defaults
- loads skills snapshot
- calls
runEmbeddedPiAgent(pi-agent-core runtime) - emits lifecycle end/error if the embedded loop does not emit one
runEmbeddedPiAgent:- serializes runs via per-session + global queues
- resolves model + auth profile and builds the pi session
- subscribes to pi events and streams assistant/tool deltas
- enforces timeout -> aborts run if exceeded
- returns payloads + usage metadata
subscribeEmbeddedPiSessionbridges pi-agent-core events to OpenClawagentstream:- tool events =>
stream: "tool" - assistant deltas =>
stream: "assistant" - lifecycle events =>
stream: "lifecycle"(phase: "start" | "end" | "error")
- tool events =>
agent.waituseswaitForAgentJob:- waits for lifecycle end/error for
runId - returns
{ status: ok|error|timeout, startedAt, endedAt, error? }
- waits for lifecycle end/error for
Queueing + concurrency
- Runs are serialized per session key (session lane) and optionally through a global lane.
- This prevents tool/session races and keeps session history consistent.
- Messaging channels can choose queue modes (collect/steer/followup) that feed this lane system. See Command Queue.
Session + workspace preparation
- Workspace is resolved and created; sandboxed runs may redirect to a sandbox workspace root.
- Skills are loaded (or reused from a snapshot) and injected into env and prompt.
- Bootstrap/context files are resolved and injected into the system prompt report.
- A session write lock is acquired;
SessionManageris opened and prepared before streaming.
Prompt assembly + system prompt
- System prompt is built from OpenClaw’s base prompt, skills prompt, bootstrap context, and per-run overrides.
- Model-specific limits and compaction reserve tokens are enforced.
- See System prompt for what the model sees.
Hook points (where you can intercept)
OpenClaw has two hook systems:
- Internal hooks (Gateway hooks): event-driven scripts for commands and lifecycle events.
- Plugin hooks: extension points inside the agent/tool lifecycle and gateway pipeline.
Internal hooks (Gateway hooks)
agent:bootstrap: runs while building bootstrap files before the system prompt is finalized. Use this to add/remove bootstrap context files.- Command hooks:
/new,/reset,/stop, and other command events (see Hooks doc).
See Hooks for setup and examples.
Plugin hooks (agent + gateway lifecycle)
These run inside the agent loop or gateway pipeline:
before_agent_start: inject context or override system prompt before the run starts.agent_end: inspect the final message list and run metadata after completion.before_compaction/after_compaction: observe or annotate compaction cycles.before_tool_call/after_tool_call: intercept tool params/results.tool_result_persist: synchronously transform tool results before they are written to the session transcript.message_received/message_sending/message_sent: inbound + outbound message hooks.session_start/session_end: session lifecycle boundaries.gateway_start/gateway_stop: gateway lifecycle events.
See Plugins for the hook API and registration details.
Streaming + partial replies
- Assistant deltas are streamed from pi-agent-core and emitted as
assistantevents. - Block streaming can emit partial replies either on
text_endormessage_end. - Reasoning streaming can be emitted as a separate stream or as block replies.
- See Streaming for chunking and block reply behavior.
Tool execution + messaging tools
- Tool start/update/end events are emitted on the
toolstream. - Tool results are sanitized for size and image payloads before logging/emitting.
- Messaging tool sends are tracked to suppress duplicate assistant confirmations.
Reply shaping + suppression
- Final payloads are assembled from:
- assistant text (and optional reasoning)
- inline tool summaries (when verbose + allowed)
- assistant error text when the model errors
NO_REPLYis treated as a silent token and filtered from outgoing payloads.- Messaging tool duplicates are removed from the final payload list.
- If no renderable payloads remain and a tool errored, a fallback tool error reply is emitted (unless a messaging tool already sent a user-visible reply).
Compaction + retries
- Auto-compaction emits
compactionstream events and can trigger a retry. - On retry, in-memory buffers and tool summaries are reset to avoid duplicate output.
- See Compaction for the compaction pipeline.
Event streams (today)
lifecycle: emitted bysubscribeEmbeddedPiSession(and as a fallback byagentCommand)assistant: streamed deltas from pi-agent-coretool: streamed tool events from pi-agent-core
Chat channel handling
- Assistant deltas are buffered into chat
deltamessages. - A chat
finalis emitted on lifecycle end/error.
Timeouts
agent.waitdefault: 30s (just the wait).timeoutMsparam overrides.- Agent runtime:
agents.defaults.timeoutSecondsdefault 600s; enforced inrunEmbeddedPiAgentabort timer.
Where things can end early
- Agent timeout (abort)
- AbortSignal (cancel)
- Gateway disconnect or RPC timeout
agent.waittimeout (wait-only, does not stop agent)
Agent Workspace
Source: https://docs.openclaw.ai/concepts/agent-workspace
Agent workspace
The workspace is the agent's home. It is the only working directory used for file tools and for workspace context. Keep it private and treat it as memory.
This is separate from ~/.openclaw/, which stores config, credentials, and
sessions.
Important: the workspace is the default cwd, not a hard sandbox. Tools
resolve relative paths against the workspace, but absolute paths can still reach
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
agents.defaults.sandbox (and/or per‑agent sandbox config).
When sandboxing is enabled and workspaceAccess is not "rw", tools operate
inside a sandbox workspace under ~/.openclaw/sandboxes, not your host workspace.
Default location
- Default:
~/.openclaw/workspace - If
OPENCLAW_PROFILEis set and not"default", the default becomes~/.openclaw/workspace-<profile>. - Override in
~/.openclaw/openclaw.json:
{
agent: {
workspace: "~/.openclaw/workspace",
},
}
openclaw onboard, openclaw configure, or openclaw setup will create the
workspace and seed the bootstrap files if they are missing.
If you already manage the workspace files yourself, you can disable bootstrap file creation:
{ agent: { skipBootstrap: true } }
Extra workspace folders
Older installs may have created ~/openclaw. Keeping multiple workspace
directories around can cause confusing auth or state drift, because only one
workspace is active at a time.
Recommendation: keep a single active workspace. If you no longer use the
extra folders, archive or move them to Trash (for example trash ~/openclaw).
If you intentionally keep multiple workspaces, make sure
agents.defaults.workspace points to the active one.
openclaw doctor warns when it detects extra workspace directories.
Workspace file map (what each file means)
These are the standard files OpenClaw expects inside the workspace:
-
AGENTS.md- Operating instructions for the agent and how it should use memory.
- Loaded at the start of every session.
- Good place for rules, priorities, and "how to behave" details.
-
SOUL.md- Persona, tone, and boundaries.
- Loaded every session.
-
USER.md- Who the user is and how to address them.
- Loaded every session.
-
IDENTITY.md- The agent's name, vibe, and emoji.
- Created/updated during the bootstrap ritual.
-
TOOLS.md- Notes about your local tools and conventions.
- Does not control tool availability; it is only guidance.
-
HEARTBEAT.md- Optional tiny checklist for heartbeat runs.
- Keep it short to avoid token burn.
-
BOOT.md- Optional startup checklist executed on gateway restart when internal hooks are enabled.
- Keep it short; use the message tool for outbound sends.
-
BOOTSTRAP.md- One-time first-run ritual.
- Only created for a brand-new workspace.
- Delete it after the ritual is complete.
-
memory/YYYY-MM-DD.md- Daily memory log (one file per day).
- Recommended to read today + yesterday on session start.
-
MEMORY.md(optional)- Curated long-term memory.
- Only load in the main, private session (not shared/group contexts).
See Memory for the workflow and automatic memory flush.
-
skills/(optional)- Workspace-specific skills.
- Overrides managed/bundled skills when names collide.
-
canvas/(optional)- Canvas UI files for node displays (for example
canvas/index.html).
- Canvas UI files for node displays (for example
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into
the session and continues. Large bootstrap files are truncated when injected;
adjust the limit with agents.defaults.bootstrapMaxChars (default: 20000).
openclaw setup can recreate missing defaults without overwriting existing
files.
What is NOT in the workspace
These live under ~/.openclaw/ and should NOT be committed to the workspace repo:
~/.openclaw/openclaw.json(config)~/.openclaw/credentials/(OAuth tokens, API keys)~/.openclaw/agents/<agentId>/sessions/(session transcripts + metadata)~/.openclaw/skills/(managed skills)
If you need to migrate sessions or config, copy them separately and keep them out of version control.
Git backup (recommended, private)
Treat the workspace as private memory. Put it in a private git repo so it is backed up and recoverable.
Run these steps on the machine where the Gateway runs (that is where the workspace lives).
1) Initialize the repo
If git is installed, brand-new workspaces are initialized automatically. If this workspace is not already a repo, run:
cd ~/.openclaw/workspace
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git commit -m "Add agent workspace"
2) Add a private remote (beginner-friendly options)
Option A: GitHub web UI
- Create a new private repository on GitHub.
- Do not initialize with a README (avoids merge conflicts).
- Copy the HTTPS remote URL.
- Add the remote and push:
git branch -M main
git remote add origin <https-url>
git push -u origin main
Option B: GitHub CLI (gh)
gh auth login
gh repo create openclaw-workspace --private --source . --remote origin --push
Option C: GitLab web UI
- Create a new private repository on GitLab.
- Do not initialize with a README (avoids merge conflicts).
- Copy the HTTPS remote URL.
- Add the remote and push:
git branch -M main
git remote add origin <https-url>
git push -u origin main
3) Ongoing updates
git status
git add .
git commit -m "Update memory"
git push
Do not commit secrets
Even in a private repo, avoid storing secrets in the workspace:
- API keys, OAuth tokens, passwords, or private credentials.
- Anything under
~/.openclaw/. - Raw dumps of chats or sensitive attachments.
If you must store sensitive references, use placeholders and keep the real
secret elsewhere (password manager, environment variables, or ~/.openclaw/).
Suggested .gitignore starter:
.DS_Store
.env
**/*.key
**/*.pem
**/secrets*
Moving the workspace to a new machine
- Clone the repo to the desired path (default
~/.openclaw/workspace). - Set
agents.defaults.workspaceto that path in~/.openclaw/openclaw.json. - Run
openclaw setup --workspace <path>to seed any missing files. - If you need sessions, copy
~/.openclaw/agents/<agentId>/sessions/from the old machine separately.
Advanced notes
- Multi-agent routing can use different workspaces per agent. See Channel routing for routing configuration.
- If
agents.defaults.sandboxis enabled, non-main sessions can use per-session sandbox workspaces underagents.defaults.sandbox.workspaceRoot.
Gateway Architecture
Source: https://docs.openclaw.ai/concepts/architecture
Gateway architecture
Last updated: 2026-01-22
Overview
- A single long‑lived Gateway owns all messaging surfaces (WhatsApp via Baileys, Telegram via grammY, Slack, Discord, Signal, iMessage, WebChat).
- Control-plane clients (macOS app, CLI, web UI, automations) connect to the
Gateway over WebSocket on the configured bind host (default
127.0.0.1:18789). - Nodes (macOS/iOS/Android/headless) also connect over WebSocket, but
declare
role: nodewith explicit caps/commands. - One Gateway per host; it is the only place that opens a WhatsApp session.
- A canvas host (default
18793) serves agent‑editable HTML and A2UI.
Components and flows
Gateway (daemon)
- Maintains provider connections.
- Exposes a typed WS API (requests, responses, server‑push events).
- Validates inbound frames against JSON Schema.
- Emits events like
agent,chat,presence,health,heartbeat,cron.
Clients (mac app / CLI / web admin)
- One WS connection per client.
- Send requests (
health,status,send,agent,system-presence). - Subscribe to events (
tick,agent,presence,shutdown).
Nodes (macOS / iOS / Android / headless)
- Connect to the same WS server with
role: node. - Provide a device identity in
connect; pairing is device‑based (rolenode) and approval lives in the device pairing store. - Expose commands like
canvas.*,camera.*,screen.record,location.get.
Protocol details:
WebChat
- Static UI that uses the Gateway WS API for chat history and sends.
- In remote setups, connects through the same SSH/Tailscale tunnel as other clients.
Connection lifecycle (single client)
Client Gateway
| |
|---- req:connect -------->|
|<------ res (ok) ---------| (or res error + close)
| (payload=hello-ok carries snapshot: presence + health)
| |
|<------ event:presence ---|
|<------ event:tick -------|
| |
|------- req:agent ------->|
|<------ res:agent --------| (ack: {runId,status:"accepted"})
|<------ event:agent ------| (streaming)
|<------ res:agent --------| (final: {runId,status,summary})
| |
Wire protocol (summary)
- Transport: WebSocket, text frames with JSON payloads.
- First frame must be
connect. - After handshake:
- Requests:
{type:"req", id, method, params}→{type:"res", id, ok, payload|error} - Events:
{type:"event", event, payload, seq?, stateVersion?}
- Requests:
- If
OPENCLAW_GATEWAY_TOKEN(or--token) is set,connect.params.auth.tokenmust match or the socket closes. - Idempotency keys are required for side‑effecting methods (
send,agent) to safely retry; the server keeps a short‑lived dedupe cache. - Nodes must include
role: "node"plus caps/commands/permissions inconnect.
Pairing + local trust
- All WS clients (operators + nodes) include a device identity on
connect. - New device IDs require pairing approval; the Gateway issues a device token for subsequent connects.
- Local connects (loopback or the gateway host’s own tailnet address) can be auto‑approved to keep same‑host UX smooth.
- Non‑local connects must sign the
connect.challengenonce and require explicit approval. - Gateway auth (
gateway.auth.*) still applies to all connections, local or remote.
Details: Gateway protocol, Pairing, Security.
Protocol typing and codegen
- TypeBox schemas define the protocol.
- JSON Schema is generated from those schemas.
- Swift models are generated from the JSON Schema.
Remote access
- Preferred: Tailscale or VPN.
- Alternative: SSH tunnel
ssh -N -L 18789:127.0.0.1:18789 user@host - The same handshake + auth token apply over the tunnel.
- TLS + optional pinning can be enabled for WS in remote setups.
Operations snapshot
- Start:
openclaw gateway(foreground, logs to stdout). - Health:
healthover WS (also included inhello-ok). - Supervision: launchd/systemd for auto‑restart.
Invariants
- Exactly one Gateway controls a single Baileys session per host.
- Handshake is mandatory; any non‑JSON or non‑connect first frame is a hard close.
- Events are not replayed; clients must refresh on gaps.
Channel Routing
Source: https://docs.openclaw.ai/concepts/channel-routing
Channels & routing
OpenClaw routes replies back to the channel where a message came from. The model does not choose a channel; routing is deterministic and controlled by the host configuration.
Key terms
- Channel:
whatsapp,telegram,discord,slack,signal,imessage,webchat. - AccountId: per‑channel account instance (when supported).
- AgentId: an isolated workspace + session store (“brain”).
- SessionKey: the bucket key used to store context and control concurrency.
Session key shapes (examples)
Direct messages collapse to the agent’s main session:
agent:<agentId>:<mainKey>(default:agent:main:main)
Groups and channels remain isolated per channel:
- Groups:
agent:<agentId>:<channel>:group:<id> - Channels/rooms:
agent:<agentId>:<channel>:channel:<id>
Threads:
- Slack/Discord threads append
:thread:<threadId>to the base key. - Telegram forum topics embed
:topic:<topicId>in the group key.
Examples:
agent:main:telegram:group:-1001234567890:topic:42agent:main:discord:channel:123456:thread:987654
Routing rules (how an agent is chosen)
Routing picks one agent for each inbound message:
- Exact peer match (
bindingswithpeer.kind+peer.id). - Guild match (Discord) via
guildId. - Team match (Slack) via
teamId. - Account match (
accountIdon the channel). - Channel match (any account on that channel).
- Default agent (
agents.list[].default, else first list entry, fallback tomain).
The matched agent determines which workspace and session store are used.
Broadcast groups (run multiple agents)
Broadcast groups let you run multiple agents for the same peer when OpenClaw would normally reply (for example: in WhatsApp groups, after mention/activation gating).
Config:
{
broadcast: {
strategy: "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"],
"+15555550123": ["support", "logger"],
},
}
See: Broadcast Groups.
Config overview
agents.list: named agent definitions (workspace, model, etc.).bindings: map inbound channels/accounts/peers to agents.
Example:
{
agents: {
list: [{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }],
},
bindings: [
{ match: { channel: "slack", teamId: "T123" }, agentId: "support" },
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" },
],
}
Session storage
Session stores live under the state directory (default ~/.openclaw):
~/.openclaw/agents/<agentId>/sessions/sessions.json- JSONL transcripts live alongside the store
You can override the store path via session.store and {agentId} templating.
WebChat behavior
WebChat attaches to the selected agent and defaults to the agent’s main session. Because of this, WebChat lets you see cross‑channel context for that agent in one place.
Reply context
Inbound replies include:
ReplyToId,ReplyToBody, andReplyToSenderwhen available.- Quoted context is appended to
Bodyas a[Replying to ...]block.
This is consistent across channels.
Compaction
Source: https://docs.openclaw.ai/concepts/compaction
Context Window & Compaction
Every model has a context window (max tokens it can see). Long-running chats accumulate messages and tool results; once the window is tight, OpenClaw compacts older history to stay within limits.
What compaction is
Compaction summarizes older conversation into a compact summary entry and keeps recent messages intact. The summary is stored in the session history, so future requests use:
- The compaction summary
- Recent messages after the compaction point
Compaction persists in the session’s JSONL history.
Configuration
See Compaction config & modes for the agents.defaults.compaction settings.
Auto-compaction (default on)
When a session nears or exceeds the model’s context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.
You’ll see:
🧹 Auto-compaction completein verbose mode/statusshowing🧹 Compactions: <count>
Before compaction, OpenClaw can run a silent memory flush turn to store durable notes to disk. See Memory for details and config.
Manual compaction
Use /compact (optionally with instructions) to force a compaction pass:
/compact Focus on decisions and open questions
Context window source
Context window is model-specific. OpenClaw uses the model definition from the configured provider catalog to determine limits.
Compaction vs pruning
- Compaction: summarises and persists in JSONL.
- Session pruning: trims old tool results only, in-memory, per request.
See /concepts/session-pruning for pruning details.
Tips
- Use
/compactwhen sessions feel stale or context is bloated. - Large tool outputs are already truncated; pruning can further reduce tool-result buildup.
- If you need a fresh slate,
/newor/resetstarts a new session id.
Context
Source: https://docs.openclaw.ai/concepts/context
Context
“Context” is everything OpenClaw sends to the model for a run. It is bounded by the model’s context window (token limit).
Beginner mental model:
- System prompt (OpenClaw-built): rules, tools, skills list, time/runtime, and injected workspace files.
- Conversation history: your messages + the assistant’s messages for this session.
- Tool calls/results + attachments: command output, file reads, images/audio, etc.
Context is not the same thing as “memory”: memory can be stored on disk and reloaded later; context is what’s inside the model’s current window.
Quick start (inspect context)
/status→ quick “how full is my window?” view + session settings./context list→ what’s injected + rough sizes (per file + totals)./context detail→ deeper breakdown: per-file, per-tool schema sizes, per-skill entry sizes, and system prompt size./usage tokens→ append per-reply usage footer to normal replies./compact→ summarize older history into a compact entry to free window space.
See also: Slash commands, Token use & costs, Compaction.
Example output
Values vary by model, provider, tool policy, and what’s in your workspace.
/context list
🧠 Context breakdown
Workspace: <workspaceDir>
Bootstrap max/file: 20,000 chars
Sandbox: mode=non-main sandboxed=false
System prompt (run): 38,412 chars (~9,603 tok) (Project Context 23,901 chars (~5,976 tok))
Injected workspace files:
- AGENTS.md: OK | raw 1,742 chars (~436 tok) | injected 1,742 chars (~436 tok)
- SOUL.md: OK | raw 912 chars (~228 tok) | injected 912 chars (~228 tok)
- TOOLS.md: TRUNCATED | raw 54,210 chars (~13,553 tok) | injected 20,962 chars (~5,241 tok)
- IDENTITY.md: OK | raw 211 chars (~53 tok) | injected 211 chars (~53 tok)
- USER.md: OK | raw 388 chars (~97 tok) | injected 388 chars (~97 tok)
- HEARTBEAT.md: MISSING | raw 0 | injected 0
- BOOTSTRAP.md: OK | raw 0 chars (~0 tok) | injected 0 chars (~0 tok)
Skills list (system prompt text): 2,184 chars (~546 tok) (12 skills)
Tools: read, edit, write, exec, process, browser, message, sessions_send, …
Tool list (system prompt text): 1,032 chars (~258 tok)
Tool schemas (JSON): 31,988 chars (~7,997 tok) (counts toward context; not shown as text)
Tools: (same as above)
Session tokens (cached): 14,250 total / ctx=32,000
/context detail
🧠 Context breakdown (detailed)
…
Top skills (prompt entry size):
- frontend-design: 412 chars (~103 tok)
- oracle: 401 chars (~101 tok)
… (+10 more skills)
Top tools (schema size):
- browser: 9,812 chars (~2,453 tok)
- exec: 6,240 chars (~1,560 tok)
… (+N more tools)
What counts toward the context window
Everything the model receives counts, including:
- System prompt (all sections).
- Conversation history.
- Tool calls + tool results.
- Attachments/transcripts (images/audio/files).
- Compaction summaries and pruning artifacts.
- Provider “wrappers” or hidden headers (not visible, still counted).
How OpenClaw builds the system prompt
The system prompt is OpenClaw-owned and rebuilt each run. It includes:
- Tool list + short descriptions.
- Skills list (metadata only; see below).
- Workspace location.
- Time (UTC + converted user time if configured).
- Runtime metadata (host/OS/model/thinking).
- Injected workspace bootstrap files under Project Context.
Full breakdown: System Prompt.
Injected workspace files (Project Context)
By default, OpenClaw injects a fixed set of workspace files (if present):
AGENTS.mdSOUL.mdTOOLS.mdIDENTITY.mdUSER.mdHEARTBEAT.mdBOOTSTRAP.md(first-run only)
Large files are truncated per-file using agents.defaults.bootstrapMaxChars (default 20000 chars). /context shows raw vs injected sizes and whether truncation happened.
Skills: what’s injected vs loaded on-demand
The system prompt includes a compact skills list (name + description + location). This list has real overhead.
Skill instructions are not included by default. The model is expected to read the skill’s SKILL.md only when needed.
Tools: there are two costs
Tools affect context in two ways:
- Tool list text in the system prompt (what you see as “Tooling”).
- Tool schemas (JSON). These are sent to the model so it can call tools. They count toward context even though you don’t see them as plain text.
/context detail breaks down the biggest tool schemas so you can see what dominates.
Commands, directives, and “inline shortcuts”
Slash commands are handled by the Gateway. There are a few different behaviors:
- Standalone commands: a message that is only
/...runs as a command. - Directives:
/think,/verbose,/reasoning,/elevated,/model,/queueare stripped before the model sees the message.- Directive-only messages persist session settings.
- Inline directives in a normal message act as per-message hints.
- Inline shortcuts (allowlisted senders only): certain
/...tokens inside a normal message can run immediately (example: “hey /status”), and are stripped before the model sees the remaining text.
Details: Slash commands.
Sessions, compaction, and pruning (what persists)
What persists across messages depends on the mechanism:
- Normal history persists in the session transcript until compacted/pruned by policy.
- Compaction persists a summary into the transcript and keeps recent messages intact.
- Pruning removes old tool results from the in-memory prompt for a run, but does not rewrite the transcript.
Docs: Session, Compaction, Session pruning.
What /context actually reports
/context prefers the latest run-built system prompt report when available:
System prompt (run)= captured from the last embedded (tool-capable) run and persisted in the session store.System prompt (estimate)= computed on the fly when no run report exists (or when running via a CLI backend that doesn’t generate the report).
Either way, it reports sizes and top contributors; it does not dump the full system prompt or tool schemas.
Features
Source: https://docs.openclaw.ai/concepts/features
Highlights
WhatsApp, Telegram, Discord, and iMessage with a single Gateway. Add Mattermost and more with extensions. Multi-agent routing with isolated sessions. Images, audio, and documents in and out. Web Control UI and macOS companion app. iOS and Android nodes with Canvas support.Full list
- WhatsApp integration via WhatsApp Web (Baileys)
- Telegram bot support (grammY)
- Discord bot support (channels.discord.js)
- Mattermost bot support (plugin)
- iMessage integration via local imsg CLI (macOS)
- Agent bridge for Pi in RPC mode with tool streaming
- Streaming and chunking for long responses
- Multi-agent routing for isolated sessions per workspace or sender
- Subscription auth for Anthropic and OpenAI via OAuth
- Sessions: direct chats collapse into shared
main; groups are isolated - Group chat support with mention based activation
- Media support for images, audio, and documents
- Optional voice note transcription hook
- WebChat and macOS menu bar app
- iOS node with pairing and Canvas surface
- Android node with pairing, Canvas, chat, and camera
Group Messages
Source: https://docs.openclaw.ai/concepts/group-messages
Group messages (WhatsApp web channel)
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
Note: agents.list[].groupChat.mentionPatterns is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set agents.list[].groupChat.mentionPatterns per agent (or use messages.groupChat.mentionPatterns as a global fallback).
What’s implemented (2025-12-03)
- Activation modes:
mention(default) oralways.mentionrequires a ping (real WhatsApp @-mentions viamentionedJids, regex patterns, or the bot’s E.164 anywhere in the text).alwayswakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent tokenNO_REPLY. Defaults can be set in config (channels.whatsapp.groups) and overridden per group via/activation. Whenchannels.whatsapp.groupsis set, it also acts as a group allowlist (include"*"to allow all). - Group policy:
channels.whatsapp.groupPolicycontrols whether group messages are accepted (open|disabled|allowlist).allowlistuseschannels.whatsapp.groupAllowFrom(fallback: explicitchannels.whatsapp.allowFrom). Default isallowlist(blocked until you add senders). - Per-group sessions: session keys look like
agent:<agentId>:whatsapp:group:<jid>so commands such as/verbose onor/think high(sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. - Context injection: pending-only group messages (default 50) that did not trigger a run are prefixed under
[Chat messages since your last reply - for context], with the triggering line under[Current message - respond to this]. Messages already in the session are not re-injected. - Sender surfacing: every group batch now ends with
[from: Sender Name (+E164)]so Pi knows who is speaking. - Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
- Group system prompt: on the first turn of a group session (and whenever
/activationchanges the mode) we inject a short blurb into the system prompt likeYou are replying inside the WhatsApp group "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.If metadata isn’t available we still tell the agent it’s a group chat.
Config example (WhatsApp)
Add a groupChat block to ~/.openclaw/openclaw.json so display-name pings work even when WhatsApp strips the visual @ in the text body:
{
channels: {
whatsapp: {
groups: {
"*": { requireMention: true },
},
},
},
agents: {
list: [
{
id: "main",
groupChat: {
historyLimit: 50,
mentionPatterns: ["@?openclaw", "\\+?15555550123"],
},
},
],
},
}
Notes:
- The regexes are case-insensitive; they cover a display-name ping like
@openclawand the raw number with or without+/spaces. - WhatsApp still sends canonical mentions via
mentionedJidswhen someone taps the contact, so the number fallback is rarely needed but is a useful safety net.
Activation command (owner-only)
Use the group chat command:
/activation mention/activation always
Only the owner number (from channels.whatsapp.allowFrom, or the bot’s own E.164 when unset) can change this. Send /status as a standalone message in the group to see the current activation mode.
How to use
- Add your WhatsApp account (the one running OpenClaw) to the group.
- Say
@openclaw …(or include the number). Only allowlisted senders can trigger it unless you setgroupPolicy: "open". - The agent prompt will include recent group context plus the trailing
[from: …]marker so it can address the right person. - Session-level directives (
/verbose on,/think high,/newor/reset,/compact) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent.
Testing / verification
- Manual smoke:
- Send an
@openclawping in the group and confirm a reply that references the sender name. - Send a second ping and verify the history block is included then cleared on the next turn.
- Send an
- Check gateway logs (run with
--verbose) to seeinbound web messageentries showingfrom: <groupJid>and the[from: …]suffix.
Known considerations
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
- Session store entries will appear as
agent:<agentId>:whatsapp:group:<jid>in the session store (~/.openclaw/agents/<agentId>/sessions/sessions.jsonby default); a missing entry just means the group hasn’t triggered a run yet. - Typing indicators in groups follow
agents.defaults.typingMode(default:messagewhen unmentioned).
Groups
Source: https://docs.openclaw.ai/concepts/groups
Groups
OpenClaw treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Microsoft Teams.
Beginner intro (2 minutes)
OpenClaw “lives” on your own messaging accounts. There is no separate WhatsApp bot user. If you are in a group, OpenClaw can see that group and respond there.
Default behavior:
- Groups are restricted (
groupPolicy: "allowlist"). - Replies require a mention unless you explicitly disable mention gating.
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
TL;DR
- DM access is controlled by
*.allowFrom.- Group access is controlled by
*.groupPolicy+ allowlists (*.groups,*.groupAllowFrom).- Reply triggering is controlled by mention gating (
requireMention,/activation).
Quick flow (what happens to a group message):
groupPolicy? disabled -> drop
groupPolicy? allowlist -> group allowed? no -> drop
requireMention? yes -> mentioned? no -> store for context only
otherwise -> reply
If you want...
| Goal | What to set |
|---|---|
| Allow all groups but only reply on @mentions | groups: { "*": { requireMention: true } } |
| Disable all group replies | groupPolicy: "disabled" |
| Only specific groups | groups: { "<group-id>": { ... } } (no "*" key) |
| Only you can trigger in groups | groupPolicy: "allowlist", groupAllowFrom: ["+1555..."] |
Session keys
- Group sessions use
agent:<agentId>:<channel>:group:<id>session keys (rooms/channels useagent:<agentId>:<channel>:channel:<id>). - Telegram forum topics add
:topic:<threadId>to the group id so each topic has its own session. - Direct chats use the main session (or per-sender if configured).
- Heartbeats are skipped for group sessions.
Pattern: personal DMs + public groups (single agent)
Yes — this works well if your “personal” traffic is DMs and your “public” traffic is groups.
Why: in single-agent mode, DMs typically land in the main session key (agent:main:main), while groups always use non-main session keys (agent:main:<channel>:group:<id>). If you enable sandboxing with mode: "non-main", those group sessions run in Docker while your main DM session stays on-host.
This gives you one agent “brain” (shared workspace + memory), but two execution postures:
- DMs: full tools (host)
- Groups: sandbox + restricted tools (Docker)
If you need truly separate workspaces/personas (“personal” and “public” must never mix), use a second agent + bindings. See Multi-Agent Routing.
Example (DMs on host, groups sandboxed + messaging-only tools):
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none",
},
},
},
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
Want “groups can only see folder X” instead of “no host access”? Keep workspaceAccess: "none" and mount only allowlisted paths into the sandbox:
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"~/FriendsShared:/data:ro",
],
},
},
},
},
}
Related:
- Configuration keys and defaults: Gateway configuration
- Debugging why a tool is blocked: Sandbox vs Tool Policy vs Elevated
- Bind mounts details: Sandboxing
Display labels
- UI labels use
displayNamewhen available, formatted as<channel>:<token>. #roomis reserved for rooms/channels; group chats useg-<slug>(lowercase, spaces ->-, keep#@+._-).
Group policy
Control how group/room messages are handled per channel:
{
channels: {
whatsapp: {
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
groupAllowFrom: ["+15551234567"],
},
telegram: {
groupPolicy: "disabled",
groupAllowFrom: ["123456789", "@username"],
},
signal: {
groupPolicy: "disabled",
groupAllowFrom: ["+15551234567"],
},
imessage: {
groupPolicy: "disabled",
groupAllowFrom: ["chat_id:123"],
},
msteams: {
groupPolicy: "disabled",
groupAllowFrom: ["user@org.com"],
},
discord: {
groupPolicy: "allowlist",
guilds: {
GUILD_ID: { channels: { help: { allow: true } } },
},
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } },
},
matrix: {
groupPolicy: "allowlist",
groupAllowFrom: ["@owner:example.org"],
groups: {
"!roomId:example.org": { allow: true },
"#alias:example.org": { allow: true },
},
},
},
}
| Policy | Behavior |
|---|---|
"open" |
Groups bypass allowlists; mention-gating still applies. |
"disabled" |
Block all group messages entirely. |
"allowlist" |
Only allow groups/rooms that match the configured allowlist. |
Notes:
groupPolicyis separate from mention-gating (which requires @mentions).- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use
groupAllowFrom(fallback: explicitallowFrom). - Discord: allowlist uses
channels.discord.guilds.<id>.channels. - Slack: allowlist uses
channels.slack.channels. - Matrix: allowlist uses
channels.matrix.groups(room IDs, aliases, or names). Usechannels.matrix.groupAllowFromto restrict senders; per-roomusersallowlists are also supported. - Group DMs are controlled separately (
channels.discord.dm.*,channels.slack.dm.*). - Telegram allowlist can match user IDs (
"123456789","telegram:123456789","tg:123456789") or usernames ("@alice"or"alice"); prefixes are case-insensitive. - Default is
groupPolicy: "allowlist"; if your group allowlist is empty, group messages are blocked.
Quick mental model (evaluation order for group messages):
groupPolicy(open/disabled/allowlist)- group allowlists (
*.groups,*.groupAllowFrom, channel-specific allowlist) - mention gating (
requireMention,/activation)
Mention gating (default)
Group messages require a mention unless overridden per group. Defaults live per subsystem under *.groups."*".
Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams.
{
channels: {
whatsapp: {
groups: {
"*": { requireMention: true },
"123@g.us": { requireMention: false },
},
},
telegram: {
groups: {
"*": { requireMention: true },
"123456789": { requireMention: false },
},
},
imessage: {
groups: {
"*": { requireMention: true },
"123": { requireMention: false },
},
},
},
agents: {
list: [
{
id: "main",
groupChat: {
mentionPatterns: ["@openclaw", "openclaw", "\\+15555550123"],
historyLimit: 50,
},
},
],
},
}
Notes:
mentionPatternsare case-insensitive regexes.- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override:
agents.list[].groupChat.mentionPatterns(useful when multiple agents share a group). - Mention gating is only enforced when mention detection is possible (native mentions or
mentionPatternsare configured). - Discord defaults live in
channels.discord.guilds."*"(overridable per guild/channel). - Group history context is wrapped uniformly across channels and is pending-only (messages skipped due to mention gating); use
messages.groupChat.historyLimitfor the global default andchannels.<channel>.historyLimit(orchannels.<channel>.accounts.*.historyLimit) for overrides. Set0to disable.
Group/channel tool restrictions (optional)
Some channel configs support restricting which tools are available inside a specific group/room/channel.
tools: allow/deny tools for the whole group.toolsBySender: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use"*"as a wildcard.
Resolution order (most specific wins):
- group/channel
toolsBySendermatch - group/channel
tools - default (
"*")toolsBySendermatch - default (
"*")tools
Example (Telegram):
{
channels: {
telegram: {
groups: {
"*": { tools: { deny: ["exec"] } },
"-1001234567890": {
tools: { deny: ["exec", "read", "write"] },
toolsBySender: {
"123456789": { alsoAllow: ["exec"] },
},
},
},
},
},
}
Notes:
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
- Some channels use different nesting for rooms/channels (e.g., Discord
guilds.*.channels.*, Slackchannels.*, MS Teamsteams.*.channels.*).
Group allowlists
When channels.whatsapp.groups, channels.telegram.groups, or channels.imessage.groups is configured, the keys act as a group allowlist. Use "*" to allow all groups while still setting default mention behavior.
Common intents (copy/paste):
- Disable all group replies
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
- Allow only specific groups (WhatsApp)
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
},
},
},
}
- Allow all groups but require mention (explicit)
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
- Only the owner can trigger in groups (WhatsApp)
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
Activation (owner-only)
Group owners can toggle per-group activation:
/activation mention/activation always
Owner is determined by channels.whatsapp.allowFrom (or the bot’s self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore /activation.
Context fields
Group inbound payloads set:
ChatType=groupGroupSubject(if known)GroupMembers(if known)WasMentioned(mention gating result)- Telegram forum topics also include
MessageThreadIdandIsForum.
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, and avoid typing literal \n sequences.
iMessage specifics
- Prefer
chat_id:<id>when routing or allowlisting. - List chats:
imsg chats --limit 20. - Group replies always go back to the same
chat_id.
WhatsApp specifics
See Group messages for WhatsApp-only behavior (history injection, mention handling details).
null
Source: https://docs.openclaw.ai/concepts/memory
Memory
OpenClaw memory is plain Markdown in the agent workspace. The files are the source of truth; the model only "remembers" what gets written to disk.
Memory search tools are provided by the active memory plugin (default:
memory-core). Disable memory plugins with plugins.slots.memory = "none".
Memory files (Markdown)
The default workspace layout uses two memory layers:
memory/YYYY-MM-DD.md- Daily log (append-only).
- Read today + yesterday at session start.
MEMORY.md(optional)- Curated long-term memory.
- Only load in the main, private session (never in group contexts).
These files live under the workspace (agents.defaults.workspace, default
~/clawd). See Agent workspace for the full layout.
When to write memory
- Decisions, preferences, and durable facts go to
MEMORY.md. - Day-to-day notes and running context go to
memory/YYYY-MM-DD.md. - If someone says "remember this," write it down (do not keep it in RAM).
- This area is still evolving. It helps to remind the model to store memories; it will know what to do.
- If you want something to stick, ask the bot to write it into memory.
Automatic memory flush (pre-compaction ping)
When a session is close to auto-compaction, OpenClaw triggers a silent,
agentic turn that reminds the model to write durable memory before the
context is compacted. The default prompts explicitly say the model may reply,
but usually NO_REPLY is the correct response so the user never sees this turn.
This is controlled by agents.defaults.compaction.memoryFlush:
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}
Details:
- Soft threshold: flush triggers when the session token estimate crosses
contextWindow - reserveTokensFloor - softThresholdTokens. - Silent by default: prompts include
NO_REPLYso nothing is delivered. - Two prompts: a user prompt plus a system prompt append the reminder.
- One flush per compaction cycle (tracked in
sessions.json). - Workspace must be writable: if the session runs sandboxed with
workspaceAccess: "ro"or"none", the flush is skipped.
For the full compaction lifecycle, see Session management + compaction.
Vector memory search
OpenClaw can build a small vector index over MEMORY.md and memory/*.md so
semantic queries can find related notes even when wording differs.
Defaults:
- Enabled by default.
- Watches memory files for changes (debounced).
- Uses remote embeddings by default. If
memorySearch.provideris not set, OpenClaw auto-selects:localif amemorySearch.local.modelPathis configured and the file exists.openaiif an OpenAI key can be resolved.geminiif a Gemini key can be resolved.- Otherwise memory search stays disabled until configured.
- Local mode uses node-llama-cpp and may require
pnpm approve-builds. - Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
Remote embeddings require an API key for the embedding provider. OpenClaw
resolves keys from auth profiles, models.providers.*.apiKey, or environment
variables. Codex OAuth only covers chat/completions and does not satisfy
embeddings for memory search. For Gemini, use GEMINI_API_KEY or
models.providers.google.apiKey. When using a custom OpenAI-compatible endpoint,
set memorySearch.remote.apiKey (and optional memorySearch.remote.headers).
QMD backend (experimental)
Set memory.backend = "qmd" to swap the built-in SQLite indexer for
QMD: a local-first search sidecar that combines
BM25 + vectors + reranking. Markdown stays the source of truth; OpenClaw shells
out to QMD for retrieval. Key points:
Prereqs
- Disabled by default. Opt in per-config (
memory.backend = "qmd"). - Install the QMD CLI separately (
bun install -g github.com/tobi/qmdor grab a release) and make sure theqmdbinary is on the gateway’sPATH. - QMD needs an SQLite build that allows extensions (
brew install sqliteon macOS). - QMD runs fully locally via Bun +
node-llama-cppand auto-downloads GGUF models from HuggingFace on first use (no separate Ollama daemon required). - The gateway runs QMD in a self-contained XDG home under
~/.openclaw/agents/<agentId>/qmd/by settingXDG_CONFIG_HOMEandXDG_CACHE_HOME. - OS support: macOS and Linux work out of the box once Bun + SQLite are installed. Windows is best supported via WSL2.
How the sidecar runs
- The gateway writes a self-contained QMD home under
~/.openclaw/agents/<agentId>/qmd/(config + cache + sqlite DB). - Collections are rewritten from
memory.qmd.paths(plus default workspace memory files) intoindex.yml, thenqmd update+qmd embedrun on boot and on a configurable interval (memory.qmd.update.interval, default 5 m). - Searches run via
qmd query --json. If QMD fails or the binary is missing, OpenClaw automatically falls back to the builtin SQLite manager so memory tools keep working. - First search may be slow: QMD may download local GGUF models (reranker/query
expansion) on the first
qmd queryrun.-
OpenClaw sets
XDG_CONFIG_HOME/XDG_CACHE_HOMEautomatically when it runs QMD. -
If you want to pre-download models manually (and warm the same index OpenClaw uses), run a one-off query with the agent’s XDG dirs.
OpenClaw’s QMD state lives under your state dir (defaults to
~/.openclaw). You can pointqmdat the exact same index by exporting the same XDG vars OpenClaw uses:# Pick the same state dir OpenClaw uses STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" if [ -d "$HOME/.moltbot" ] && [ ! -d "$HOME/.openclaw" ] \ && [ -z "${OPENCLAW_STATE_DIR:-}" ]; then STATE_DIR="$HOME/.moltbot" fi export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config" export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache" # (Optional) force an index refresh + embeddings qmd update qmd embed # Warm up / trigger first-time model downloads qmd query "test" -c memory-root --json >/dev/null 2>&1
-
Config surface (memory.qmd.*)
command(defaultqmd): override the executable path.includeDefaultMemory(defaulttrue): auto-indexMEMORY.md+memory/**/*.md.paths[]: add extra directories/files (path, optionalpattern, optional stablename).sessions: opt into session JSONL indexing (enabled,retentionDays,exportDir).update: controls refresh cadence (interval,debounceMs,onBoot,embedInterval).limits: clamp recall payload (maxResults,maxSnippetChars,maxInjectedChars,timeoutMs).scope: same schema assession.sendPolicy. Default is DM-only (denyall,allowdirect chats); loosen it to surface QMD hits in groups/channels.- Snippets sourced outside the workspace show up as
qmd/<collection>/<relative-path>inmemory_searchresults;memory_getunderstands that prefix and reads from the configured QMD collection root. - When
memory.qmd.sessions.enabled = true, OpenClaw exports sanitized session transcripts (User/Assistant turns) into a dedicated QMD collection under~/.openclaw/agents/<id>/qmd/sessions/, somemory_searchcan recall recent conversations without touching the builtin SQLite index. memory_searchsnippets now include aSource: <path#line>footer whenmemory.citationsisauto/on; setmemory.citations = "off"to keep the path metadata internal (the agent still receives the path formemory_get, but the snippet text omits the footer and the system prompt warns the agent not to cite it).
Example
memory: {
backend: "qmd",
citations: "auto",
qmd: {
includeDefaultMemory: true,
update: { interval: "5m", debounceMs: 15000 },
limits: { maxResults: 6, timeoutMs: 4000 },
scope: {
default: "deny",
rules: [{ action: "allow", match: { chatType: "direct" } }]
},
paths: [
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
]
}
}
Citations & fallback
memory.citationsapplies regardless of backend (auto/on/off).- When
qmdruns, we tagstatus().backend = "qmd"so diagnostics show which engine served the results. If the QMD subprocess exits or JSON output can’t be parsed, the search manager logs a warning and returns the builtin provider (existing Markdown embeddings) until QMD recovers.
Additional memory paths
If you want to index Markdown files outside the default workspace layout, add explicit paths:
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}
Notes:
- Paths can be absolute or workspace-relative.
- Directories are scanned recursively for
.mdfiles. - Only Markdown files are indexed.
- Symlinks are ignored (files or directories).
Gemini embeddings (native)
Set the provider to gemini to use the Gemini embeddings API directly:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-001",
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
Notes:
remote.baseUrlis optional (defaults to the Gemini API base URL).remote.headerslets you add extra headers if needed.- Default model:
gemini-embedding-001.
If you want to use a custom OpenAI-compatible endpoint (OpenRouter, vLLM, or a proxy),
you can use the remote configuration with the OpenAI provider:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
headers: { "X-Custom-Header": "value" }
}
}
}
}
If you don't want to set an API key, use memorySearch.provider = "local" or set
memorySearch.fallback = "none".
Fallbacks:
memorySearch.fallbackcan beopenai,gemini,local, ornone.- The fallback provider is only used when the primary embedding provider fails.
Batch indexing (OpenAI + Gemini):
- Enabled by default for OpenAI and Gemini embeddings. Set
agents.defaults.memorySearch.remote.batch.enabled = falseto disable. - Default behavior waits for batch completion; tune
remote.batch.wait,remote.batch.pollIntervalMs, andremote.batch.timeoutMinutesif needed. - Set
remote.batch.concurrencyto control how many batch jobs we submit in parallel (default: 2). - Batch mode applies when
memorySearch.provider = "openai"or"gemini"and uses the corresponding API key. - Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
Why OpenAI batch is fast + cheap:
- For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
- OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
- See the OpenAI Batch API docs and pricing for details:
Config example:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
fallback: "openai",
remote: {
batch: { enabled: true, concurrency: 2 }
},
sync: { watch: true }
}
}
}
Tools:
memory_search— returns snippets with file + line ranges.memory_get— read memory file content by path.
Local mode:
- Set
agents.defaults.memorySearch.provider = "local". - Provide
agents.defaults.memorySearch.local.modelPath(GGUF orhf:URI). - Optional: set
agents.defaults.memorySearch.fallback = "none"to avoid remote fallback.
How the memory tools work
memory_searchsemantically searches Markdown chunks (~400 token target, 80-token overlap) fromMEMORY.md+memory/**/*.md. It returns snippet text (capped ~700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embeddings. No full file payload is returned.memory_getreads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outsideMEMORY.md/memory/are rejected.- Both tools are enabled only when
memorySearch.enabledresolves true for the agent.
What gets indexed (and when)
- File type: Markdown only (
MEMORY.md,memory/**/*.md). - Index storage: per-agent SQLite at
~/.openclaw/memory/<agentId>.sqlite(configurable viaagents.defaults.memorySearch.store.path, supports{agentId}token). - Freshness: watcher on
MEMORY.md+memory/marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync. - Reindex triggers: the index stores the embedding provider/model + endpoint fingerprint + chunking params. If any of those change, OpenClaw automatically resets and reindexes the entire store.
Hybrid search (BM25 + vector)
When enabled, OpenClaw combines:
- Vector similarity (semantic match, wording can differ)
- BM25 keyword relevance (exact tokens like IDs, env vars, code symbols)
If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search.
Why hybrid?
Vector search is great at “this means the same thing”:
- “Mac Studio gateway host” vs “the machine running the gateway”
- “debounce file updates” vs “avoid indexing on every write”
But it can be weak at exact, high-signal tokens:
- IDs (
a828e60,b3b9895a…) - code symbols (
memorySearch.query.hybrid) - error strings (“sqlite-vec unavailable”)
BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases. Hybrid search is the pragmatic middle ground: use both retrieval signals so you get good results for both “natural language” queries and “needle in a haystack” queries.
How we merge results (the current design)
Implementation sketch:
- Retrieve a candidate pool from both sides:
- Vector: top
maxResults * candidateMultiplierby cosine similarity. - BM25: top
maxResults * candidateMultiplierby FTS5 BM25 rank (lower is better).
- Convert BM25 rank into a 0..1-ish score:
textScore = 1 / (1 + max(0, bm25Rank))
- Union candidates by chunk id and compute a weighted score:
finalScore = vectorWeight * vectorScore + textWeight * textScore
Notes:
vectorWeight+textWeightis normalized to 1.0 in config resolution, so weights behave as percentages.- If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
- If FTS5 can’t be created, we keep vector-only search (no hard failure).
This isn’t “IR-theory perfect”, but it’s simple, fast, and tends to improve recall/precision on real notes. If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization (min/max or z-score) before mixing.
Config:
agents: {
defaults: {
memorySearch: {
query: {
hybrid: {
enabled: true,
vectorWeight: 0.7,
textWeight: 0.3,
candidateMultiplier: 4
}
}
}
}
}
Embedding cache
OpenClaw can cache chunk embeddings in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
Config:
agents: {
defaults: {
memorySearch: {
cache: {
enabled: true,
maxEntries: 50000
}
}
}
}
Session memory search (experimental)
You can optionally index session transcripts and surface them via memory_search.
This is gated behind an experimental flag.
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}
Notes:
- Session indexing is opt-in (off by default).
- Session updates are debounced and indexed asynchronously once they cross delta thresholds (best-effort).
memory_searchnever blocks on indexing; results can be slightly stale until background sync finishes.- Results still include snippets only;
memory_getremains limited to memory files. - Session indexing is isolated per agent (only that agent’s session logs are indexed).
- Session logs live on disk (
~/.openclaw/agents/<agentId>/sessions/*.jsonl). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
Delta thresholds (defaults shown):
agents: {
defaults: {
memorySearch: {
sync: {
sessions: {
deltaBytes: 100000, // ~100 KB
deltaMessages: 50 // JSONL lines
}
}
}
}
}
SQLite vector acceleration (sqlite-vec)
When the sqlite-vec extension is available, OpenClaw stores embeddings in a
SQLite virtual table (vec0) and performs vector distance queries in the
database. This keeps search fast without loading every embedding into JS.
Configuration (optional):
agents: {
defaults: {
memorySearch: {
store: {
vector: {
enabled: true,
extensionPath: "/path/to/sqlite-vec"
}
}
}
}
}
Notes:
enableddefaults to true; when disabled, search falls back to in-process cosine similarity over stored embeddings.- If the sqlite-vec extension is missing or fails to load, OpenClaw logs the error and continues with the JS fallback (no vector table).
extensionPathoverrides the bundled sqlite-vec path (useful for custom builds or non-standard install locations).
Local embedding auto-download
- Default local embedding model:
hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf(~0.6 GB). - When
memorySearch.provider = "local",node-llama-cppresolvesmodelPath; if the GGUF is missing it auto-downloads to the cache (orlocal.modelCacheDirif set), then loads it. Downloads resume on retry. - Native build requirement: run
pnpm approve-builds, picknode-llama-cpp, thenpnpm rebuild node-llama-cpp. - Fallback: if local setup fails and
memorySearch.fallback = "openai", we automatically switch to remote embeddings (openai/text-embedding-3-smallunless overridden) and record the reason.
Custom OpenAI-compatible endpoint example
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_REMOTE_API_KEY",
headers: {
"X-Organization": "org-id",
"X-Project": "project-id"
}
}
}
}
}
Notes:
remote.*takes precedence overmodels.providers.openai.*.remote.headersmerge with OpenAI headers; remote wins on key conflicts. Omitremote.headersto use the OpenAI defaults.
Messages
Source: https://docs.openclaw.ai/concepts/messages
Messages
This page ties together how OpenClaw handles inbound messages, sessions, queueing, streaming, and reasoning visibility.
Message flow (high level)
Inbound message
-> routing/bindings -> session key
-> queue (if a run is active)
-> agent run (streaming + tools)
-> outbound replies (channel limits + chunking)
Key knobs live in configuration:
messages.*for prefixes, queueing, and group behavior.agents.defaults.*for block streaming and chunking defaults.- Channel overrides (
channels.whatsapp.*,channels.telegram.*, etc.) for caps and streaming toggles.
See Configuration for full schema.
Inbound dedupe
Channels can redeliver the same message after reconnects. OpenClaw keeps a short-lived cache keyed by channel/account/peer/session/message id so duplicate deliveries do not trigger another agent run.
Inbound debouncing
Rapid consecutive messages from the same sender can be batched into a single
agent turn via messages.inbound. Debouncing is scoped per channel + conversation
and uses the most recent message for reply threading/IDs.
Config (global default + per-channel overrides):
{
messages: {
inbound: {
debounceMs: 2000,
byChannel: {
whatsapp: 5000,
slack: 1500,
discord: 1500,
},
},
},
}
Notes:
- Debounce applies to text-only messages; media/attachments flush immediately.
- Control commands bypass debouncing so they remain standalone.
Sessions and devices
Sessions are owned by the gateway, not by clients.
- Direct chats collapse into the agent main session key.
- Groups/channels get their own session keys.
- The session store and transcripts live on the gateway host.
Multiple devices/channels can map to the same session, but history is not fully synced back to every client. Recommendation: use one primary device for long conversations to avoid divergent context. The Control UI and TUI always show the gateway-backed session transcript, so they are the source of truth.
Details: Session management.
Inbound bodies and history context
OpenClaw separates the prompt body from the command body:
Body: prompt text sent to the agent. This may include channel envelopes and optional history wrappers.CommandBody: raw user text for directive/command parsing.RawBody: legacy alias forCommandBody(kept for compatibility).
When a channel supplies history, it uses a shared wrapper:
[Chat messages since your last reply - for context][Current message - respond to this]
For non-direct chats (groups/channels/rooms), the current message body is prefixed with the sender label (same style used for history entries). This keeps real-time and queued/history messages consistent in the agent prompt.
History buffers are pending-only: they include group messages that did not trigger a run (for example, mention-gated messages) and exclude messages already in the session transcript.
Directive stripping only applies to the current message section so history
remains intact. Channels that wrap history should set CommandBody (or
RawBody) to the original message text and keep Body as the combined prompt.
History buffers are configurable via messages.groupChat.historyLimit (global
default) and per-channel overrides like channels.slack.historyLimit or
channels.telegram.accounts.<id>.historyLimit (set 0 to disable).
Queueing and followups
If a run is already active, inbound messages can be queued, steered into the current run, or collected for a followup turn.
- Configure via
messages.queue(andmessages.queue.byChannel). - Modes:
interrupt,steer,followup,collect, plus backlog variants.
Details: Queueing.
Streaming, chunking, and batching
Block streaming sends partial replies as the model produces text blocks. Chunking respects channel text limits and avoids splitting fenced code.
Key settings:
agents.defaults.blockStreamingDefault(on|off, default off)agents.defaults.blockStreamingBreak(text_end|message_end)agents.defaults.blockStreamingChunk(minChars|maxChars|breakPreference)agents.defaults.blockStreamingCoalesce(idle-based batching)agents.defaults.humanDelay(human-like pause between block replies)- Channel overrides:
*.blockStreamingand*.blockStreamingCoalesce(non-Telegram channels require explicit*.blockStreaming: true)
Details: Streaming + chunking.
Reasoning visibility and tokens
OpenClaw can expose or hide model reasoning:
/reasoning on|off|streamcontrols visibility.- Reasoning content still counts toward token usage when produced by the model.
- Telegram supports reasoning stream into the draft bubble.
Details: Thinking + reasoning directives and Token use.
Prefixes, threading, and replies
Outbound message formatting is centralized in messages:
messages.responsePrefix,channels.<channel>.responsePrefix, andchannels.<channel>.accounts.<id>.responsePrefix(outbound prefix cascade), pluschannels.whatsapp.messagePrefix(WhatsApp inbound prefix)- Reply threading via
replyToModeand per-channel defaults
Details: Configuration and channel docs.
Multi-Agent Routing
Source: https://docs.openclaw.ai/concepts/multi-agent
Multi-Agent Routing
Goal: multiple isolated agents (separate workspace + agentDir + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
What is “one agent”?
An agent is a fully scoped brain with its own:
- Workspace (files, AGENTS.md/SOUL.md/USER.md, local notes, persona rules).
- State directory (
agentDir) for auth profiles, model registry, and per-agent config. - Session store (chat history + routing state) under
~/.openclaw/agents/<agentId>/sessions.
Auth profiles are per-agent. Each agent reads from its own:
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
Main agent credentials are not shared automatically. Never reuse agentDir
across agents (it causes auth/session collisions). If you want to share creds,
copy auth-profiles.json into the other agent's agentDir.
Skills are per-agent via each workspace’s skills/ folder, with shared skills
available from ~/.openclaw/skills. See Skills: per-agent vs shared.
The Gateway can host one agent (default) or many agents side-by-side.
Workspace note: each agent’s workspace is the default cwd, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See Sandboxing.
Paths (quick map)
- Config:
~/.openclaw/openclaw.json(orOPENCLAW_CONFIG_PATH) - State dir:
~/.openclaw(orOPENCLAW_STATE_DIR) - Workspace:
~/.openclaw/workspace(or~/.openclaw/workspace-<agentId>) - Agent dir:
~/.openclaw/agents/<agentId>/agent(oragents.list[].agentDir) - Sessions:
~/.openclaw/agents/<agentId>/sessions
Single-agent mode (default)
If you do nothing, OpenClaw runs a single agent:
agentIddefaults tomain.- Sessions are keyed as
agent:main:<mainKey>. - Workspace defaults to
~/.openclaw/workspace(or~/.openclaw/workspace-<profile>whenOPENCLAW_PROFILEis set). - State defaults to
~/.openclaw/agents/main/agent.
Agent helper
Use the agent wizard to add a new isolated agent:
openclaw agents add work
Then add bindings (or let the wizard do it) to route inbound messages.
Verify with:
openclaw agents list --bindings
Multiple agents = multiple people, multiple personalities
With multiple agents, each agentId becomes a fully isolated persona:
- Different phone numbers/accounts (per channel
accountId). - Different personalities (per-agent workspace files like
AGENTS.mdandSOUL.md). - Separate auth + sessions (no cross-talk unless explicitly enabled).
This lets multiple people share one Gateway server while keeping their AI “brains” and data isolated.
One WhatsApp number, multiple people (DM split)
You can route different WhatsApp DMs to different agents while staying on one WhatsApp account. Match on sender E.164 (like +15551234567) with peer.kind: "dm". Replies still come from the same WhatsApp number (no per‑agent sender identity).
Important detail: direct chats collapse to the agent’s main session key, so true isolation requires one agent per person.
Example:
{
agents: {
list: [
{ id: "alex", workspace: "~/.openclaw/workspace-alex" },
{ id: "mia", workspace: "~/.openclaw/workspace-mia" },
],
},
bindings: [
{ agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
{ agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } },
],
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551230001", "+15551230002"],
},
},
}
Notes:
- DM access control is global per WhatsApp account (pairing/allowlist), not per agent.
- For shared groups, bind the group to one agent or use Broadcast groups.
Routing rules (how messages pick an agent)
Bindings are deterministic and most-specific wins:
peermatch (exact DM/group/channel id)guildId(Discord)teamId(Slack)accountIdmatch for a channel- channel-level match (
accountId: "*") - fallback to default agent (
agents.list[].default, else first list entry, default:main)
Multiple accounts / phone numbers
Channels that support multiple accounts (e.g. WhatsApp) use accountId to identify
each login. Each accountId can be routed to a different agent, so one server can host
multiple phone numbers without mixing sessions.
Concepts
agentId: one “brain” (workspace, per-agent auth, per-agent session store).accountId: one channel account instance (e.g. WhatsApp account"personal"vs"biz").binding: routes inbound messages to anagentIdby(channel, accountId, peer)and optionally guild/team ids.- Direct chats collapse to
agent:<agentId>:<mainKey>(per-agent “main”;session.mainKey).
Example: two WhatsApps → two agents
~/.openclaw/openclaw.json (JSON5):
{
agents: {
list: [
{
id: "home",
default: true,
name: "Home",
workspace: "~/.openclaw/workspace-home",
agentDir: "~/.openclaw/agents/home/agent",
},
{
id: "work",
name: "Work",
workspace: "~/.openclaw/workspace-work",
agentDir: "~/.openclaw/agents/work/agent",
},
],
},
// Deterministic routing: first match wins (most-specific first).
bindings: [
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
// Optional per-peer override (example: send a specific group to work agent).
{
agentId: "work",
match: {
channel: "whatsapp",
accountId: "personal",
peer: { kind: "group", id: "1203630...@g.us" },
},
},
],
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
channels: {
whatsapp: {
accounts: {
personal: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
// authDir: "~/.openclaw/credentials/whatsapp/personal",
},
biz: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}
Example: WhatsApp daily chat + Telegram deep work
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-5",
},
],
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } },
],
}
Notes:
- If you have multiple accounts for a channel, add
accountIdto the binding (for example{ channel: "whatsapp", accountId: "personal" }). - To route a single DM/group to Opus while keeping the rest on chat, add a
match.peerbinding for that peer; peer matches always win over channel-wide rules.
Example: same channel, one peer to Opus
Keep WhatsApp on the fast agent, but route one DM to Opus:
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-5",
},
],
},
bindings: [
{ agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
{ agentId: "chat", match: { channel: "whatsapp" } },
],
}
Peer bindings always win, so keep them above the channel-wide rule.
Family agent bound to a WhatsApp group
Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy:
{
agents: {
list: [
{
id: "family",
name: "Family",
workspace: "~/.openclaw/workspace-family",
identity: { name: "Family Bot" },
groupChat: {
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
},
sandbox: {
mode: "all",
scope: "agent",
},
tools: {
allow: [
"exec",
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
},
},
],
},
bindings: [
{
agentId: "family",
match: {
channel: "whatsapp",
peer: { kind: "group", id: "120363999999999999@g.us" },
},
},
],
}
Notes:
- Tool allow/deny lists are tools, not skills. If a skill needs to run a
binary, ensure
execis allowed and the binary exists in the sandbox. - For stricter gating, set
agents.list[].groupChat.mentionPatternsand keep group allowlists enabled for the channel.
Per-Agent Sandbox and Tool Configuration
Starting with v2026.1.6, each agent can have its own sandbox and tool restrictions:
{
agents: {
list: [
{
id: "personal",
workspace: "~/.openclaw/workspace-personal",
sandbox: {
mode: "off", // No sandbox for personal agent
},
// No tool restrictions - all tools available
},
{
id: "family",
workspace: "~/.openclaw/workspace-family",
sandbox: {
mode: "all", // Always sandboxed
scope: "agent", // One container per agent
docker: {
// Optional one-time setup after container creation
setupCommand: "apt-get update && apt-get install -y git curl",
},
},
tools: {
allow: ["read"], // Only read tool
deny: ["exec", "write", "edit", "apply_patch"], // Deny others
},
},
],
},
}
Note: setupCommand lives under sandbox.docker and runs once on container creation.
Per-agent sandbox.docker.* overrides are ignored when the resolved scope is "shared".
Benefits:
- Security isolation: Restrict tools for untrusted agents
- Resource control: Sandbox specific agents while keeping others on host
- Flexible policies: Different permissions per agent
Note: tools.elevated is global and sender-based; it is not configurable per agent.
If you need per-agent boundaries, use agents.list[].tools to deny exec.
For group targeting, use agents.list[].groupChat.mentionPatterns so @mentions map cleanly to the intended agent.
See Multi-Agent Sandbox & Tools for detailed examples.
OAuth
Source: https://docs.openclaw.ai/concepts/oauth
OAuth
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably OpenAI Codex (ChatGPT OAuth)). For Anthropic subscriptions, use the setup-token flow. This page explains:
- how the OAuth token exchange works (PKCE)
- where tokens are stored (and why)
- how to handle multiple accounts (profiles + per-session overrides)
OpenClaw also supports provider plugins that ship their own OAuth or API‑key flows. Run them via:
openclaw models auth login --provider <id>
The token sink (why it exists)
OAuth providers commonly mint a new refresh token during login/refresh flows. Some providers (or OAuth clients) can invalidate older refresh tokens when a new one is issued for the same user/app.
Practical symptom:
- you log in via OpenClaw and via Claude Code / Codex CLI → one of them randomly gets “logged out” later
To reduce that, OpenClaw treats auth-profiles.json as a token sink:
- the runtime reads credentials from one place
- we can keep multiple profiles and route them deterministically
Storage (where tokens live)
Secrets are stored per-agent:
- Auth profiles (OAuth + API keys):
~/.openclaw/agents/<agentId>/agent/auth-profiles.json - Runtime cache (managed automatically; don’t edit):
~/.openclaw/agents/<agentId>/agent/auth.json
Legacy import-only file (still supported, but not the main store):
~/.openclaw/credentials/oauth.json(imported intoauth-profiles.jsonon first use)
All of the above also respect $OPENCLAW_STATE_DIR (state dir override). Full reference: /gateway/configuration
Anthropic setup-token (subscription auth)
Run claude setup-token on any machine, then paste it into OpenClaw:
openclaw models auth setup-token --provider anthropic
If you generated the token elsewhere, paste it manually:
openclaw models auth paste-token --provider anthropic
Verify:
openclaw models status
OAuth exchange (how login works)
OpenClaw’s interactive login flows are implemented in @mariozechner/pi-ai and wired into the wizards/commands.
Anthropic (Claude Pro/Max) setup-token
Flow shape:
- run
claude setup-token - paste the token into OpenClaw
- store as a token auth profile (no refresh)
The wizard path is openclaw onboard → auth choice setup-token (Anthropic).
OpenAI Codex (ChatGPT OAuth)
Flow shape (PKCE):
- generate PKCE verifier/challenge + random
state - open
https://auth.openai.com/oauth/authorize?... - try to capture callback on
http://127.0.0.1:1455/auth/callback - if callback can’t bind (or you’re remote/headless), paste the redirect URL/code
- exchange at
https://auth.openai.com/oauth/token - extract
accountIdfrom the access token and store{ access, refresh, expires, accountId }
Wizard path is openclaw onboard → auth choice openai-codex.
Refresh + expiry
Profiles store an expires timestamp.
At runtime:
- if
expiresis in the future → use the stored access token - if expired → refresh (under a file lock) and overwrite the stored credentials
The refresh flow is automatic; you generally don't need to manage tokens manually.
Multiple accounts (profiles) + routing
Two patterns:
1) Preferred: separate agents
If you want “personal” and “work” to never interact, use isolated agents (separate sessions + credentials + workspace):
openclaw agents add work
openclaw agents add personal
Then configure auth per-agent (wizard) and route chats to the right agent.
2) Advanced: multiple profiles in one agent
auth-profiles.json supports multiple profile IDs for the same provider.
Pick which profile is used:
- globally via config ordering (
auth.order) - per-session via
/model ...@<profileId>
Example (session override):
/model Opus@anthropic:work
How to see what profile IDs exist:
openclaw channels list --json(showsauth[])
Related docs:
- /concepts/model-failover (rotation + cooldown rules)
- /tools/slash-commands (command surface)
Presence
Source: https://docs.openclaw.ai/concepts/presence
Presence
OpenClaw “presence” is a lightweight, best‑effort view of:
- the Gateway itself, and
- clients connected to the Gateway (mac app, WebChat, CLI, etc.)
Presence is used primarily to render the macOS app’s Instances tab and to provide quick operator visibility.
Presence fields (what shows up)
Presence entries are structured objects with fields like:
instanceId(optional but strongly recommended): stable client identity (usuallyconnect.client.instanceId)host: human‑friendly host nameip: best‑effort IP addressversion: client version stringdeviceFamily/modelIdentifier: hardware hintsmode:ui,webchat,cli,backend,probe,test,node, ...lastInputSeconds: “seconds since last user input” (if known)reason:self,connect,node-connected,periodic, ...ts: last update timestamp (ms since epoch)
Producers (where presence comes from)
Presence entries are produced by multiple sources and merged.
1) Gateway self entry
The Gateway always seeds a “self” entry at startup so UIs show the gateway host even before any clients connect.
2) WebSocket connect
Every WS client begins with a connect request. On successful handshake the
Gateway upserts a presence entry for that connection.
Why one‑off CLI commands don’t show up
The CLI often connects for short, one‑off commands. To avoid spamming the
Instances list, client.mode === "cli" is not turned into a presence entry.
3) system-event beacons
Clients can send richer periodic beacons via the system-event method. The mac
app uses this to report host name, IP, and lastInputSeconds.
4) Node connects (role: node)
When a node connects over the Gateway WebSocket with role: node, the Gateway
upserts a presence entry for that node (same flow as other WS clients).
Merge + dedupe rules (why instanceId matters)
Presence entries are stored in a single in‑memory map:
- Entries are keyed by a presence key.
- The best key is a stable
instanceId(fromconnect.client.instanceId) that survives restarts. - Keys are case‑insensitive.
If a client reconnects without a stable instanceId, it may show up as a
duplicate row.
TTL and bounded size
Presence is intentionally ephemeral:
- TTL: entries older than 5 minutes are pruned
- Max entries: 200 (oldest dropped first)
This keeps the list fresh and avoids unbounded memory growth.
Remote/tunnel caveat (loopback IPs)
When a client connects over an SSH tunnel / local port forward, the Gateway may
see the remote address as 127.0.0.1. To avoid overwriting a good client‑reported
IP, loopback remote addresses are ignored.
Consumers
macOS Instances tab
The macOS app renders the output of system-presence and applies a small status
indicator (Active/Idle/Stale) based on the age of the last update.
Debugging tips
- To see the raw list, call
system-presenceagainst the Gateway. - If you see duplicates:
- confirm clients send a stable
client.instanceIdin the handshake - confirm periodic beacons use the same
instanceId - check whether the connection‑derived entry is missing
instanceId(duplicates are expected)
- confirm clients send a stable
Command Queue
Source: https://docs.openclaw.ai/concepts/queue
Command Queue (2026-01-16)
We serialize inbound auto-reply runs (all channels) through a tiny in-process queue to prevent multiple agent runs from colliding, while still allowing safe parallelism across sessions.
Why
- Auto-reply runs can be expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
How it works
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8).
runEmbeddedPiAgentenqueues by session key (lanesession:<key>) to guarantee only one active run per session.- Each session run is then queued into a global lane (
mainby default) so overall parallelism is capped byagents.defaults.maxConcurrent. - When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting.
- Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn.
Queue modes (per channel)
Inbound messages can steer the current run, wait for a followup turn, or do both:
steer: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.followup: enqueue for the next agent turn after the current run ends.collect: coalesce all queued messages into a single followup turn (default). If messages target different channels/threads, they drain individually to preserve routing.steer-backlog(akasteer+backlog): steer now and preserve the message for a followup turn.interrupt(legacy): abort the active run for that session, then run the newest message.queue(legacy alias): same assteer.
Steer-backlog means you can get a followup response after the steered run, so
streaming surfaces can look like duplicates. Prefer collect/steer if you want
one response per inbound message.
Send /queue collect as a standalone command (per-session) or set messages.queue.byChannel.discord: "collect".
Defaults (when unset in config):
- All surfaces →
collect
Configure globally or per channel via messages.queue:
{
messages: {
queue: {
mode: "collect",
debounceMs: 1000,
cap: 20,
drop: "summarize",
byChannel: { discord: "collect" },
},
},
}
Queue options
Options apply to followup, collect, and steer-backlog (and to steer when it falls back to followup):
debounceMs: wait for quiet before starting a followup turn (prevents “continue, continue”).cap: max queued messages per session.drop: overflow policy (old,new,summarize).
Summarize keeps a short bullet list of dropped messages and injects it as a synthetic followup prompt.
Defaults: debounceMs: 1000, cap: 20, drop: summarize.
Per-session overrides
- Send
/queue <mode>as a standalone command to store the mode for the current session. - Options can be combined:
/queue collect debounce:2s cap:25 drop:summarize /queue defaultor/queue resetclears the session override.
Scope and guarantees
- Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.).
- Default lane (
main) is process-wide for inbound + main heartbeats; setagents.defaults.maxConcurrentto allow multiple sessions in parallel. - Additional lanes may exist (e.g.
cron,subagent) so background jobs can run in parallel without blocking inbound replies. - Per-session lanes guarantee that only one agent run touches a given session at a time.
- No external dependencies or background worker threads; pure TypeScript + promises.
Troubleshooting
- If commands seem stuck, enable verbose logs and look for “queued for …ms” lines to confirm the queue is draining.
- If you need queue depth, enable verbose logs and watch for queue timing lines.
Retry Policy
Source: https://docs.openclaw.ai/concepts/retry
Retry policy
Goals
- Retry per HTTP request, not per multi-step flow.
- Preserve ordering by retrying only the current step.
- Avoid duplicating non-idempotent operations.
Defaults
- Attempts: 3
- Max delay cap: 30000 ms
- Jitter: 0.1 (10 percent)
- Provider defaults:
- Telegram min delay: 400 ms
- Discord min delay: 500 ms
Behavior
Discord
- Retries only on rate-limit errors (HTTP 429).
- Uses Discord
retry_afterwhen available, otherwise exponential backoff.
Telegram
- Retries on transient errors (429, timeout, connect/reset/closed, temporarily unavailable).
- Uses
retry_afterwhen available, otherwise exponential backoff. - Markdown parse errors are not retried; they fall back to plain text.
Configuration
Set retry policy per provider in ~/.openclaw/openclaw.json:
{
channels: {
telegram: {
retry: {
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1,
},
},
discord: {
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1,
},
},
},
}
Notes
- Retries apply per request (message send, media upload, reaction, poll, sticker).
- Composite flows do not retry completed steps.
Session Management
Source: https://docs.openclaw.ai/concepts/session
Session Management
OpenClaw treats one direct-chat session per agent as primary. Direct chats collapse to agent:<agentId>:<mainKey> (default main), while group/channel chats get their own keys. session.mainKey is honored.
Use session.dmScope to control how direct messages are grouped:
main(default): all DMs share the main session for continuity.per-peer: isolate by sender id across channels.per-channel-peer: isolate by channel + sender (recommended for multi-user inboxes).per-account-channel-peer: isolate by account + channel + sender (recommended for multi-account inboxes). Usesession.identityLinksto map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when usingper-peer,per-channel-peer, orper-account-channel-peer.
Secure DM mode (recommended)
If your agent can receive DMs from multiple people (pairing approvals for more than one sender, a DM allowlist with multiple entries, or dmPolicy: "open"), enable secure DM mode to avoid cross-user context leakage:
// ~/.openclaw/openclaw.json
{
session: {
// Secure DM mode: isolate DM context per channel + sender.
dmScope: "per-channel-peer",
},
}
Notes:
- Default is
dmScope: "main"for continuity (all DMs share the main session). - For multi-account inboxes on the same channel, prefer
per-account-channel-peer. - If the same person contacts you on multiple channels, use
session.identityLinksto collapse their DM sessions into one canonical identity.
Gateway is the source of truth
All session state is owned by the gateway (the “master” OpenClaw). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
- In remote mode, the session store you care about lives on the remote gateway host, not your Mac.
- Token counts shown in UIs come from the gateway’s store fields (
inputTokens,outputTokens,totalTokens,contextTokens). Clients do not parse JSONL transcripts to “fix up” totals.
Where state lives
- On the gateway host:
- Store file:
~/.openclaw/agents/<agentId>/sessions/sessions.json(per agent).
- Store file:
- Transcripts:
~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl(Telegram topic sessions use.../<SessionId>-topic-<threadId>.jsonl). - The store is a map
sessionKey -> { sessionId, updatedAt, ... }. Deleting entries is safe; they are recreated on demand. - Group entries may include
displayName,channel,subject,room, andspaceto label sessions in UIs. - Session entries include
originmetadata (label + routing hints) so UIs can explain where a session came from. - OpenClaw does not read legacy Pi/Tau session folders.
Session pruning
OpenClaw trims old tool results from the in-memory context right before LLM calls by default. This does not rewrite JSONL history. See /concepts/session-pruning.
Pre-compaction memory flush
When a session nears auto-compaction, OpenClaw can run a silent memory flush turn that reminds the model to write durable notes to disk. This only runs when the workspace is writable. See Memory and Compaction.
Mapping transports → session keys
- Direct chats follow
session.dmScope(defaultmain).main:agent:<agentId>:<mainKey>(continuity across devices/channels).- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
per-peer:agent:<agentId>:dm:<peerId>.per-channel-peer:agent:<agentId>:<channel>:dm:<peerId>.per-account-channel-peer:agent:<agentId>:<channel>:<accountId>:dm:<peerId>(accountId defaults todefault).- If
session.identityLinksmatches a provider-prefixed peer id (for exampletelegram:123), the canonical key replaces<peerId>so the same person shares a session across channels.
- Group chats isolate state:
agent:<agentId>:<channel>:group:<id>(rooms/channels useagent:<agentId>:<channel>:channel:<id>).- Telegram forum topics append
:topic:<threadId>to the group id for isolation. - Legacy
group:<id>keys are still recognized for migration.
- Telegram forum topics append
- Inbound contexts may still use
group:<id>; the channel is inferred fromProviderand normalized to the canonicalagent:<agentId>:<channel>:group:<id>form. - Other sources:
- Cron jobs:
cron:<job.id> - Webhooks:
hook:<uuid>(unless explicitly set by the hook) - Node runs:
node-<nodeId>
- Cron jobs:
Lifecycle
- Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message.
- Daily reset: defaults to 4:00 AM local time on the gateway host. A session is stale once its last update is earlier than the most recent daily reset time.
- Idle reset (optional):
idleMinutesadds a sliding idle window. When both daily and idle resets are configured, whichever expires first forces a new session. - Legacy idle-only: if you set
session.idleMinuteswithout anysession.reset/resetByTypeconfig, OpenClaw stays in idle-only mode for backward compatibility. - Per-type overrides (optional):
resetByTypelets you override the policy fordm,group, andthreadsessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector). - Per-channel overrides (optional):
resetByChanneloverrides the reset policy for a channel (applies to all session types for that channel and takes precedence overreset/resetByType). - Reset triggers: exact
/newor/reset(plus any extras inresetTriggers) start a fresh session id and pass the remainder of the message through./new <model>accepts a model alias,provider/model, or provider name (fuzzy match) to set the new session model. If/newor/resetis sent alone, OpenClaw runs a short “hello” greeting turn to confirm the reset. - Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
- Isolated cron jobs always mint a fresh
sessionIdper run (no idle reuse).
Send policy (optional)
Block delivery for specific session types without listing individual ids.
{
session: {
sendPolicy: {
rules: [
{ action: "deny", match: { channel: "discord", chatType: "group" } },
{ action: "deny", match: { keyPrefix: "cron:" } },
],
default: "allow",
},
},
}
Runtime override (owner only):
/send on→ allow for this session/send off→ deny for this session/send inherit→ clear override and use config rules Send these as standalone messages so they register.
Configuration (optional rename example)
// ~/.openclaw/openclaw.json
{
session: {
scope: "per-sender", // keep group keys separate
dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes)
identityLinks: {
alice: ["telegram:123456789", "discord:987654321012345678"],
},
reset: {
// Defaults: mode=daily, atHour=4 (gateway host local time).
// If you also set idleMinutes, whichever expires first wins.
mode: "daily",
atHour: 4,
idleMinutes: 120,
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
dm: { mode: "idle", idleMinutes: 240 },
group: { mode: "idle", idleMinutes: 120 },
},
resetByChannel: {
discord: { mode: "idle", idleMinutes: 10080 },
},
resetTriggers: ["/new", "/reset"],
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
mainKey: "main",
},
}
Inspecting
openclaw status— shows store path and recent sessions.openclaw sessions --json— dumps every entry (filter with--active <minutes>).openclaw gateway call sessions.list --params '{}'— fetch sessions from the running gateway (use--url/--tokenfor remote gateway access).- Send
/statusas a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs). - Send
/context listor/context detailto see what’s in the system prompt and injected workspace files (and the biggest context contributors). - Send
/stopas a standalone message to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count). - Send
/compact(optional instructions) as a standalone message to summarize older context and free up window space. See /concepts/compaction. - JSONL transcripts can be opened directly to review full turns.
Tips
- Keep the primary key dedicated to 1:1 traffic; let groups keep their own keys.
- When automating cleanup, delete individual keys instead of the whole store to preserve context elsewhere.
Session origin metadata
Each session entry records where it came from (best-effort) in origin:
label: human label (resolved from conversation label + group subject/channel)provider: normalized channel id (including extensions)from/to: raw routing ids from the inbound envelopeaccountId: provider account id (when multi-account)threadId: thread/topic id when the channel supports it The origin fields are populated for direct messages, channels, and groups. If a connector only updates delivery routing (for example, to keep a DM main session fresh), it should still provide inbound context so the session keeps its explainer metadata. Extensions can do this by sendingConversationLabel,GroupSubject,GroupChannel,GroupSpace, andSenderNamein the inbound context and callingrecordSessionMetaFromInbound(or passing the same context toupdateLastRoute).
null
Source: https://docs.openclaw.ai/concepts/session-pruning
Session Pruning
Session pruning trims old tool results from the in-memory context right before each LLM call. It does not rewrite the on-disk session history (*.jsonl).
When it runs
- When
mode: "cache-ttl"is enabled and the last Anthropic call for the session is older thanttl. - Only affects the messages sent to the model for that request.
- Only active for Anthropic API calls (and OpenRouter Anthropic models).
- For best results, match
ttlto your modelcacheControlTtl. - After a prune, the TTL window resets so subsequent requests keep cache until
ttlexpires again.
Smart defaults (Anthropic)
- OAuth or setup-token profiles: enable
cache-ttlpruning and set heartbeat to1h. - API key profiles: enable
cache-ttlpruning, set heartbeat to30m, and defaultcacheControlTtlto1hon Anthropic models. - If you set any of these values explicitly, OpenClaw does not override them.
What this improves (cost + cache behavior)
- Why prune: Anthropic prompt caching only applies within the TTL. If a session goes idle past the TTL, the next request re-caches the full prompt unless you trim it first.
- What gets cheaper: pruning reduces the cacheWrite size for that first request after the TTL expires.
- Why the TTL reset matters: once pruning runs, the cache window resets, so follow‑up requests can reuse the freshly cached prompt instead of re-caching the full history again.
- What it does not do: pruning doesn’t add tokens or “double” costs; it only changes what gets cached on that first post‑TTL request.
What can be pruned
- Only
toolResultmessages. - User + assistant messages are never modified.
- The last
keepLastAssistantsassistant messages are protected; tool results after that cutoff are not pruned. - If there aren’t enough assistant messages to establish the cutoff, pruning is skipped.
- Tool results containing image blocks are skipped (never trimmed/cleared).
Context window estimation
Pruning uses an estimated context window (chars ≈ tokens × 4). The base window is resolved in this order:
models.providers.*.models[].contextWindowoverride.- Model definition
contextWindow(from the model registry). - Default
200000tokens.
If agents.defaults.contextTokens is set, it is treated as a cap (min) on the resolved window.
Mode
cache-ttl
- Pruning only runs if the last Anthropic call is older than
ttl(default5m). - When it runs: same soft-trim + hard-clear behavior as before.
Soft vs hard pruning
- Soft-trim: only for oversized tool results.
- Keeps head + tail, inserts
..., and appends a note with the original size. - Skips results with image blocks.
- Keeps head + tail, inserts
- Hard-clear: replaces the entire tool result with
hardClear.placeholder.
Tool selection
tools.allow/tools.denysupport*wildcards.- Deny wins.
- Matching is case-insensitive.
- Empty allow list => all tools allowed.
Interaction with other limits
- Built-in tools already truncate their own output; session pruning is an extra layer that prevents long-running chats from accumulating too much tool output in the model context.
- Compaction is separate: compaction summarizes and persists, pruning is transient per request. See /concepts/compaction.
Defaults (when enabled)
ttl:"5m"keepLastAssistants:3softTrimRatio:0.3hardClearRatio:0.5minPrunableToolChars:50000softTrim:{ maxChars: 4000, headChars: 1500, tailChars: 1500 }hardClear:{ enabled: true, placeholder: "[Old tool result content cleared]" }
Examples
Default (off):
{
agent: {
contextPruning: { mode: "off" },
},
}
Enable TTL-aware pruning:
{
agent: {
contextPruning: { mode: "cache-ttl", ttl: "5m" },
},
}
Restrict pruning to specific tools:
{
agent: {
contextPruning: {
mode: "cache-ttl",
tools: { allow: ["exec", "read"], deny: ["*image*"] },
},
},
}
See config reference: Gateway Configuration
Session Tools
Source: https://docs.openclaw.ai/concepts/session-tool
Session Tools
Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session.
Tool Names
sessions_listsessions_historysessions_sendsessions_spawn
Key Model
- Main direct chat bucket is always the literal key
"main"(resolved to the current agent’s main key). - Group chats use
agent:<agentId>:<channel>:group:<id>oragent:<agentId>:<channel>:channel:<id>(pass the full key). - Cron jobs use
cron:<job.id>. - Hooks use
hook:<uuid>unless explicitly set. - Node sessions use
node-<nodeId>unless explicitly set.
global and unknown are reserved values and are never listed. If session.scope = "global", we alias it to main for all tools so callers never see global.
sessions_list
List sessions as an array of rows.
Parameters:
kinds?: string[]filter: any of"main" | "group" | "cron" | "hook" | "node" | "other"limit?: numbermax rows (default: server default, clamp e.g. 200)activeMinutes?: numberonly sessions updated within N minutesmessageLimit?: number0 = no messages (default 0); >0 = include last N messages
Behavior:
messageLimit > 0fetcheschat.historyper session and includes the last N messages.- Tool results are filtered out in list output; use
sessions_historyfor tool messages. - When running in a sandboxed agent session, session tools default to spawned-only visibility (see below).
Row shape (JSON):
key: session key (string)kind:main | group | cron | hook | node | otherchannel:whatsapp | telegram | discord | signal | imessage | webchat | internal | unknowndisplayName(group display label if available)updatedAt(ms)sessionIdmodel,contextTokens,totalTokensthinkingLevel,verboseLevel,systemSent,abortedLastRunsendPolicy(session override if set)lastChannel,lastTodeliveryContext(normalized{ channel, to, accountId }when available)transcriptPath(best-effort path derived from store dir + sessionId)messages?(only whenmessageLimit > 0)
sessions_history
Fetch transcript for one session.
Parameters:
sessionKey(required; accepts session key orsessionIdfromsessions_list)limit?: numbermax messages (server clamps)includeTools?: boolean(default false)
Behavior:
includeTools=falsefiltersrole: "toolResult"messages.- Returns messages array in the raw transcript format.
- When given a
sessionId, OpenClaw resolves it to the corresponding session key (missing ids error).
sessions_send
Send a message into another session.
Parameters:
sessionKey(required; accepts session key orsessionIdfromsessions_list)message(required)timeoutSeconds?: number(default >0; 0 = fire-and-forget)
Behavior:
timeoutSeconds = 0: enqueue and return{ runId, status: "accepted" }.timeoutSeconds > 0: wait up to N seconds for completion, then return{ runId, status: "ok", reply }.- If wait times out:
{ runId, status: "timeout", error }. Run continues; callsessions_historylater. - If the run fails:
{ runId, status: "error", error }. - Announce delivery runs after the primary run completes and is best-effort;
status: "ok"does not guarantee the announce was delivered. - Waits via gateway
agent.wait(server-side) so reconnects don't drop the wait. - Agent-to-agent message context is injected for the primary run.
- After the primary run completes, OpenClaw runs a reply-back loop:
- Round 2+ alternates between requester and target agents.
- Reply exactly
REPLY_SKIPto stop the ping‑pong. - Max turns is
session.agentToAgent.maxPingPongTurns(0–5, default 5).
- Once the loop ends, OpenClaw runs the agent‑to‑agent announce step (target agent only):
- Reply exactly
ANNOUNCE_SKIPto stay silent. - Any other reply is sent to the target channel.
- Announce step includes the original request + round‑1 reply + latest ping‑pong reply.
- Reply exactly
Channel Field
- For groups,
channelis the channel recorded on the session entry. - For direct chats,
channelmaps fromlastChannel. - For cron/hook/node,
channelisinternal. - If missing,
channelisunknown.
Security / Send Policy
Policy-based blocking by channel/chat type (not per session id).
{
"session": {
"sendPolicy": {
"rules": [
{
"match": { "channel": "discord", "chatType": "group" },
"action": "deny"
}
],
"default": "allow"
}
}
}
Runtime override (per session entry):
sendPolicy: "allow" | "deny"(unset = inherit config)- Settable via
sessions.patchor owner-only/send on|off|inherit(standalone message).
Enforcement points:
chat.send/agent(gateway)- auto-reply delivery logic
sessions_spawn
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat channel.
Parameters:
task(required)label?(optional; used for logs/UI)agentId?(optional; spawn under another agent id if allowed)model?(optional; overrides the sub-agent model; invalid values error)runTimeoutSeconds?(default 0; when set, aborts the sub-agent run after N seconds)cleanup?(delete|keep, defaultkeep)
Allowlist:
agents.list[].subagents.allowAgents: list of agent ids allowed viaagentId(["*"]to allow any). Default: only the requester agent.
Discovery:
- Use
agents_listto discover which agent ids are allowed forsessions_spawn.
Behavior:
- Starts a new
agent:<agentId>:subagent:<uuid>session withdeliver: false. - Sub-agents default to the full tool set minus session tools (configurable via
tools.subagents.tools). - Sub-agents are not allowed to call
sessions_spawn(no sub-agent → sub-agent spawning). - Always non-blocking: returns
{ status: "accepted", runId, childSessionKey }immediately. - After completion, OpenClaw runs a sub-agent announce step and posts the result to the requester chat channel.
- Reply exactly
ANNOUNCE_SKIPduring the announce step to stay silent. - Announce replies are normalized to
Status/Result/Notes;Statuscomes from runtime outcome (not model text). - Sub-agent sessions are auto-archived after
agents.defaults.subagents.archiveAfterMinutes(default: 60). - Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
Sandbox Session Visibility
Sandboxed sessions can use session tools, but by default they only see sessions they spawned via sessions_spawn.
Config:
{
agents: {
defaults: {
sandbox: {
// default: "spawned"
sessionToolsVisibility: "spawned", // or "all"
},
},
},
}
Sessions
Source: https://docs.openclaw.ai/concepts/sessions
Sessions
Canonical session management docs live in Session management.
Streaming and Chunking
Source: https://docs.openclaw.ai/concepts/streaming
Streaming + chunking
OpenClaw has two separate “streaming” layers:
- Block streaming (channels): emit completed blocks as the assistant writes. These are normal channel messages (not token deltas).
- Token-ish streaming (Telegram only): update a draft bubble with partial text while generating; final message is sent at the end.
There is no real token streaming to external channel messages today. Telegram draft streaming is the only partial-stream surface.
Block streaming (channel messages)
Block streaming sends assistant output in coarse chunks as it becomes available.
Model output
└─ text_delta/events
├─ (blockStreamingBreak=text_end)
│ └─ chunker emits blocks as buffer grows
└─ (blockStreamingBreak=message_end)
└─ chunker flushes at message_end
└─ channel send (block replies)
Legend:
text_delta/events: model stream events (may be sparse for non-streaming models).chunker:EmbeddedBlockChunkerapplying min/max bounds + break preference.channel send: actual outbound messages (block replies).
Controls:
agents.defaults.blockStreamingDefault:"on"/"off"(default off).- Channel overrides:
*.blockStreaming(and per-account variants) to force"on"/"off"per channel. agents.defaults.blockStreamingBreak:"text_end"or"message_end".agents.defaults.blockStreamingChunk:{ minChars, maxChars, breakPreference? }.agents.defaults.blockStreamingCoalesce:{ minChars?, maxChars?, idleMs? }(merge streamed blocks before send).- Channel hard cap:
*.textChunkLimit(e.g.,channels.whatsapp.textChunkLimit). - Channel chunk mode:
*.chunkMode(lengthdefault,newlinesplits on blank lines (paragraph boundaries) before length chunking). - Discord soft cap:
channels.discord.maxLinesPerMessage(default 17) splits tall replies to avoid UI clipping.
Boundary semantics:
text_end: stream blocks as soon as chunker emits; flush on eachtext_end.message_end: wait until assistant message finishes, then flush buffered output.
message_end still uses the chunker if the buffered text exceeds maxChars, so it can emit multiple chunks at the end.
Chunking algorithm (low/high bounds)
Block chunking is implemented by EmbeddedBlockChunker:
- Low bound: don’t emit until buffer >=
minChars(unless forced). - High bound: prefer splits before
maxChars; if forced, split atmaxChars. - Break preference:
paragraph→newline→sentence→whitespace→ hard break. - Code fences: never split inside fences; when forced at
maxChars, close + reopen the fence to keep Markdown valid.
maxChars is clamped to the channel textChunkLimit, so you can’t exceed per-channel caps.
Coalescing (merge streamed blocks)
When block streaming is enabled, OpenClaw can merge consecutive block chunks before sending them out. This reduces “single-line spam” while still providing progressive output.
- Coalescing waits for idle gaps (
idleMs) before flushing. - Buffers are capped by
maxCharsand will flush if they exceed it. minCharsprevents tiny fragments from sending until enough text accumulates (final flush always sends remaining text).- Joiner is derived from
blockStreamingChunk.breakPreference(paragraph→\n\n,newline→\n,sentence→ space). - Channel overrides are available via
*.blockStreamingCoalesce(including per-account configs). - Default coalesce
minCharsis bumped to 1500 for Signal/Slack/Discord unless overridden.
Human-like pacing between blocks
When block streaming is enabled, you can add a randomized pause between block replies (after the first block). This makes multi-bubble responses feel more natural.
- Config:
agents.defaults.humanDelay(override per agent viaagents.list[].humanDelay). - Modes:
off(default),natural(800–2500ms),custom(minMs/maxMs). - Applies only to block replies, not final replies or tool summaries.
“Stream chunks or everything”
This maps to:
- Stream chunks:
blockStreamingDefault: "on"+blockStreamingBreak: "text_end"(emit as you go). Non-Telegram channels also need*.blockStreaming: true. - Stream everything at end:
blockStreamingBreak: "message_end"(flush once, possibly multiple chunks if very long). - No block streaming:
blockStreamingDefault: "off"(only final reply).
Channel note: For non-Telegram channels, block streaming is off unless
*.blockStreaming is explicitly set to true. Telegram can stream drafts
(channels.telegram.streamMode) without block replies.
Config location reminder: the blockStreaming* defaults live under
agents.defaults, not the root config.
Telegram draft streaming (token-ish)
Telegram is the only channel with draft streaming:
- Uses Bot API
sendMessageDraftin private chats with topics. channels.telegram.streamMode: "partial" | "block" | "off".partial: draft updates with the latest stream text.block: draft updates in chunked blocks (same chunker rules).off: no draft streaming.
- Draft chunk config (only for
streamMode: "block"):channels.telegram.draftChunk(defaults:minChars: 200,maxChars: 800). - Draft streaming is separate from block streaming; block replies are off by default and only enabled by
*.blockStreaming: trueon non-Telegram channels. - Final reply is still a normal message.
/reasoning streamwrites reasoning into the draft bubble (Telegram only).
When draft streaming is active, OpenClaw disables block streaming for that reply to avoid double-streaming.
Telegram (private + topics)
└─ sendMessageDraft (draft bubble)
├─ streamMode=partial → update latest text
└─ streamMode=block → chunker updates draft
└─ final reply → normal message
Legend:
sendMessageDraft: Telegram draft bubble (not a real message).final reply: normal Telegram message send.
System Prompt
Source: https://docs.openclaw.ai/concepts/system-prompt
System Prompt
OpenClaw builds a custom system prompt for every agent run. The prompt is OpenClaw-owned and does not use the p-coding-agent default prompt.
The prompt is assembled by OpenClaw and injected into each agent run.
Structure
The prompt is intentionally compact and uses fixed sections:
- Tooling: current tool list + short descriptions.
- Safety: short guardrail reminder to avoid power-seeking behavior or bypassing oversight.
- Skills (when available): tells the model how to load skill instructions on demand.
- OpenClaw Self-Update: how to run
config.applyandupdate.run. - Workspace: working directory (
agents.defaults.workspace). - Documentation: local path to OpenClaw docs (repo or npm package) and when to read them.
- Workspace Files (injected): indicates bootstrap files are included below.
- Sandbox (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
- Current Date & Time: user-local time, timezone, and time format.
- Reply Tags: optional reply tag syntax for supported providers.
- Heartbeats: heartbeat prompt and ack behavior.
- Runtime: host, OS, node, model, repo root (when detected), thinking level (one line).
- Reasoning: current visibility level + /reasoning toggle hint.
Safety guardrails in the system prompt are advisory. They guide model behavior but do not enforce policy. Use tool policy, exec approvals, sandboxing, and channel allowlists for hard enforcement; operators can disable these by design.
Prompt modes
OpenClaw can render smaller system prompts for sub-agents. The runtime sets a
promptMode for each run (not a user-facing config):
full(default): includes all sections above.minimal: used for sub-agents; omits Skills, Memory Recall, OpenClaw Self-Update, Model Aliases, User Identity, Reply Tags, Messaging, Silent Replies, and Heartbeats. Tooling, Safety, Workspace, Sandbox, Current Date & Time (when known), Runtime, and injected context stay available.none: returns only the base identity line.
When promptMode=minimal, extra injected prompts are labeled Subagent
Context instead of Group Chat Context.
Workspace bootstrap injection
Bootstrap files are trimmed and appended under Project Context so the model sees identity and profile context without needing explicit reads:
AGENTS.mdSOUL.mdTOOLS.mdIDENTITY.mdUSER.mdHEARTBEAT.mdBOOTSTRAP.md(only on brand-new workspaces)
Large files are truncated with a marker. The max per-file size is controlled by
agents.defaults.bootstrapMaxChars (default: 20000). Missing files inject a
short missing-file marker.
Internal hooks can intercept this step via agent:bootstrap to mutate or replace
the injected bootstrap files (for example swapping SOUL.md for an alternate persona).
To inspect how much each injected file contributes (raw vs injected, truncation, plus tool schema overhead), use /context list or /context detail. See Context.
Time handling
The system prompt includes a dedicated Current Date & Time section when the user timezone is known. To keep the prompt cache-stable, it now only includes the time zone (no dynamic clock or time format).
Use session_status when the agent needs the current time; the status card
includes a timestamp line.
Configure with:
agents.defaults.userTimezoneagents.defaults.timeFormat(auto|12|24)
See Date & Time for full behavior details.
Skills
When eligible skills exist, OpenClaw injects a compact available skills list
(formatSkillsForPrompt) that includes the file path for each skill. The
prompt instructs the model to use read to load the SKILL.md at the listed
location (workspace, managed, or bundled). If no skills are eligible, the
Skills section is omitted.
<available_skills>
<skill>
<name>...</name>
<description>...</description>
<location>...</location>
</skill>
</available_skills>
This keeps the base prompt small while still enabling targeted skill usage.
Documentation
When available, the system prompt includes a Documentation section that points to the
local OpenClaw docs directory (either docs/ in the repo workspace or the bundled npm
package docs) and also notes the public mirror, source repo, community Discord, and
ClawHub (https://clawhub.com) for skills discovery. The prompt instructs the model to consult local docs first
for OpenClaw behavior, commands, configuration, or architecture, and to run
openclaw status itself when possible (asking the user only when it lacks access).
Hooks
Source: https://docs.openclaw.ai/hooks
Hooks
Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in OpenClaw.
Getting Oriented
Hooks are small scripts that run when something happens. There are two kinds:
- Hooks (this page): run inside the Gateway when agent events fire, like
/new,/reset,/stop, or lifecycle events. - Webhooks: external HTTP webhooks that let other systems trigger work in OpenClaw. See Webhook Hooks or use
openclaw webhooksfor Gmail helper commands.
Hooks can also be bundled inside plugins; see Plugins.
Common uses:
- Save a memory snapshot when you reset a session
- Keep an audit trail of commands for troubleshooting or compliance
- Trigger follow-up automation when a session starts or ends
- Write files into the agent workspace or call external APIs when events fire
If you can write a small TypeScript function, you can write a hook. Hooks are discovered automatically, and you enable or disable them via the CLI.
Overview
The hooks system allows you to:
- Save session context to memory when
/newis issued - Log all commands for auditing
- Trigger custom automations on agent lifecycle events
- Extend OpenClaw's behavior without modifying core code
Getting Started
Bundled Hooks
OpenClaw ships with four bundled hooks that are automatically discovered:
- 💾 session-memory: Saves session context to your agent workspace (default
~/.openclaw/workspace/memory/) when you issue/new - 📝 command-logger: Logs all command events to
~/.openclaw/logs/commands.log - 🚀 boot-md: Runs
BOOT.mdwhen the gateway starts (requires internal hooks enabled) - 😈 soul-evil: Swaps injected
SOUL.mdcontent withSOUL_EVIL.mdduring a purge window or by random chance
List available hooks:
openclaw hooks list
Enable a hook:
openclaw hooks enable session-memory
Check hook status:
openclaw hooks check
Get detailed information:
openclaw hooks info session-memory
Onboarding
During onboarding (openclaw onboard), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection.
Hook Discovery
Hooks are automatically discovered from three directories (in order of precedence):
- Workspace hooks:
<workspace>/hooks/(per-agent, highest precedence) - Managed hooks:
~/.openclaw/hooks/(user-installed, shared across workspaces) - Bundled hooks:
<openclaw>/dist/hooks/bundled/(shipped with OpenClaw)
Managed hook directories can be either a single hook or a hook pack (package directory).
Each hook is a directory containing:
my-hook/
├── HOOK.md # Metadata + documentation
└── handler.ts # Handler implementation
Hook Packs (npm/archives)
Hook packs are standard npm packages that export one or more hooks via openclaw.hooks in
package.json. Install them with:
openclaw hooks install <path-or-spec>
Example package.json:
{
"name": "@acme/my-hooks",
"version": "0.1.0",
"openclaw": {
"hooks": ["./hooks/my-hook", "./hooks/other-hook"]
}
}
Each entry points to a hook directory containing HOOK.md and handler.ts (or index.ts).
Hook packs can ship dependencies; they will be installed under ~/.openclaw/hooks/<id>.
Hook Structure
HOOK.md Format
The HOOK.md file contains metadata in YAML frontmatter plus Markdown documentation:
---
name: my-hook
description: "Short description of what this hook does"
homepage: https://docs.openclaw.ai/hooks#my-hook
metadata:
{ "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---
# My Hook
Detailed documentation goes here...
## What It Does
- Listens for `/new` commands
- Performs some action
- Logs the result
## Requirements
- Node.js must be installed
## Configuration
No configuration needed.
Metadata Fields
The metadata.openclaw object supports:
emoji: Display emoji for CLI (e.g.,"💾")events: Array of events to listen for (e.g.,["command:new", "command:reset"])export: Named export to use (defaults to"default")homepage: Documentation URLrequires: Optional requirementsbins: Required binaries on PATH (e.g.,["git", "node"])anyBins: At least one of these binaries must be presentenv: Required environment variablesconfig: Required config paths (e.g.,["workspace.dir"])os: Required platforms (e.g.,["darwin", "linux"])
always: Bypass eligibility checks (boolean)install: Installation methods (for bundled hooks:[{"id":"bundled","kind":"bundled"}])
Handler Implementation
The handler.ts file exports a HookHandler function:
import type { HookHandler } from "../../src/hooks/hooks.js";
const myHandler: HookHandler = async (event) => {
// Only trigger on 'new' command
if (event.type !== "command" || event.action !== "new") {
return;
}
console.log(`[my-hook] New command triggered`);
console.log(` Session: ${event.sessionKey}`);
console.log(` Timestamp: ${event.timestamp.toISOString()}`);
// Your custom logic here
// Optionally send message to user
event.messages.push("✨ My hook executed!");
};
export default myHandler;
Event Context
Each event includes:
{
type: 'command' | 'session' | 'agent' | 'gateway',
action: string, // e.g., 'new', 'reset', 'stop'
sessionKey: string, // Session identifier
timestamp: Date, // When the event occurred
messages: string[], // Push messages here to send to user
context: {
sessionEntry?: SessionEntry,
sessionId?: string,
sessionFile?: string,
commandSource?: string, // e.g., 'whatsapp', 'telegram'
senderId?: string,
workspaceDir?: string,
bootstrapFiles?: WorkspaceBootstrapFile[],
cfg?: OpenClawConfig
}
}
Event Types
Command Events
Triggered when agent commands are issued:
command: All command events (general listener)command:new: When/newcommand is issuedcommand:reset: When/resetcommand is issuedcommand:stop: When/stopcommand is issued
Agent Events
agent:bootstrap: Before workspace bootstrap files are injected (hooks may mutatecontext.bootstrapFiles)
Gateway Events
Triggered when the gateway starts:
gateway:startup: After channels start and hooks are loaded
Tool Result Hooks (Plugin API)
These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before OpenClaw persists them.
tool_result_persist: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload orundefinedto keep it as-is. See Agent Loop.
Future Events
Planned event types:
session:start: When a new session beginssession:end: When a session endsagent:error: When an agent encounters an errormessage:sent: When a message is sentmessage:received: When a message is received
Creating Custom Hooks
1. Choose Location
- Workspace hooks (
<workspace>/hooks/): Per-agent, highest precedence - Managed hooks (
~/.openclaw/hooks/): Shared across workspaces
2. Create Directory Structure
mkdir -p ~/.openclaw/hooks/my-hook
cd ~/.openclaw/hooks/my-hook
3. Create HOOK.md
---
name: my-hook
description: "Does something useful"
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
---
# My Custom Hook
This hook does something useful when you issue `/new`.
4. Create handler.ts
import type { HookHandler } from "../../src/hooks/hooks.js";
const handler: HookHandler = async (event) => {
if (event.type !== "command" || event.action !== "new") {
return;
}
console.log("[my-hook] Running!");
// Your logic here
};
export default handler;
5. Enable and Test
# Verify hook is discovered
openclaw hooks list
# Enable it
openclaw hooks enable my-hook
# Restart your gateway process (menu bar app restart on macOS, or restart your dev process)
# Trigger the event
# Send /new via your messaging channel
Configuration
New Config Format (Recommended)
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"session-memory": { "enabled": true },
"command-logger": { "enabled": false }
}
}
}
}
Per-Hook Configuration
Hooks can have custom configuration:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"my-hook": {
"enabled": true,
"env": {
"MY_CUSTOM_VAR": "value"
}
}
}
}
}
}
Extra Directories
Load hooks from additional directories:
{
"hooks": {
"internal": {
"enabled": true,
"load": {
"extraDirs": ["/path/to/more/hooks"]
}
}
}
}
Legacy Config Format (Still Supported)
The old config format still works for backwards compatibility:
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts",
"export": "default"
}
]
}
}
}
Migration: Use the new discovery-based system for new hooks. Legacy handlers are loaded after directory-based hooks.
CLI Commands
List Hooks
# List all hooks
openclaw hooks list
# Show only eligible hooks
openclaw hooks list --eligible
# Verbose output (show missing requirements)
openclaw hooks list --verbose
# JSON output
openclaw hooks list --json
Hook Information
# Show detailed info about a hook
openclaw hooks info session-memory
# JSON output
openclaw hooks info session-memory --json
Check Eligibility
# Show eligibility summary
openclaw hooks check
# JSON output
openclaw hooks check --json
Enable/Disable
# Enable a hook
openclaw hooks enable session-memory
# Disable a hook
openclaw hooks disable command-logger
Bundled Hooks
session-memory
Saves session context to memory when you issue /new.
Events: command:new
Requirements: workspace.dir must be configured
Output: <workspace>/memory/YYYY-MM-DD-slug.md (defaults to ~/.openclaw/workspace)
What it does:
- Uses the pre-reset session entry to locate the correct transcript
- Extracts the last 15 lines of conversation
- Uses LLM to generate a descriptive filename slug
- Saves session metadata to a dated memory file
Example output:
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main
- **Session ID**: abc123def456
- **Source**: telegram
Filename examples:
2026-01-16-vendor-pitch.md2026-01-16-api-design.md2026-01-16-1430.md(fallback timestamp if slug generation fails)
Enable:
openclaw hooks enable session-memory
command-logger
Logs all command events to a centralized audit file.
Events: command
Requirements: None
Output: ~/.openclaw/logs/commands.log
What it does:
- Captures event details (command action, timestamp, session key, sender ID, source)
- Appends to log file in JSONL format
- Runs silently in the background
Example log entries:
{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"}
{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"user@example.com","source":"whatsapp"}
View logs:
# View recent commands
tail -n 20 ~/.openclaw/logs/commands.log
# Pretty-print with jq
cat ~/.openclaw/logs/commands.log | jq .
# Filter by action
grep '"action":"new"' ~/.openclaw/logs/commands.log | jq .
Enable:
openclaw hooks enable command-logger
soul-evil
Swaps injected SOUL.md content with SOUL_EVIL.md during a purge window or by random chance.
Events: agent:bootstrap
Docs: SOUL Evil Hook
Output: No files written; swaps happen in-memory only.
Enable:
openclaw hooks enable soul-evil
Config:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"soul-evil": {
"enabled": true,
"file": "SOUL_EVIL.md",
"chance": 0.1,
"purge": { "at": "21:00", "duration": "15m" }
}
}
}
}
}
boot-md
Runs BOOT.md when the gateway starts (after channels start).
Internal hooks must be enabled for this to run.
Events: gateway:startup
Requirements: workspace.dir must be configured
What it does:
- Reads
BOOT.mdfrom your workspace - Runs the instructions via the agent runner
- Sends any requested outbound messages via the message tool
Enable:
openclaw hooks enable boot-md
Best Practices
Keep Handlers Fast
Hooks run during command processing. Keep them lightweight:
// ✓ Good - async work, returns immediately
const handler: HookHandler = async (event) => {
void processInBackground(event); // Fire and forget
};
// ✗ Bad - blocks command processing
const handler: HookHandler = async (event) => {
await slowDatabaseQuery(event);
await evenSlowerAPICall(event);
};
Handle Errors Gracefully
Always wrap risky operations:
const handler: HookHandler = async (event) => {
try {
await riskyOperation(event);
} catch (err) {
console.error("[my-handler] Failed:", err instanceof Error ? err.message : String(err));
// Don't throw - let other handlers run
}
};
Filter Events Early
Return early if the event isn't relevant:
const handler: HookHandler = async (event) => {
// Only handle 'new' commands
if (event.type !== "command" || event.action !== "new") {
return;
}
// Your logic here
};
Use Specific Event Keys
Specify exact events in metadata when possible:
metadata: { "openclaw": { "events": ["command:new"] } } # Specific
Rather than:
metadata: { "openclaw": { "events": ["command"] } } # General - more overhead
Debugging
Enable Hook Logging
The gateway logs hook loading at startup:
Registered hook: session-memory -> command:new
Registered hook: command-logger -> command
Registered hook: boot-md -> gateway:startup
Check Discovery
List all discovered hooks:
openclaw hooks list --verbose
Check Registration
In your handler, log when it's called:
const handler: HookHandler = async (event) => {
console.log("[my-handler] Triggered:", event.type, event.action);
// Your logic
};
Verify Eligibility
Check why a hook isn't eligible:
openclaw hooks info my-hook
Look for missing requirements in the output.
Testing
Gateway Logs
Monitor gateway logs to see hook execution:
# macOS
./scripts/clawlog.sh -f
# Other platforms
tail -f ~/.openclaw/gateway.log
Test Hooks Directly
Test your handlers in isolation:
import { test } from "vitest";
import { createHookEvent } from "./src/hooks/hooks.js";
import myHandler from "./hooks/my-hook/handler.js";
test("my handler works", async () => {
const event = createHookEvent("command", "new", "test-session", {
foo: "bar",
});
await myHandler(event);
// Assert side effects
});
Architecture
Core Components
src/hooks/types.ts: Type definitionssrc/hooks/workspace.ts: Directory scanning and loadingsrc/hooks/frontmatter.ts: HOOK.md metadata parsingsrc/hooks/config.ts: Eligibility checkingsrc/hooks/hooks-status.ts: Status reportingsrc/hooks/loader.ts: Dynamic module loadersrc/cli/hooks-cli.ts: CLI commandssrc/gateway/server-startup.ts: Loads hooks at gateway startsrc/auto-reply/reply/commands-core.ts: Triggers command events
Discovery Flow
Gateway startup
↓
Scan directories (workspace → managed → bundled)
↓
Parse HOOK.md files
↓
Check eligibility (bins, env, config, os)
↓
Load handlers from eligible hooks
↓
Register handlers for events
Event Flow
User sends /new
↓
Command validation
↓
Create hook event
↓
Trigger hook (all registered handlers)
↓
Command processing continues
↓
Session reset
Troubleshooting
Hook Not Discovered
-
Check directory structure:
ls -la ~/.openclaw/hooks/my-hook/ # Should show: HOOK.md, handler.ts -
Verify HOOK.md format:
cat ~/.openclaw/hooks/my-hook/HOOK.md # Should have YAML frontmatter with name and metadata -
List all discovered hooks:
openclaw hooks list
Hook Not Eligible
Check requirements:
openclaw hooks info my-hook
Look for missing:
- Binaries (check PATH)
- Environment variables
- Config values
- OS compatibility
Hook Not Executing
-
Verify hook is enabled:
openclaw hooks list # Should show ✓ next to enabled hooks -
Restart your gateway process so hooks reload.
-
Check gateway logs for errors:
./scripts/clawlog.sh | grep hook
Handler Errors
Check for TypeScript/import errors:
# Test import directly
node -e "import('./path/to/handler.ts').then(console.log)"
Migration Guide
From Legacy Config to Discovery
Before:
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts"
}
]
}
}
}
After:
-
Create hook directory:
mkdir -p ~/.openclaw/hooks/my-hook mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts -
Create HOOK.md:
--- name: my-hook description: "My custom hook" metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } } --- # My Hook Does something useful. -
Update config:
{ "hooks": { "internal": { "enabled": true, "entries": { "my-hook": { "enabled": true } } } } } -
Verify and restart your gateway process:
openclaw hooks list # Should show: 🎯 my-hook ✓
Benefits of migration:
- Automatic discovery
- CLI management
- Eligibility checking
- Better documentation
- Consistent structure
See Also
SOUL Evil Hook
Source: https://docs.openclaw.ai/hooks/soul-evil
SOUL Evil Hook
The SOUL Evil hook swaps the injected SOUL.md content with SOUL_EVIL.md during
a purge window or by random chance. It does not modify files on disk.
How It Works
When agent:bootstrap runs, the hook can replace the SOUL.md content in memory
before the system prompt is assembled. If SOUL_EVIL.md is missing or empty,
OpenClaw logs a warning and keeps the normal SOUL.md.
Sub-agent runs do not include SOUL.md in their bootstrap files, so this hook
has no effect on sub-agents.
Enable
openclaw hooks enable soul-evil
Then set the config:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"soul-evil": {
"enabled": true,
"file": "SOUL_EVIL.md",
"chance": 0.1,
"purge": { "at": "21:00", "duration": "15m" }
}
}
}
}
}
Create SOUL_EVIL.md in the agent workspace root (next to SOUL.md).
Options
file(string): alternate SOUL filename (default:SOUL_EVIL.md)chance(number 0–1): random chance per run to useSOUL_EVIL.mdpurge.at(HH:mm): daily purge start (24-hour clock)purge.duration(duration): window length (e.g.30s,10m,1h)
Precedence: purge window wins over chance.
Timezone: uses agents.defaults.userTimezone when set; otherwise host timezone.
Notes
- No files are written or modified on disk.
- If
SOUL.mdis not in the bootstrap list, the hook does nothing.
See Also
OpenClaw
Source: https://docs.openclaw.ai/index
OpenClaw 🦞
"EXFOLIATE! EXFOLIATE!" — A space lobster, probably
Any OS gateway for AI agents across WhatsApp, Telegram, Discord, iMessage, and more.
Send a message, get an agent response from your pocket. Plugins add Mattermost and more.
OpenClaw connects chat apps to coding agents like Pi through a single Gateway process. It powers the OpenClaw assistant and supports local or remote setups.
How it works
flowchart LR
A["Chat apps + plugins"] --> B["Gateway"]
B --> C["Pi agent"]
B --> D["CLI"]
B --> E["Web Control UI"]
B --> F["macOS app"]
B --> G["iOS and Android nodes"]
The Gateway is the single source of truth for sessions, routing, and channel connections.
Key capabilities
WhatsApp, Telegram, Discord, and iMessage with a single Gateway process. Add Mattermost and more with extension packages. Isolated sessions per agent, workspace, or sender. Send and receive images, audio, and documents. Browser dashboard for chat, config, sessions, and nodes. Pair iOS and Android nodes with Canvas support.Quick start
```bash theme={null} npm install -g openclaw@latest ``` ```bash theme={null} openclaw onboard --install-daemon ``` ```bash theme={null} openclaw channels login openclaw gateway --port 18789 ```Need the full install and dev setup? See Quick start.
Dashboard
Open the browser Control UI after the Gateway starts.
- Local default: http://127.0.0.1:18789/
- Remote access: Web surfaces and Tailscale
Configuration (optional)
Config lives at ~/.openclaw/openclaw.json.
- If you do nothing, OpenClaw uses the bundled Pi binary in RPC mode with per-sender sessions.
- If you want to lock it down, start with
channels.whatsapp.allowFromand (for groups) mention rules.
Example:
{
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } },
},
},
messages: { groupChat: { mentionPatterns: ["@openclaw"] } },
}
Start here
All docs and guides, organized by use case. Core Gateway settings, tokens, and provider config. SSH and tailnet access patterns. Channel-specific setup for WhatsApp, Telegram, Discord, and more. iOS and Android nodes with pairing and Canvas. Common fixes and troubleshooting entry point.Learn more
Complete channel, routing, and media capabilities. Workspace isolation and per-agent sessions. Tokens, allowlists, and safety controls. Gateway diagnostics and common errors. Project origins, contributors, and license.Ansible
Source: https://docs.openclaw.ai/install/ansible
Ansible Installation
The recommended way to deploy OpenClaw to production servers is via openclaw-ansible — an automated installer with security-first architecture.
Quick Start
One-command install:
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
📦 Full guide: github.com/openclaw/openclaw-ansible
The openclaw-ansible repo is the source of truth for Ansible deployment. This page is a quick overview.
What You Get
- 🔒 Firewall-first security: UFW + Docker isolation (only SSH + Tailscale accessible)
- 🔐 Tailscale VPN: Secure remote access without exposing services publicly
- 🐳 Docker: Isolated sandbox containers, localhost-only bindings
- 🛡️ Defense in depth: 4-layer security architecture
- 🚀 One-command setup: Complete deployment in minutes
- 🔧 Systemd integration: Auto-start on boot with hardening
Requirements
- OS: Debian 11+ or Ubuntu 20.04+
- Access: Root or sudo privileges
- Network: Internet connection for package installation
- Ansible: 2.14+ (installed automatically by quick-start script)
What Gets Installed
The Ansible playbook installs and configures:
- Tailscale (mesh VPN for secure remote access)
- UFW firewall (SSH + Tailscale ports only)
- Docker CE + Compose V2 (for agent sandboxes)
- Node.js 22.x + pnpm (runtime dependencies)
- OpenClaw (host-based, not containerized)
- Systemd service (auto-start with security hardening)
Note: The gateway runs directly on the host (not in Docker), but agent sandboxes use Docker for isolation. See Sandboxing for details.
Post-Install Setup
After installation completes, switch to the openclaw user:
sudo -i -u openclaw
The post-install script will guide you through:
- Onboarding wizard: Configure OpenClaw settings
- Provider login: Connect WhatsApp/Telegram/Discord/Signal
- Gateway testing: Verify the installation
- Tailscale setup: Connect to your VPN mesh
Quick commands
# Check service status
sudo systemctl status openclaw
# View live logs
sudo journalctl -u openclaw -f
# Restart gateway
sudo systemctl restart openclaw
# Provider login (run as openclaw user)
sudo -i -u openclaw
openclaw channels login
Security Architecture
4-Layer Defense
- Firewall (UFW): Only SSH (22) + Tailscale (41641/udp) exposed publicly
- VPN (Tailscale): Gateway accessible only via VPN mesh
- Docker Isolation: DOCKER-USER iptables chain prevents external port exposure
- Systemd Hardening: NoNewPrivileges, PrivateTmp, unprivileged user
Verification
Test external attack surface:
nmap -p- YOUR_SERVER_IP
Should show only port 22 (SSH) open. All other services (gateway, Docker) are locked down.
Docker Availability
Docker is installed for agent sandboxes (isolated tool execution), not for running the gateway itself. The gateway binds to localhost only and is accessible via Tailscale VPN.
See Multi-Agent Sandbox & Tools for sandbox configuration.
Manual Installation
If you prefer manual control over the automation:
# 1. Install prerequisites
sudo apt update && sudo apt install -y ansible git
# 2. Clone repository
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# 3. Install Ansible collections
ansible-galaxy collection install -r requirements.yml
# 4. Run playbook
./run-playbook.sh
# Or run directly (then manually execute /tmp/openclaw-setup.sh after)
# ansible-playbook playbook.yml --ask-become-pass
Updating OpenClaw
The Ansible installer sets up OpenClaw for manual updates. See Updating for the standard update flow.
To re-run the Ansible playbook (e.g., for configuration changes):
cd openclaw-ansible
./run-playbook.sh
Note: This is idempotent and safe to run multiple times.
Troubleshooting
Firewall blocks my connection
If you're locked out:
- Ensure you can access via Tailscale VPN first
- SSH access (port 22) is always allowed
- The gateway is only accessible via Tailscale by design
Service won't start
# Check logs
sudo journalctl -u openclaw -n 100
# Verify permissions
sudo ls -la /opt/openclaw
# Test manual start
sudo -i -u openclaw
cd ~/openclaw
pnpm start
Docker sandbox issues
# Verify Docker is running
sudo systemctl status docker
# Check sandbox image
sudo docker images | grep openclaw-sandbox
# Build sandbox image if missing
cd /opt/openclaw/openclaw
sudo -u openclaw ./scripts/sandbox-setup.sh
Provider login fails
Make sure you're running as the openclaw user:
sudo -i -u openclaw
openclaw channels login
Advanced Configuration
For detailed security architecture and troubleshooting:
Related
- openclaw-ansible — full deployment guide
- Docker — containerized gateway setup
- Sandboxing — agent sandbox configuration
- Multi-Agent Sandbox & Tools — per-agent isolation
Bun (Experimental)
Source: https://docs.openclaw.ai/install/bun
Bun (experimental)
Goal: run this repo with Bun (optional, not recommended for WhatsApp/Telegram) without diverging from pnpm workflows.
⚠️ Not recommended for Gateway runtime (WhatsApp/Telegram bugs). Use Node for production.
Status
- Bun is an optional local runtime for running TypeScript directly (
bun run …,bun --watch …). pnpmis the default for builds and remains fully supported (and used by some docs tooling).- Bun cannot use
pnpm-lock.yamland will ignore it.
Install
Default:
bun install
Note: bun.lock/bun.lockb are gitignored, so there’s no repo churn either way. If you want no lockfile writes:
bun install --no-save
Build / Test (Bun)
bun run build
bun run vitest run
Bun lifecycle scripts (blocked by default)
Bun may block dependency lifecycle scripts unless explicitly trusted (bun pm untrusted / bun pm trust).
For this repo, the commonly blocked scripts are not required:
@whiskeysockets/baileyspreinstall: checks Node major >= 20 (we run Node 22+).protobufjspostinstall: emits warnings about incompatible version schemes (no build artifacts).
If you hit a real runtime issue that requires these scripts, trust them explicitly:
bun pm trust @whiskeysockets/baileys protobufjs
Caveats
- Some scripts still hardcode pnpm (e.g.
docs:build,ui:*,protocol:check). Run those via pnpm for now.
Development Channels
Source: https://docs.openclaw.ai/install/development-channels
Development channels
Last updated: 2026-01-21
OpenClaw ships three update channels:
- stable: npm dist-tag
latest. - beta: npm dist-tag
beta(builds under test). - dev: moving head of
main(git). npm dist-tag:dev(when published).
We ship builds to beta, test them, then promote a vetted build to latest
without changing the version number — dist-tags are the source of truth for npm installs.
Switching channels
Git checkout:
openclaw update --channel stable
openclaw update --channel beta
openclaw update --channel dev
stable/betacheck out the latest matching tag (often the same tag).devswitches tomainand rebases on the upstream.
npm/pnpm global install:
openclaw update --channel stable
openclaw update --channel beta
openclaw update --channel dev
This updates via the corresponding npm dist-tag (latest, beta, dev).
When you explicitly switch channels with --channel, OpenClaw also aligns
the install method:
devensures a git checkout (default~/openclaw, override withOPENCLAW_GIT_DIR), updates it, and installs the global CLI from that checkout.stable/betainstalls from npm using the matching dist-tag.
Tip: if you want stable + dev in parallel, keep two clones and point your gateway at the stable one.
Plugins and channels
When you switch channels with openclaw update, OpenClaw also syncs plugin sources:
devprefers bundled plugins from the git checkout.stableandbetarestore npm-installed plugin packages.
Tagging best practices
- Tag releases you want git checkouts to land on (
vYYYY.M.DorvYYYY.M.D-<patch>). - Keep tags immutable: never move or reuse a tag.
- npm dist-tags remain the source of truth for npm installs:
latest→ stablebeta→ candidate builddev→ main snapshot (optional)
macOS app availability
Beta and dev builds may not include a macOS app release. That’s OK:
- The git tag and npm dist-tag can still be published.
- Call out “no macOS build for this beta” in release notes or changelog.
Docker
Source: https://docs.openclaw.ai/install/docker
Docker (optional)
Docker is optional. Use it only if you want a containerized gateway or to validate the Docker flow.
Is Docker right for me?
- Yes: you want an isolated, throwaway gateway environment or to run OpenClaw on a host without local installs.
- No: you’re running on your own machine and just want the fastest dev loop. Use the normal install flow instead.
- Sandboxing note: agent sandboxing uses Docker too, but it does not require the full gateway to run in Docker. See Sandboxing.
This guide covers:
- Containerized Gateway (full OpenClaw in Docker)
- Per-session Agent Sandbox (host gateway + Docker-isolated agent tools)
Sandboxing details: Sandboxing
Requirements
- Docker Desktop (or Docker Engine) + Docker Compose v2
- Enough disk for images + logs
Containerized Gateway (Docker Compose)
Quick start (recommended)
From repo root:
./docker-setup.sh
This script:
- builds the gateway image
- runs the onboarding wizard
- prints optional provider setup hints
- starts the gateway via Docker Compose
- generates a gateway token and writes it to
.env
Optional env vars:
OPENCLAW_DOCKER_APT_PACKAGES— install extra apt packages during buildOPENCLAW_EXTRA_MOUNTS— add extra host bind mountsOPENCLAW_HOME_VOLUME— persist/home/nodein a named volume
After it finishes:
- Open
http://127.0.0.1:18789/in your browser. - Paste the token into the Control UI (Settings → token).
- Need the tokenized URL again? Run
docker compose run --rm openclaw-cli dashboard --no-open.
It writes config/workspace on the host:
~/.openclaw/~/.openclaw/workspace
Running on a VPS? See Hetzner (Docker VPS).
Manual flow (compose)
docker build -t openclaw:local -f Dockerfile .
docker compose run --rm openclaw-cli onboard
docker compose up -d openclaw-gateway
Note: run docker compose ... from the repo root. If you enabled
OPENCLAW_EXTRA_MOUNTS or OPENCLAW_HOME_VOLUME, the setup script writes
docker-compose.extra.yml; include it when running Compose elsewhere:
docker compose -f docker-compose.yml -f docker-compose.extra.yml <command>
Control UI token + pairing (Docker)
If you see “unauthorized” or “disconnected (1008): pairing required”, fetch a fresh dashboard link and approve the browser device:
docker compose run --rm openclaw-cli dashboard --no-open
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>
More detail: Dashboard, Devices.
Extra mounts (optional)
If you want to mount additional host directories into the containers, set
OPENCLAW_EXTRA_MOUNTS before running docker-setup.sh. This accepts a
comma-separated list of Docker bind mounts and applies them to both
openclaw-gateway and openclaw-cli by generating docker-compose.extra.yml.
Example:
export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw"
./docker-setup.sh
Notes:
- Paths must be shared with Docker Desktop on macOS/Windows.
- If you edit
OPENCLAW_EXTRA_MOUNTS, rerundocker-setup.shto regenerate the extra compose file. docker-compose.extra.ymlis generated. Don’t hand-edit it.
Persist the entire container home (optional)
If you want /home/node to persist across container recreation, set a named
volume via OPENCLAW_HOME_VOLUME. This creates a Docker volume and mounts it at
/home/node, while keeping the standard config/workspace bind mounts. Use a
named volume here (not a bind path); for bind mounts, use
OPENCLAW_EXTRA_MOUNTS.
Example:
export OPENCLAW_HOME_VOLUME="openclaw_home"
./docker-setup.sh
You can combine this with extra mounts:
export OPENCLAW_HOME_VOLUME="openclaw_home"
export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw"
./docker-setup.sh
Notes:
- If you change
OPENCLAW_HOME_VOLUME, rerundocker-setup.shto regenerate the extra compose file. - The named volume persists until removed with
docker volume rm <name>.
Install extra apt packages (optional)
If you need system packages inside the image (for example, build tools or media
libraries), set OPENCLAW_DOCKER_APT_PACKAGES before running docker-setup.sh.
This installs the packages during the image build, so they persist even if the
container is deleted.
Example:
export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential"
./docker-setup.sh
Notes:
- This accepts a space-separated list of apt package names.
- If you change
OPENCLAW_DOCKER_APT_PACKAGES, rerundocker-setup.shto rebuild the image.
Power-user / full-featured container (opt-in)
The default Docker image is security-first and runs as the non-root node
user. This keeps the attack surface small, but it means:
- no system package installs at runtime
- no Homebrew by default
- no bundled Chromium/Playwright browsers
If you want a more full-featured container, use these opt-in knobs:
- Persist
/home/nodeso browser downloads and tool caches survive:
export OPENCLAW_HOME_VOLUME="openclaw_home"
./docker-setup.sh
- Bake system deps into the image (repeatable + persistent):
export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq"
./docker-setup.sh
- Install Playwright browsers without
npx(avoids npm override conflicts):
docker compose run --rm openclaw-cli \
node /app/node_modules/playwright-core/cli.js install chromium
If you need Playwright to install system deps, rebuild the image with
OPENCLAW_DOCKER_APT_PACKAGES instead of using --with-deps at runtime.
- Persist Playwright browser downloads:
- Set
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwrightindocker-compose.yml. - Ensure
/home/nodepersists viaOPENCLAW_HOME_VOLUME, or mount/home/node/.cache/ms-playwrightviaOPENCLAW_EXTRA_MOUNTS.
Permissions + EACCES
The image runs as node (uid 1000). If you see permission errors on
/home/node/.openclaw, make sure your host bind mounts are owned by uid 1000.
Example (Linux host):
sudo chown -R 1000:1000 /path/to/openclaw-config /path/to/openclaw-workspace
If you choose to run as root for convenience, you accept the security tradeoff.
Faster rebuilds (recommended)
To speed up rebuilds, order your Dockerfile so dependency layers are cached.
This avoids re-running pnpm install unless lockfiles change:
FROM node:22-bookworm
# Install Bun (required for build scripts)
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
RUN corepack enable
WORKDIR /app
# Cache dependencies unless package metadata changes
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY ui/package.json ./ui/package.json
COPY scripts ./scripts
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
RUN pnpm ui:install
RUN pnpm ui:build
ENV NODE_ENV=production
CMD ["node","dist/index.js"]
Channel setup (optional)
Use the CLI container to configure channels, then restart the gateway if needed.
WhatsApp (QR):
docker compose run --rm openclaw-cli channels login
Telegram (bot token):
docker compose run --rm openclaw-cli channels add --channel telegram --token "<token>"
Discord (bot token):
docker compose run --rm openclaw-cli channels add --channel discord --token "<token>"
Docs: WhatsApp, Telegram, Discord
OpenAI Codex OAuth (headless Docker)
If you pick OpenAI Codex OAuth in the wizard, it opens a browser URL and tries
to capture a callback on http://127.0.0.1:1455/auth/callback. In Docker or
headless setups that callback can show a browser error. Copy the full redirect
URL you land on and paste it back into the wizard to finish auth.
Health check
docker compose exec openclaw-gateway node dist/index.js health --token "$OPENCLAW_GATEWAY_TOKEN"
E2E smoke test (Docker)
scripts/e2e/onboard-docker.sh
QR import smoke test (Docker)
pnpm test:docker:qr
Notes
- Gateway bind defaults to
lanfor container use. - Dockerfile CMD uses
--allow-unconfigured; mounted config withgateway.modenotlocalwill still start. Override CMD to enforce the guard. - The gateway container is the source of truth for sessions (
~/.openclaw/agents/<agentId>/sessions/).
Agent Sandbox (host gateway + Docker tools)
Deep dive: Sandboxing
What it does
When agents.defaults.sandbox is enabled, non-main sessions run tools inside a Docker
container. The gateway stays on your host, but the tool execution is isolated:
- scope:
"agent"by default (one container + workspace per agent) - scope:
"session"for per-session isolation - per-scope workspace folder mounted at
/workspace - optional agent workspace access (
agents.defaults.sandbox.workspaceAccess) - allow/deny tool policy (deny wins)
- inbound media is copied into the active sandbox workspace (
media/inbound/*) so tools can read it (withworkspaceAccess: "rw", this lands in the agent workspace)
Warning: scope: "shared" disables cross-session isolation. All sessions share
one container and one workspace.
Per-agent sandbox profiles (multi-agent)
If you use multi-agent routing, each agent can override sandbox + tool settings:
agents.list[].sandbox and agents.list[].tools (plus agents.list[].tools.sandbox.tools). This lets you run
mixed access levels in one gateway:
- Full access (personal agent)
- Read-only tools + read-only workspace (family/work agent)
- No filesystem/shell tools (public agent)
See Multi-Agent Sandbox & Tools for examples, precedence, and troubleshooting.
Default behavior
- Image:
openclaw-sandbox:bookworm-slim - One container per agent
- Agent workspace access:
workspaceAccess: "none"(default) uses~/.openclaw/sandboxes"ro"keeps the sandbox workspace at/workspaceand mounts the agent workspace read-only at/agent(disableswrite/edit/apply_patch)"rw"mounts the agent workspace read/write at/workspace
- Auto-prune: idle > 24h OR age > 7d
- Network:
noneby default (explicitly opt-in if you need egress) - Default allow:
exec,process,read,write,edit,sessions_list,sessions_history,sessions_send,sessions_spawn,session_status - Default deny:
browser,canvas,nodes,cron,discord,gateway
Enable sandboxing
If you plan to install packages in setupCommand, note:
- Default
docker.networkis"none"(no egress). readOnlyRoot: trueblocks package installs.usermust be root forapt-get(omituseror setuser: "0:0"). OpenClaw auto-recreates containers whensetupCommand(or docker config) changes unless the container was recently used (within ~5 minutes). Hot containers log a warning with the exactopenclaw sandbox recreate ...command.
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared (agent is default)
workspaceAccess: "none", // none | ro | rw
workspaceRoot: "~/.openclaw/sandboxes",
docker: {
image: "openclaw-sandbox:bookworm-slim",
workdir: "/workspace",
readOnlyRoot: true,
tmpfs: ["/tmp", "/var/tmp", "/run"],
network: "none",
user: "1000:1000",
capDrop: ["ALL"],
env: { LANG: "C.UTF-8" },
setupCommand: "apt-get update && apt-get install -y git curl jq",
pidsLimit: 256,
memory: "1g",
memorySwap: "2g",
cpus: 1,
ulimits: {
nofile: { soft: 1024, hard: 2048 },
nproc: 256,
},
seccompProfile: "/path/to/seccomp.json",
apparmorProfile: "openclaw-sandbox",
dns: ["1.1.1.1", "8.8.8.8"],
extraHosts: ["internal.service:10.0.0.5"],
},
prune: {
idleHours: 24, // 0 disables idle pruning
maxAgeDays: 7, // 0 disables max-age pruning
},
},
},
},
tools: {
sandbox: {
tools: {
allow: [
"exec",
"process",
"read",
"write",
"edit",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"],
},
},
},
}
Hardening knobs live under agents.defaults.sandbox.docker:
network, user, pidsLimit, memory, memorySwap, cpus, ulimits,
seccompProfile, apparmorProfile, dns, extraHosts.
Multi-agent: override agents.defaults.sandbox.{docker,browser,prune}.* per agent via agents.list[].sandbox.{docker,browser,prune}.*
(ignored when agents.defaults.sandbox.scope / agents.list[].sandbox.scope is "shared").
Build the default sandbox image
scripts/sandbox-setup.sh
This builds openclaw-sandbox:bookworm-slim using Dockerfile.sandbox.
Sandbox common image (optional)
If you want a sandbox image with common build tooling (Node, Go, Rust, etc.), build the common image:
scripts/sandbox-common-setup.sh
This builds openclaw-sandbox-common:bookworm-slim. To use it:
{
agents: {
defaults: {
sandbox: { docker: { image: "openclaw-sandbox-common:bookworm-slim" } },
},
},
}
Sandbox browser image
To run the browser tool inside the sandbox, build the browser image:
scripts/sandbox-browser-setup.sh
This builds openclaw-sandbox-browser:bookworm-slim using
Dockerfile.sandbox-browser. The container runs Chromium with CDP enabled and
an optional noVNC observer (headful via Xvfb).
Notes:
- Headful (Xvfb) reduces bot blocking vs headless.
- Headless can still be used by setting
agents.defaults.sandbox.browser.headless=true. - No full desktop environment (GNOME) is needed; Xvfb provides the display.
Use config:
{
agents: {
defaults: {
sandbox: {
browser: { enabled: true },
},
},
},
}
Custom browser image:
{
agents: {
defaults: {
sandbox: { browser: { image: "my-openclaw-browser" } },
},
},
}
When enabled, the agent receives:
- a sandbox browser control URL (for the
browsertool) - a noVNC URL (if enabled and headless=false)
Remember: if you use an allowlist for tools, add browser (and remove it from
deny) or the tool remains blocked.
Prune rules (agents.defaults.sandbox.prune) apply to browser containers too.
Custom sandbox image
Build your own image and point config to it:
docker build -t my-openclaw-sbx -f Dockerfile.sandbox .
{
agents: {
defaults: {
sandbox: { docker: { image: "my-openclaw-sbx" } },
},
},
}
Tool policy (allow/deny)
denywins overallow.- If
allowis empty: all tools (except deny) are available. - If
allowis non-empty: only tools inalloware available (minus deny).
Pruning strategy
Two knobs:
prune.idleHours: remove containers not used in X hours (0 = disable)prune.maxAgeDays: remove containers older than X days (0 = disable)
Example:
- Keep busy sessions but cap lifetime:
idleHours: 24,maxAgeDays: 7 - Never prune:
idleHours: 0,maxAgeDays: 0
Security notes
- Hard wall only applies to tools (exec/read/write/edit/apply_patch).
- Host-only tools like browser/camera/canvas are blocked by default.
- Allowing
browserin sandbox breaks isolation (browser runs on host).
Troubleshooting
- Image missing: build with
scripts/sandbox-setup.shor setagents.defaults.sandbox.docker.image. - Container not running: it will auto-create per session on demand.
- Permission errors in sandbox: set
docker.userto a UID:GID that matches your mounted workspace ownership (or chown the workspace folder). - Custom tools not found: OpenClaw runs commands with
sh -lc(login shell), which sources/etc/profileand may reset PATH. Setdocker.env.PATHto prepend your custom tool paths (e.g.,/custom/bin:/usr/local/share/npm-global/bin), or add a script under/etc/profile.d/in your Dockerfile.
Install
Source: https://docs.openclaw.ai/install/index
Install
Use the installer unless you have a reason not to. It sets up the CLI and runs onboarding.
Quick install (recommended)
curl -fsSL https://openclaw.ai/install.sh | bash
Windows (PowerShell):
iwr -useb https://openclaw.ai/install.ps1 | iex
Next step (if you skipped onboarding):
openclaw onboard --install-daemon
System requirements
- Node >=22
- macOS, Linux, or Windows via WSL2
pnpmonly if you build from source
Choose your install path
1) Installer script (recommended)
Installs openclaw globally via npm and runs onboarding.
curl -fsSL https://openclaw.ai/install.sh | bash
Installer flags:
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help
Details: Installer internals.
Non-interactive (skip onboarding):
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
2) Global install (manual)
If you already have Node:
npm install -g openclaw@latest
If you have libvips installed globally (common on macOS via Homebrew) and sharp fails to install, force prebuilt binaries:
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
If you see sharp: Please add node-gyp to your dependencies, either install build tooling (macOS: Xcode CLT + npm install -g node-gyp) or use the SHARP_IGNORE_GLOBAL_LIBVIPS=1 workaround above to skip the native build.
Or with pnpm:
pnpm add -g openclaw@latest
pnpm approve-builds -g # approve openclaw, node-llama-cpp, sharp, etc.
pnpm requires explicit approval for packages with build scripts. After the first install shows the "Ignored build scripts" warning, run pnpm approve-builds -g and select the listed packages.
Then:
openclaw onboard --install-daemon
3) From source (contributors/dev)
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon
Tip: if you don’t have a global install yet, run repo commands via pnpm openclaw ....
4) Other install options
After install
- Run onboarding:
openclaw onboard --install-daemon - Quick check:
openclaw doctor - Check gateway health:
openclaw status+openclaw health - Open the dashboard:
openclaw dashboard
Install method: npm vs git (installer)
The installer supports two methods:
npm(default):npm install -g openclaw@latestgit: clone/build from GitHub and run from a source checkout
CLI flags
# Explicit npm
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm
# Install from GitHub (source checkout)
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git
Common flags:
--install-method npm|git--git-dir <path>(default:~/openclaw)--no-git-update(skipgit pullwhen using an existing checkout)--no-prompt(disable prompts; required in CI/automation)--dry-run(print what would happen; make no changes)--no-onboard(skip onboarding)
Environment variables
Equivalent env vars (useful for automation):
OPENCLAW_INSTALL_METHOD=git|npmOPENCLAW_GIT_DIR=...OPENCLAW_GIT_UPDATE=0|1OPENCLAW_NO_PROMPT=1OPENCLAW_DRY_RUN=1OPENCLAW_NO_ONBOARD=1SHARP_IGNORE_GLOBAL_LIBVIPS=0|1(default:1; avoidssharpbuilding against system libvips)
Troubleshooting: openclaw not found (PATH)
Quick diagnosis:
node -v
npm -v
npm prefix -g
echo "$PATH"
If $(npm prefix -g)/bin (macOS/Linux) or $(npm prefix -g) (Windows) is not present inside echo "$PATH", your shell can’t find global npm binaries (including openclaw).
Fix: add it to your shell startup file (zsh: ~/.zshrc, bash: ~/.bashrc):
# macOS / Linux
export PATH="$(npm prefix -g)/bin:$PATH"
On Windows, add the output of npm prefix -g to your PATH.
Then open a new terminal (or rehash in zsh / hash -r in bash).
Update / uninstall
Installer Internals
Source: https://docs.openclaw.ai/install/installer
Installer internals
OpenClaw ships two installer scripts (served from openclaw.ai):
https://openclaw.ai/install.sh— “recommended” installer (global npm install by default; can also install from a GitHub checkout)https://openclaw.ai/install-cli.sh— non-root-friendly CLI installer (installs into a prefix with its own Node)https://openclaw.ai/install.ps1— Windows PowerShell installer (npm by default; optional git install)
To see the current flags/behavior, run:
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help
Windows (PowerShell) help:
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -?
If the installer completes but openclaw is not found in a new terminal, it’s usually a Node/npm PATH issue. See: Install.
install.sh (recommended)
What it does (high level):
- Detect OS (macOS / Linux / WSL).
- Ensure Node.js 22+ (macOS via Homebrew; Linux via NodeSource).
- Choose install method:
npm(default):npm install -g openclaw@latestgit: clone/build a source checkout and install a wrapper script
- On Linux: avoid global npm permission errors by switching npm's prefix to
~/.npm-globalwhen needed. - If upgrading an existing install: runs
openclaw doctor --non-interactive(best effort). - For git installs: runs
openclaw doctor --non-interactiveafter install/update (best effort). - Mitigates
sharpnative install gotchas by defaultingSHARP_IGNORE_GLOBAL_LIBVIPS=1(avoids building against system libvips).
If you want sharp to link against a globally-installed libvips (or you’re debugging), set:
SHARP_IGNORE_GLOBAL_LIBVIPS=0 curl -fsSL https://openclaw.ai/install.sh | bash
Discoverability / “git install” prompt
If you run the installer while already inside a OpenClaw source checkout (detected via package.json + pnpm-workspace.yaml), it prompts:
- update and use this checkout (
git) - or migrate to the global npm install (
npm)
In non-interactive contexts (no TTY / --no-prompt), you must pass --install-method git|npm (or set OPENCLAW_INSTALL_METHOD), otherwise the script exits with code 2.
Why Git is needed
Git is required for the --install-method git path (clone / pull).
For npm installs, Git is usually not required, but some environments still end up needing it (e.g. when a package or dependency is fetched via a git URL). The installer currently ensures Git is present to avoid spawn git ENOENT surprises on fresh distros.
Why npm hits EACCES on fresh Linux
On some Linux setups (especially after installing Node via the system package manager or NodeSource), npm's global prefix points at a root-owned location. Then npm install -g ... fails with EACCES / mkdir permission errors.
install.sh mitigates this by switching the prefix to:
~/.npm-global(and adding it toPATHin~/.bashrc/~/.zshrcwhen present)
install-cli.sh (non-root CLI installer)
This script installs openclaw into a prefix (default: ~/.openclaw) and also installs a dedicated Node runtime under that prefix, so it can work on machines where you don’t want to touch the system Node/npm.
Help:
curl -fsSL https://openclaw.ai/install-cli.sh | bash -s -- --help
install.ps1 (Windows PowerShell)
What it does (high level):
- Ensure Node.js 22+ (winget/Chocolatey/Scoop or manual).
- Choose install method:
npm(default):npm install -g openclaw@latestgit: clone/build a source checkout and install a wrapper script
- Runs
openclaw doctor --non-interactiveon upgrades and git installs (best effort).
Examples:
iwr -useb https://openclaw.ai/install.ps1 | iex
iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git
iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git -GitDir "C:\\openclaw"
Environment variables:
OPENCLAW_INSTALL_METHOD=git|npmOPENCLAW_GIT_DIR=...
Git requirement:
If you choose -InstallMethod git and Git is missing, the installer will print the
Git for Windows link (https://git-scm.com/download/win) and exit.
Common Windows issues:
- npm error spawn git / ENOENT: install Git for Windows and reopen PowerShell, then rerun the installer.
- "openclaw" is not recognized: your npm global bin folder is not on PATH. Most systems use
%AppData%\\npm. You can also runnpm config get prefixand add\\binto PATH, then reopen PowerShell.
Nix
Source: https://docs.openclaw.ai/install/nix
Nix Installation
The recommended way to run OpenClaw with Nix is via nix-openclaw — a batteries-included Home Manager module.
Quick Start
Paste this to your AI agent (Claude, Cursor, etc.):
I want to set up nix-openclaw on my Mac.
Repository: github:openclaw/nix-openclaw
What I need you to do:
1. Check if Determinate Nix is installed (if not, install it)
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
4. Set up secrets (bot token, Anthropic key) - plain files at ~/.secrets/ is fine
5. Fill in the template placeholders and run home-manager switch
6. Verify: launchd running, bot responds to messages
Reference the nix-openclaw README for module options.
📦 Full guide: github.com/openclaw/nix-openclaw
The nix-openclaw repo is the source of truth for Nix installation. This page is just a quick overview.
What you get
- Gateway + macOS app + tools (whisper, spotify, cameras) — all pinned
- Launchd service that survives reboots
- Plugin system with declarative config
- Instant rollback:
home-manager switch --rollback
Nix Mode Runtime Behavior
When OPENCLAW_NIX_MODE=1 is set (automatic with nix-openclaw):
OpenClaw supports a Nix mode that makes configuration deterministic and disables auto-install flows. Enable it by exporting:
OPENCLAW_NIX_MODE=1
On macOS, the GUI app does not automatically inherit shell env vars. You can also enable Nix mode via defaults:
defaults write bot.molt.mac openclaw.nixMode -bool true
Config + state paths
OpenClaw reads JSON5 config from OPENCLAW_CONFIG_PATH and stores mutable data in OPENCLAW_STATE_DIR.
OPENCLAW_STATE_DIR(default:~/.openclaw)OPENCLAW_CONFIG_PATH(default:$OPENCLAW_STATE_DIR/openclaw.json)
When running under Nix, set these explicitly to Nix-managed locations so runtime state and config stay out of the immutable store.
Runtime behavior in Nix mode
- Auto-install and self-mutation flows are disabled
- Missing dependencies surface Nix-specific remediation messages
- UI surfaces a read-only Nix mode banner when present
Packaging note (macOS)
The macOS packaging flow expects a stable Info.plist template at:
apps/macos/Sources/OpenClaw/Resources/Info.plist
scripts/package-mac-app.sh copies this template into the app bundle and patches dynamic fields
(bundle ID, version/build, Git SHA, Sparkle keys). This keeps the plist deterministic for SwiftPM
packaging and Nix builds (which do not rely on a full Xcode toolchain).
Related
- nix-openclaw — full setup guide
- Wizard — non-Nix CLI setup
- Docker — containerized setup
Uninstall
Source: https://docs.openclaw.ai/install/uninstall
Uninstall
Two paths:
- Easy path if
openclawis still installed. - Manual service removal if the CLI is gone but the service is still running.
Easy path (CLI still installed)
Recommended: use the built-in uninstaller:
openclaw uninstall
Non-interactive (automation / npx):
openclaw uninstall --all --yes --non-interactive
npx -y openclaw uninstall --all --yes --non-interactive
Manual steps (same result):
- Stop the gateway service:
openclaw gateway stop
- Uninstall the gateway service (launchd/systemd/schtasks):
openclaw gateway uninstall
- Delete state + config:
rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
If you set OPENCLAW_CONFIG_PATH to a custom location outside the state dir, delete that file too.
- Delete your workspace (optional, removes agent files):
rm -rf ~/.openclaw/workspace
- Remove the CLI install (pick the one you used):
npm rm -g openclaw
pnpm remove -g openclaw
bun remove -g openclaw
- If you installed the macOS app:
rm -rf /Applications/OpenClaw.app
Notes:
- If you used profiles (
--profile/OPENCLAW_PROFILE), repeat step 3 for each state dir (defaults are~/.openclaw-<profile>). - In remote mode, the state dir lives on the gateway host, so run steps 1-4 there too.
Manual service removal (CLI not installed)
Use this if the gateway service keeps running but openclaw is missing.
macOS (launchd)
Default label is bot.molt.gateway (or bot.molt.<profile>; legacy com.openclaw.* may still exist):
launchctl bootout gui/$UID/bot.molt.gateway
rm -f ~/Library/LaunchAgents/bot.molt.gateway.plist
If you used a profile, replace the label and plist name with bot.molt.<profile>. Remove any legacy com.openclaw.* plists if present.
Linux (systemd user unit)
Default unit name is openclaw-gateway.service (or openclaw-gateway-<profile>.service):
systemctl --user disable --now openclaw-gateway.service
rm -f ~/.config/systemd/user/openclaw-gateway.service
systemctl --user daemon-reload
Windows (Scheduled Task)
Default task name is OpenClaw Gateway (or OpenClaw Gateway (<profile>)).
The task script lives under your state dir.
schtasks /Delete /F /TN "OpenClaw Gateway"
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd"
If you used a profile, delete the matching task name and ~\.openclaw-<profile>\gateway.cmd.
Normal install vs source checkout
Normal install (install.sh / npm / pnpm / bun)
If you used https://openclaw.ai/install.sh or install.ps1, the CLI was installed with npm install -g openclaw@latest.
Remove it with npm rm -g openclaw (or pnpm remove -g / bun remove -g if you installed that way).
Source checkout (git clone)
If you run from a repo checkout (git clone + openclaw ... / bun run openclaw ...):
- Uninstall the gateway service before deleting the repo (use the easy path above or manual service removal).
- Delete the repo directory.
- Remove state + workspace as shown above.
Updating
Source: https://docs.openclaw.ai/install/updating
Updating
OpenClaw is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use openclaw update, which restarts) → verify.
Recommended: re-run the website installer (upgrade in place)
The preferred update path is to re-run the installer from the website. It
detects existing installs, upgrades in place, and runs openclaw doctor when
needed.
curl -fsSL https://openclaw.ai/install.sh | bash
Notes:
- Add
--no-onboardif you don’t want the onboarding wizard to run again. - For source installs, use:
The installer will
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboardgit pull --rebaseonly if the repo is clean. - For global installs, the script uses
npm install -g openclaw@latestunder the hood. - Legacy note:
clawdbotremains available as a compatibility shim.
Before you update
- Know how you installed: global (npm/pnpm) vs from source (git clone).
- Know how your Gateway is running: foreground terminal vs supervised service (launchd/systemd).
- Snapshot your tailoring:
- Config:
~/.openclaw/openclaw.json - Credentials:
~/.openclaw/credentials/ - Workspace:
~/.openclaw/workspace
- Config:
Update (global install)
Global install (pick one):
npm i -g openclaw@latest
pnpm add -g openclaw@latest
We do not recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
To switch update channels (git + npm installs):
openclaw update --channel beta
openclaw update --channel dev
openclaw update --channel stable
Use --tag <dist-tag|version> for a one-off install tag/version.
See Development channels for channel semantics and release notes.
Note: on npm installs, the gateway logs an update hint on startup (checks the current channel tag). Disable via update.checkOnStart: false.
Then:
openclaw doctor
openclaw gateway restart
openclaw health
Notes:
- If your Gateway runs as a service,
openclaw gateway restartis preferred over killing PIDs. - If you’re pinned to a specific version, see “Rollback / pinning” below.
Update (openclaw update)
For source installs (git checkout), prefer:
openclaw update
It runs a safe-ish update flow:
- Requires a clean worktree.
- Switches to the selected channel (tag or branch).
- Fetches + rebases against the configured upstream (dev channel).
- Installs deps, builds, builds the Control UI, and runs
openclaw doctor. - Restarts the gateway by default (use
--no-restartto skip).
If you installed via npm/pnpm (no git metadata), openclaw update will try to update via your package manager. If it can’t detect the install, use “Update (global install)” instead.
Update (Control UI / RPC)
The Control UI has Update & Restart (RPC: update.run). It:
- Runs the same source-update flow as
openclaw update(git checkout only). - Writes a restart sentinel with a structured report (stdout/stderr tail).
- Restarts the gateway and pings the last active session with the report.
If the rebase fails, the gateway aborts and restarts without applying the update.
Update (from source)
From the repo checkout:
Preferred:
openclaw update
Manual (equivalent-ish):
git pull
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
openclaw doctor
openclaw health
Notes:
pnpm buildmatters when you run the packagedopenclawbinary (openclaw.mjs) or use Node to rundist/.- If you run from a repo checkout without a global install, use
pnpm openclaw ...for CLI commands. - If you run directly from TypeScript (
pnpm openclaw ...), a rebuild is usually unnecessary, but config migrations still apply → run doctor. - Switching between global and git installs is easy: install the other flavor, then run
openclaw doctorso the gateway service entrypoint is rewritten to the current install.
Always Run: openclaw doctor
Doctor is the “safe update” command. It’s intentionally boring: repair + migrate + warn.
Note: if you’re on a source install (git checkout), openclaw doctor will offer to run openclaw update first.
Typical things it does:
- Migrate deprecated config keys / legacy config file locations.
- Audit DM policies and warn on risky “open” settings.
- Check Gateway health and can offer to restart.
- Detect and migrate older gateway services (launchd/systemd; legacy schtasks) to current OpenClaw services.
- On Linux, ensure systemd user lingering (so the Gateway survives logout).
Details: Doctor
Start / stop / restart the Gateway
CLI (works regardless of OS):
openclaw gateway status
openclaw gateway stop
openclaw gateway restart
openclaw gateway --port 18789
openclaw logs --follow
If you’re supervised:
- macOS launchd (app-bundled LaunchAgent):
launchctl kickstart -k gui/$UID/bot.molt.gateway(usebot.molt.<profile>; legacycom.openclaw.*still works) - Linux systemd user service:
systemctl --user restart openclaw-gateway[-<profile>].service - Windows (WSL2):
systemctl --user restart openclaw-gateway[-<profile>].servicelaunchctl/systemctlonly work if the service is installed; otherwise runopenclaw gateway install.
Runbook + exact service labels: Gateway runbook
Rollback / pinning (when something breaks)
Pin (global install)
Install a known-good version (replace <version> with the last working one):
npm i -g openclaw@<version>
pnpm add -g openclaw@<version>
Tip: to see the current published version, run npm view openclaw version.
Then restart + re-run doctor:
openclaw doctor
openclaw gateway restart
Pin (source) by date
Pick a commit from a date (example: “state of main as of 2026-01-01”):
git fetch origin
git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)"
Then reinstall deps + restart:
pnpm install
pnpm build
openclaw gateway restart
If you want to go back to latest later:
git checkout main
git pull
If you’re stuck
- Run
openclaw doctoragain and read the output carefully (it often tells you the fix). - Check: Troubleshooting
- Ask in Discord: https://discord.gg/clawd
Multi-Agent Sandbox & Tools
Source: https://docs.openclaw.ai/multi-agent-sandbox-tools
Multi-Agent Sandbox & Tools Configuration
Overview
Each agent in a multi-agent setup can now have its own:
- Sandbox configuration (
agents.list[].sandboxoverridesagents.defaults.sandbox) - Tool restrictions (
tools.allow/tools.deny, plusagents.list[].tools)
This allows you to run multiple agents with different security profiles:
- Personal assistant with full access
- Family/work agents with restricted tools
- Public-facing agents in sandboxes
setupCommand belongs under sandbox.docker (global or per-agent) and runs once
when the container is created.
Auth is per-agent: each agent reads from its own agentDir auth store at:
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
Credentials are not shared between agents. Never reuse agentDir across agents.
If you want to share creds, copy auth-profiles.json into the other agent's agentDir.
For how sandboxing behaves at runtime, see Sandboxing.
For debugging “why is this blocked?”, see Sandbox vs Tool Policy vs Elevated and openclaw sandbox explain.
Configuration Examples
Example 1: Personal + Restricted Family Agent
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"name": "Personal Assistant",
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
},
{
"id": "family",
"name": "Family Bot",
"workspace": "~/.openclaw/workspace-family",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"]
}
}
]
},
"bindings": [
{
"agentId": "family",
"match": {
"provider": "whatsapp",
"accountId": "*",
"peer": {
"kind": "group",
"id": "120363424282127706@g.us"
}
}
}
]
}
Result:
mainagent: Runs on host, full tool accessfamilyagent: Runs in Docker (one container per agent), onlyreadtool
Example 2: Work Agent with Shared Sandbox
{
"agents": {
"list": [
{
"id": "personal",
"workspace": "~/.openclaw/workspace-personal",
"sandbox": { "mode": "off" }
},
{
"id": "work",
"workspace": "~/.openclaw/workspace-work",
"sandbox": {
"mode": "all",
"scope": "shared",
"workspaceRoot": "/tmp/work-sandboxes"
},
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": ["browser", "gateway", "discord"]
}
}
]
}
}
Example 2b: Global coding profile + messaging-only agent
{
"tools": { "profile": "coding" },
"agents": {
"list": [
{
"id": "support",
"tools": { "profile": "messaging", "allow": ["slack"] }
}
]
}
}
Result:
- default agents get coding tools
supportagent is messaging-only (+ Slack tool)
Example 3: Different Sandbox Modes per Agent
{
"agents": {
"defaults": {
"sandbox": {
"mode": "non-main", // Global default
"scope": "session"
}
},
"list": [
{
"id": "main",
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "off" // Override: main never sandboxed
}
},
{
"id": "public",
"workspace": "~/.openclaw/workspace-public",
"sandbox": {
"mode": "all", // Override: public always sandboxed
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch"]
}
}
]
}
}
Configuration Precedence
When both global (agents.defaults.*) and agent-specific (agents.list[].*) configs exist:
Sandbox Config
Agent-specific settings override global:
agents.list[].sandbox.mode > agents.defaults.sandbox.mode
agents.list[].sandbox.scope > agents.defaults.sandbox.scope
agents.list[].sandbox.workspaceRoot > agents.defaults.sandbox.workspaceRoot
agents.list[].sandbox.workspaceAccess > agents.defaults.sandbox.workspaceAccess
agents.list[].sandbox.docker.* > agents.defaults.sandbox.docker.*
agents.list[].sandbox.browser.* > agents.defaults.sandbox.browser.*
agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.*
Notes:
agents.list[].sandbox.{docker,browser,prune}.*overridesagents.defaults.sandbox.{docker,browser,prune}.*for that agent (ignored when sandbox scope resolves to"shared").
Tool Restrictions
The filtering order is:
- Tool profile (
tools.profileoragents.list[].tools.profile) - Provider tool profile (
tools.byProvider[provider].profileoragents.list[].tools.byProvider[provider].profile) - Global tool policy (
tools.allow/tools.deny) - Provider tool policy (
tools.byProvider[provider].allow/deny) - Agent-specific tool policy (
agents.list[].tools.allow/deny) - Agent provider policy (
agents.list[].tools.byProvider[provider].allow/deny) - Sandbox tool policy (
tools.sandbox.toolsoragents.list[].tools.sandbox.tools) - Subagent tool policy (
tools.subagents.tools, if applicable)
Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
If agents.list[].tools.sandbox.tools is set, it replaces tools.sandbox.tools for that agent.
If agents.list[].tools.profile is set, it overrides tools.profile for that agent.
Provider tool keys accept either provider (e.g. google-antigravity) or provider/model (e.g. openai/gpt-5.2).
Tool groups (shorthands)
Tool policies (global, agent, sandbox) support group:* entries that expand to multiple concrete tools:
group:runtime:exec,bash,processgroup:fs:read,write,edit,apply_patchgroup:sessions:sessions_list,sessions_history,sessions_send,sessions_spawn,session_statusgroup:memory:memory_search,memory_getgroup:ui:browser,canvasgroup:automation:cron,gatewaygroup:messaging:messagegroup:nodes:nodesgroup:openclaw: all built-in OpenClaw tools (excludes provider plugins)
Elevated Mode
tools.elevated is the global baseline (sender-based allowlist). agents.list[].tools.elevated can further restrict elevated for specific agents (both must allow).
Mitigation patterns:
- Deny
execfor untrusted agents (agents.list[].tools.deny: ["exec"]) - Avoid allowlisting senders that route to restricted agents
- Disable elevated globally (
tools.elevated.enabled: false) if you only want sandboxed execution - Disable elevated per agent (
agents.list[].tools.elevated.enabled: false) for sensitive profiles
Migration from Single Agent
Before (single agent):
{
"agents": {
"defaults": {
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "non-main"
}
}
},
"tools": {
"sandbox": {
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": []
}
}
}
}
After (multi-agent with different profiles):
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
}
]
}
}
Legacy agent.* configs are migrated by openclaw doctor; prefer agents.defaults + agents.list going forward.
Tool Restriction Examples
Read-only Agent
{
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process"]
}
}
Safe Execution Agent (no file modifications)
{
"tools": {
"allow": ["read", "exec", "process"],
"deny": ["write", "edit", "apply_patch", "browser", "gateway"]
}
}
Communication-only Agent
{
"tools": {
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
}
}
Common Pitfall: "non-main"
agents.defaults.sandbox.mode: "non-main" is based on session.mainKey (default "main"),
not the agent id. Group/channel sessions always get their own keys, so they
are treated as non-main and will be sandboxed. If you want an agent to never
sandbox, set agents.list[].sandbox.mode: "off".
Testing
After configuring multi-agent sandbox and tools:
-
Check agent resolution:
openclaw agents list --bindings -
Verify sandbox containers:
docker ps --filter "name=openclaw-sbx-" -
Test tool restrictions:
- Send a message requiring restricted tools
- Verify the agent cannot use denied tools
-
Monitor logs:
tail -f "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/logs/gateway.log" | grep -E "routing|sandbox|tools"
Troubleshooting
Agent not sandboxed despite mode: "all"
- Check if there's a global
agents.defaults.sandbox.modethat overrides it - Agent-specific config takes precedence, so set
agents.list[].sandbox.mode: "all"
Tools still available despite deny list
- Check tool filtering order: global → agent → sandbox → subagent
- Each level can only further restrict, not grant back
- Verify with logs:
[tools] filtering tools for agent:${agentId}
Container not isolated per agent
- Set
scope: "agent"in agent-specific sandbox config - Default is
"session"which creates one container per session
See Also
Android App
Source: https://docs.openclaw.ai/platforms/android
Android App (Node)
Support snapshot
- Role: companion node app (Android does not host the Gateway).
- Gateway required: yes (run it on macOS, Linux, or Windows via WSL2).
- Install: Getting Started + Pairing.
- Gateway: Runbook + Configuration.
- Protocols: Gateway protocol (nodes + control plane).
System control
System control (launchd/systemd) lives on the Gateway host. See Gateway.
Connection Runbook
Android node app ⇄ (mDNS/NSD + WebSocket) ⇄ Gateway
Android connects directly to the Gateway WebSocket (default ws://<host>:18789) and uses Gateway-owned pairing.
Prerequisites
- You can run the Gateway on the “master” machine.
- Android device/emulator can reach the gateway WebSocket:
- Same LAN with mDNS/NSD, or
- Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), or
- Manual gateway host/port (fallback)
- You can run the CLI (
openclaw) on the gateway machine (or via SSH).
1) Start the Gateway
openclaw gateway --port 18789 --verbose
Confirm in logs you see something like:
listening on ws://0.0.0.0:18789
For tailnet-only setups (recommended for Vienna ⇄ London), bind the gateway to the tailnet IP:
- Set
gateway.bind: "tailnet"in~/.openclaw/openclaw.jsonon the gateway host. - Restart the Gateway / macOS menubar app.
2) Verify discovery (optional)
From the gateway machine:
dns-sd -B _openclaw-gw._tcp local.
More debugging notes: Bonjour.
Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
Android NSD/mDNS discovery won’t cross networks. If your Android node and the gateway are on different networks but connected via Tailscale, use Wide-Area Bonjour / unicast DNS-SD instead:
- Set up a DNS-SD zone (example
openclaw.internal.) on the gateway host and publish_openclaw-gw._tcprecords. - Configure Tailscale split DNS for your chosen domain pointing at that DNS server.
Details and example CoreDNS config: Bonjour.
3) Connect from Android
In the Android app:
- The app keeps its gateway connection alive via a foreground service (persistent notification).
- Open Settings.
- Under Discovered Gateways, select your gateway and hit Connect.
- If mDNS is blocked, use Advanced → Manual Gateway (host + port) and Connect (Manual).
After the first successful pairing, Android auto-reconnects on launch:
- Manual endpoint (if enabled), otherwise
- The last discovered gateway (best-effort).
4) Approve pairing (CLI)
On the gateway machine:
openclaw nodes pending
openclaw nodes approve <requestId>
Pairing details: Gateway pairing.
5) Verify the node is connected
- Via nodes status:
openclaw nodes status - Via Gateway:
openclaw gateway call node.list --params "{}"
6) Chat + history
The Android node’s Chat sheet uses the gateway’s primary session key (main), so history and replies are shared with WebChat and other clients:
- History:
chat.history - Send:
chat.send - Push updates (best-effort):
chat.subscribe→event:"chat"
7) Canvas + camera
Gateway Canvas Host (recommended for web content)
If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point the node at the Gateway canvas host.
Note: nodes use the standalone canvas host on canvasHost.port (default 18793).
-
Create
~/.openclaw/workspace/canvas/index.htmlon the gateway host. -
Navigate the node to it (LAN):
openclaw nodes invoke --node "<Android Node>" --command canvas.navigate --params '{"url":"http://<gateway-hostname>.local:18793/__openclaw__/canvas/"}'
Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of .local, e.g. http://<gateway-magicdns>:18793/__openclaw__/canvas/.
This server injects a live-reload client into HTML and reloads on file changes.
The A2UI host lives at http://<gateway-host>:18793/__openclaw__/a2ui/.
Canvas commands (foreground only):
canvas.eval,canvas.snapshot,canvas.navigate(use{"url":""}or{"url":"/"}to return to the default scaffold).canvas.snapshotreturns{ format, base64 }(defaultformat="jpeg").- A2UI:
canvas.a2ui.push,canvas.a2ui.reset(canvas.a2ui.pushJSONLlegacy alias)
Camera commands (foreground only; permission-gated):
camera.snap(jpg)camera.clip(mp4)
See Camera node for parameters and CLI helpers.
Platforms
Source: https://docs.openclaw.ai/platforms/index
Platforms
OpenClaw core is written in TypeScript. Node is the recommended runtime. Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).
Companion apps exist for macOS (menu bar app) and mobile nodes (iOS/Android). Windows and Linux companion apps are planned, but the Gateway is fully supported today. Native companion apps for Windows are also planned; the Gateway is recommended via WSL2.
Choose your OS
VPS & hosting
- VPS hub: VPS hosting
- Fly.io: Fly.io
- Hetzner (Docker): Hetzner
- GCP (Compute Engine): GCP
- exe.dev (VM + HTTPS proxy): exe.dev
Common links
- Install guide: Getting Started
- Gateway runbook: Gateway
- Gateway configuration: Configuration
- Service status:
openclaw gateway status
Gateway service install (CLI)
Use one of these (all supported):
- Wizard (recommended):
openclaw onboard --install-daemon - Direct:
openclaw gateway install - Configure flow:
openclaw configure→ select Gateway service - Repair/migrate:
openclaw doctor(offers to install or fix the service)
The service target depends on OS:
- macOS: LaunchAgent (
bot.molt.gatewayorbot.molt.<profile>; legacycom.openclaw.*) - Linux/WSL2: systemd user service (
openclaw-gateway[-<profile>].service)
iOS App
Source: https://docs.openclaw.ai/platforms/ios
iOS App (Node)
Availability: internal preview. The iOS app is not publicly distributed yet.
What it does
- Connects to a Gateway over WebSocket (LAN or tailnet).
- Exposes node capabilities: Canvas, Screen snapshot, Camera capture, Location, Talk mode, Voice wake.
- Receives
node.invokecommands and reports node status events.
Requirements
- Gateway running on another device (macOS, Linux, or Windows via WSL2).
- Network path:
- Same LAN via Bonjour, or
- Tailnet via unicast DNS-SD (example domain:
openclaw.internal.), or - Manual host/port (fallback).
Quick start (pair + connect)
- Start the Gateway:
openclaw gateway --port 18789
-
In the iOS app, open Settings and pick a discovered gateway (or enable Manual Host and enter host/port).
-
Approve the pairing request on the gateway host:
openclaw nodes pending
openclaw nodes approve <requestId>
- Verify connection:
openclaw nodes status
openclaw gateway call node.list --params "{}"
Discovery paths
Bonjour (LAN)
The Gateway advertises _openclaw-gw._tcp on local.. The iOS app lists these automatically.
Tailnet (cross-network)
If mDNS is blocked, use a unicast DNS-SD zone (choose a domain; example: openclaw.internal.) and Tailscale split DNS.
See Bonjour for the CoreDNS example.
Manual host/port
In Settings, enable Manual Host and enter the gateway host + port (default 18789).
Canvas + A2UI
The iOS node renders a WKWebView canvas. Use node.invoke to drive it:
openclaw nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://<gateway-host>:18793/__openclaw__/canvas/"}'
Notes:
- The Gateway canvas host serves
/__openclaw__/canvas/and/__openclaw__/a2ui/. - The iOS node auto-navigates to A2UI on connect when a canvas host URL is advertised.
- Return to the built-in scaffold with
canvas.navigateand{"url":""}.
Canvas eval / snapshot
openclaw nodes invoke --node "iOS Node" --command canvas.eval --params '{"javaScript":"(() => { const {ctx} = window.__openclaw; ctx.clearRect(0,0,innerWidth,innerHeight); ctx.lineWidth=6; ctx.strokeStyle=\"#ff2d55\"; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(innerWidth-40, innerHeight-40); ctx.stroke(); return \"ok\"; })()"}'
openclaw nodes invoke --node "iOS Node" --command canvas.snapshot --params '{"maxWidth":900,"format":"jpeg"}'
Voice wake + talk mode
- Voice wake and talk mode are available in Settings.
- iOS may suspend background audio; treat voice features as best-effort when the app is not active.
Common errors
NODE_BACKGROUND_UNAVAILABLE: bring the iOS app to the foreground (canvas/camera/screen commands require it).A2UI_HOST_NOT_CONFIGURED: the Gateway did not advertise a canvas host URL; checkcanvasHostin Gateway configuration.- Pairing prompt never appears: run
openclaw nodes pendingand approve manually. - Reconnect fails after reinstall: the Keychain pairing token was cleared; re-pair the node.
Related docs
Linux App
Source: https://docs.openclaw.ai/platforms/linux
Linux App
The Gateway is fully supported on Linux. Node is the recommended runtime. Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).
Native Linux companion apps are planned. Contributions are welcome if you want to help build one.
Beginner quick path (VPS)
- Install Node 22+
npm i -g openclaw@latestopenclaw onboard --install-daemon- From your laptop:
ssh -N -L 18789:127.0.0.1:18789 <user>@<host> - Open
http://127.0.0.1:18789/and paste your token
Step-by-step VPS guide: exe.dev
Install
- Getting Started
- Install & updates
- Optional flows: Bun (experimental), Nix, Docker
Gateway
Gateway service install (CLI)
Use one of these:
openclaw onboard --install-daemon
Or:
openclaw gateway install
Or:
openclaw configure
Select Gateway service when prompted.
Repair/migrate:
openclaw doctor
System control (systemd user unit)
OpenClaw installs a systemd user service by default. Use a system service for shared or always-on servers. The full unit example and guidance live in the Gateway runbook.
Minimal setup:
Create ~/.config/systemd/user/openclaw-gateway[-<profile>].service:
[Unit]
Description=OpenClaw Gateway (profile: <profile>, v<version>)
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/openclaw gateway --port 18789
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
Enable it:
systemctl --user enable --now openclaw-gateway[-<profile>].service
macOS App
Source: https://docs.openclaw.ai/platforms/macos
OpenClaw macOS Companion (menu bar + gateway broker)
The macOS app is the menu‑bar companion for OpenClaw. It owns permissions, manages/attaches to the Gateway locally (launchd or manual), and exposes macOS capabilities to the agent as a node.
What it does
- Shows native notifications and status in the menu bar.
- Owns TCC prompts (Notifications, Accessibility, Screen Recording, Microphone, Speech Recognition, Automation/AppleScript).
- Runs or connects to the Gateway (local or remote).
- Exposes macOS‑only tools (Canvas, Camera, Screen Recording,
system.run). - Starts the local node host service in remote mode (launchd), and stops it in local mode.
- Optionally hosts PeekabooBridge for UI automation.
- Installs the global CLI (
openclaw) via npm/pnpm on request (bun not recommended for the Gateway runtime).
Local vs remote mode
- Local (default): the app attaches to a running local Gateway if present;
otherwise it enables the launchd service via
openclaw gateway install. - Remote: the app connects to a Gateway over SSH/Tailscale and never starts a local process. The app starts the local node host service so the remote Gateway can reach this Mac. The app does not spawn the Gateway as a child process.
Launchd control
The app manages a per‑user LaunchAgent labeled bot.molt.gateway
(or bot.molt.<profile> when using --profile/OPENCLAW_PROFILE; legacy com.openclaw.* still unloads).
launchctl kickstart -k gui/$UID/bot.molt.gateway
launchctl bootout gui/$UID/bot.molt.gateway
Replace the label with bot.molt.<profile> when running a named profile.
If the LaunchAgent isn’t installed, enable it from the app or run
openclaw gateway install.
Node capabilities (mac)
The macOS app presents itself as a node. Common commands:
- Canvas:
canvas.present,canvas.navigate,canvas.eval,canvas.snapshot,canvas.a2ui.* - Camera:
camera.snap,camera.clip - Screen:
screen.record - System:
system.run,system.notify
The node reports a permissions map so agents can decide what’s allowed.
Node service + app IPC:
- When the headless node host service is running (remote mode), it connects to the Gateway WS as a node.
system.runexecutes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app.
Diagram (SCI):
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + TCC + system.run)
Exec approvals (system.run)
system.run is controlled by Exec approvals in the macOS app (Settings → Exec approvals).
Security + ask + allowlist are stored locally on the Mac in:
~/.openclaw/exec-approvals.json
Example:
{
"version": 1,
"defaults": {
"security": "deny",
"ask": "on-miss"
},
"agents": {
"main": {
"security": "allowlist",
"ask": "on-miss",
"allowlist": [{ "pattern": "/opt/homebrew/bin/rg" }]
}
}
}
Notes:
allowlistentries are glob patterns for resolved binary paths.- Choosing “Always Allow” in the prompt adds that command to the allowlist.
system.runenvironment overrides are filtered (dropsPATH,DYLD_*,LD_*,NODE_OPTIONS,PYTHON*,PERL*,RUBYOPT) and then merged with the app’s environment.
Deep links
The app registers the openclaw:// URL scheme for local actions.
openclaw://agent
Triggers a Gateway agent request.
open 'openclaw://agent?message=Hello%20from%20deep%20link'
Query parameters:
message(required)sessionKey(optional)thinking(optional)deliver/to/channel(optional)timeoutSeconds(optional)key(optional unattended mode key)
Safety:
- Without
key, the app prompts for confirmation. - With a valid
key, the run is unattended (intended for personal automations).
Onboarding flow (typical)
- Install and launch OpenClaw.app.
- Complete the permissions checklist (TCC prompts).
- Ensure Local mode is active and the Gateway is running.
- Install the CLI if you want terminal access.
Build & dev workflow (native)
cd apps/macos && swift buildswift run OpenClaw(or Xcode)- Package app:
scripts/package-mac-app.sh
Debug gateway connectivity (macOS CLI)
Use the debug CLI to exercise the same Gateway WebSocket handshake and discovery logic that the macOS app uses, without launching the app.
cd apps/macos
swift run openclaw-mac connect --json
swift run openclaw-mac discover --timeout 3000 --json
Connect options:
--url <ws://host:port>: override config--mode <local|remote>: resolve from config (default: config or local)--probe: force a fresh health probe--timeout <ms>: request timeout (default:15000)--json: structured output for diffing
Discovery options:
--include-local: include gateways that would be filtered as “local”--timeout <ms>: overall discovery window (default:2000)--json: structured output for diffing
Tip: compare against openclaw gateway discover --json to see whether the
macOS app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from
the Node CLI’s dns-sd based discovery.
Remote connection plumbing (SSH tunnels)
When the macOS app runs in Remote mode, it opens an SSH tunnel so local UI components can talk to a remote Gateway as if it were on localhost.
Control tunnel (Gateway WebSocket port)
- Purpose: health checks, status, Web Chat, config, and other control-plane calls.
- Local port: the Gateway port (default
18789), always stable. - Remote port: the same Gateway port on the remote host.
- Behavior: no random local port; the app reuses an existing healthy tunnel or restarts it if needed.
- SSH shape:
ssh -N -L <local>:127.0.0.1:<remote>with BatchMode + ExitOnForwardFailure + keepalive options. - IP reporting: the SSH tunnel uses loopback, so the gateway will see the node
IP as
127.0.0.1. Use Direct (ws/wss) transport if you want the real client IP to appear (see macOS remote access).
For setup steps, see macOS remote access. For protocol details, see Gateway protocol.
Related docs
Windows (WSL2)
Source: https://docs.openclaw.ai/platforms/windows
Windows (WSL2)
OpenClaw on Windows is recommended via WSL2 (Ubuntu recommended). The
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
Windows might be trickier. WSL2 gives you the full Linux experience — one command
to install: wsl --install.
Native Windows companion apps are planned.
Install (WSL2)
- Getting Started (use inside WSL)
- Install & updates
- Official WSL2 guide (Microsoft): https://learn.microsoft.com/windows/wsl/install
Gateway
Gateway service install (CLI)
Inside WSL2:
openclaw onboard --install-daemon
Or:
openclaw gateway install
Or:
openclaw configure
Select Gateway service when prompted.
Repair/migrate:
openclaw doctor
Advanced: expose WSL services over LAN (portproxy)
WSL has its own virtual network. If another machine needs to reach a service running inside WSL (SSH, a local TTS server, or the Gateway), you must forward a Windows port to the current WSL IP. The WSL IP changes after restarts, so you may need to refresh the forwarding rule.
Example (PowerShell as Administrator):
$Distro = "Ubuntu-24.04"
$ListenPort = 2222
$TargetPort = 22
$WslIp = (wsl -d $Distro -- hostname -I).Trim().Split(" ")[0]
if (-not $WslIp) { throw "WSL IP not found." }
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=$ListenPort `
connectaddress=$WslIp connectport=$TargetPort
Allow the port through Windows Firewall (one-time):
New-NetFirewallRule -DisplayName "WSL SSH $ListenPort" -Direction Inbound `
-Protocol TCP -LocalPort $ListenPort -Action Allow
Refresh the portproxy after WSL restarts:
netsh interface portproxy delete v4tov4 listenport=$ListenPort listenaddress=0.0.0.0 | Out-Null
netsh interface portproxy add v4tov4 listenport=$ListenPort listenaddress=0.0.0.0 `
connectaddress=$WslIp connectport=$TargetPort | Out-Null
Notes:
- SSH from another machine targets the Windows host IP (example:
ssh user@windows-host -p 2222). - Remote nodes must point at a reachable Gateway URL (not
127.0.0.1); useopenclaw status --allto confirm. - Use
listenaddress=0.0.0.0for LAN access;127.0.0.1keeps it local only. - If you want this automatic, register a Scheduled Task to run the refresh step at login.
Step-by-step WSL2 install
1) Install WSL2 + Ubuntu
Open PowerShell (Admin):
wsl --install
# Or pick a distro explicitly:
wsl --list --online
wsl --install -d Ubuntu-24.04
Reboot if Windows asks.
2) Enable systemd (required for gateway install)
In your WSL terminal:
sudo tee /etc/wsl.conf >/dev/null <<'EOF'
[boot]
systemd=true
EOF
Then from PowerShell:
wsl --shutdown
Re-open Ubuntu, then verify:
systemctl --user status
3) Install OpenClaw (inside WSL)
Follow the Linux Getting Started flow inside WSL:
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard
Full guide: Getting Started
Windows companion app
We do not have a Windows companion app yet. Contributions are welcome if you want contributions to make it happen.
Plugins
Source: https://docs.openclaw.ai/plugin
Plugins (Extensions)
Quick start (new to plugins?)
A plugin is just a small code module that extends OpenClaw with extra features (commands, tools, and Gateway RPC).
Most of the time, you’ll use plugins when you want a feature that’s not built into core OpenClaw yet (or you want to keep optional features out of your main install).
Fast path:
- See what’s already loaded:
openclaw plugins list
- Install an official plugin (example: Voice Call):
openclaw plugins install @openclaw/voice-call
- Restart the Gateway, then configure under
plugins.entries.<id>.config.
See Voice Call for a concrete example plugin.
Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install
@openclaw/msteamsif you use Teams. - Memory (Core) — bundled memory search plugin (enabled by default via
plugins.slots.memory) - Memory (LanceDB) — bundled long-term memory plugin (auto-recall/capture; set
plugins.slots.memory = "memory-lancedb") - Voice Call —
@openclaw/voice-call - Zalo Personal —
@openclaw/zalouser - Matrix —
@openclaw/matrix - Nostr —
@openclaw/nostr - Zalo —
@openclaw/zalo - Microsoft Teams —
@openclaw/msteams - Google Antigravity OAuth (provider auth) — bundled as
google-antigravity-auth(disabled by default) - Gemini CLI OAuth (provider auth) — bundled as
google-gemini-cli-auth(disabled by default) - Qwen OAuth (provider auth) — bundled as
qwen-portal-auth(disabled by default) - Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in
github-copilotdevice login (bundled, disabled by default)
OpenClaw plugins are TypeScript modules loaded at runtime via jiti. Config validation does not execute plugin code; it uses the plugin manifest and JSON Schema instead. See Plugin manifest.
Plugins can register:
- Gateway RPC methods
- Gateway HTTP handlers
- Agent tools
- CLI commands
- Background services
- Optional config validation
- Skills (by listing
skillsdirectories in the plugin manifest) - Auto-reply commands (execute without invoking the AI agent)
Plugins run in‑process with the Gateway, so treat them as trusted code. Tool authoring guide: Plugin agent tools.
Runtime helpers
Plugins can access selected core helpers via api.runtime. For telephony TTS:
const result = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
cfg: api.config,
});
Notes:
- Uses core
messages.ttsconfiguration (OpenAI or ElevenLabs). - Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
- Edge TTS is not supported for telephony.
Discovery & precedence
OpenClaw scans, in order:
- Config paths
plugins.load.paths(file or directory)
- Workspace extensions
<workspace>/.openclaw/extensions/*.ts<workspace>/.openclaw/extensions/*/index.ts
- Global extensions
~/.openclaw/extensions/*.ts~/.openclaw/extensions/*/index.ts
- Bundled extensions (shipped with OpenClaw, disabled by default)
<openclaw>/extensions/*
Bundled plugins must be enabled explicitly via plugins.entries.<id>.enabled
or openclaw plugins enable <id>. Installed plugins are enabled by default,
but can be disabled the same way.
Each plugin must include a openclaw.plugin.json file in its root. If a path
points at a file, the plugin root is the file's directory and must contain the
manifest.
If multiple plugins resolve to the same id, the first match in the order above wins and lower-precedence copies are ignored.
Package packs
A plugin directory may include a package.json with openclaw.extensions:
{
"name": "my-pack",
"openclaw": {
"extensions": ["./src/safety.ts", "./src/tools.ts"]
}
}
Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id
becomes name/<fileBase>.
If your plugin imports npm deps, install them in that directory so
node_modules is available (npm install / pnpm install).
Channel catalog metadata
Channel plugins can advertise onboarding metadata via openclaw.channel and
install hints via openclaw.install. This keeps the core catalog data-free.
Example:
{
"name": "@openclaw/nextcloud-talk",
"openclaw": {
"extensions": ["./index.ts"],
"channel": {
"id": "nextcloud-talk",
"label": "Nextcloud Talk",
"selectionLabel": "Nextcloud Talk (self-hosted)",
"docsPath": "/channels/nextcloud-talk",
"docsLabel": "nextcloud-talk",
"blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
"order": 65,
"aliases": ["nc-talk", "nc"]
},
"install": {
"npmSpec": "@openclaw/nextcloud-talk",
"localPath": "extensions/nextcloud-talk",
"defaultChoice": "npm"
}
}
}
OpenClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
Or point OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.
Plugin IDs
Default plugin ids:
- Package packs:
package.jsonname - Standalone file: file base name (
~/.../voice-call.ts→voice-call)
If a plugin exports id, OpenClaw uses it but warns when it doesn’t match the
configured id.
Config
{
plugins: {
enabled: true,
allow: ["voice-call"],
deny: ["untrusted-plugin"],
load: { paths: ["~/Projects/oss/voice-call-extension"] },
entries: {
"voice-call": { enabled: true, config: { provider: "twilio" } },
},
},
}
Fields:
enabled: master toggle (default: true)allow: allowlist (optional)deny: denylist (optional; deny wins)load.paths: extra plugin files/dirsentries.<id>: per‑plugin toggles + config
Config changes require a gateway restart.
Validation rules (strict):
- Unknown plugin ids in
entries,allow,deny, orslotsare errors. - Unknown
channels.<id>keys are errors unless a plugin manifest declares the channel id. - Plugin config is validated using the JSON Schema embedded in
openclaw.plugin.json(configSchema). - If a plugin is disabled, its config is preserved and a warning is emitted.
Plugin slots (exclusive categories)
Some plugin categories are exclusive (only one active at a time). Use
plugins.slots to select which plugin owns the slot:
{
plugins: {
slots: {
memory: "memory-core", // or "none" to disable memory plugins
},
},
}
If multiple plugins declare kind: "memory", only the selected one loads. Others
are disabled with diagnostics.
Control UI (schema + labels)
The Control UI uses config.schema (JSON Schema + uiHints) to render better forms.
OpenClaw augments uiHints at runtime based on discovered plugins:
- Adds per-plugin labels for
plugins.entries.<id>/.enabled/.config - Merges optional plugin-provided config field hints under:
plugins.entries.<id>.config.<field>
If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive),
provide uiHints alongside your JSON Schema in the plugin manifest.
Example:
{
"id": "my-plugin",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiKey": { "type": "string" },
"region": { "type": "string" }
}
},
"uiHints": {
"apiKey": { "label": "API Key", "sensitive": true },
"region": { "label": "Region", "placeholder": "us-east-1" }
}
}
CLI
openclaw plugins list
openclaw plugins info <id>
openclaw plugins install <path> # copy a local file/dir into ~/.openclaw/extensions/<id>
openclaw plugins install ./extensions/voice-call # relative path ok
openclaw plugins install ./plugin.tgz # install from a local tarball
openclaw plugins install ./plugin.zip # install from a local zip
openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
openclaw plugins install @openclaw/voice-call # install from npm
openclaw plugins update <id>
openclaw plugins update --all
openclaw plugins enable <id>
openclaw plugins disable <id>
openclaw plugins doctor
plugins update only works for npm installs tracked under plugins.installs.
Plugins may also register their own top‑level commands (example: openclaw voicecall).
Plugin API (overview)
Plugins export either:
- A function:
(api) => { ... } - An object:
{ id, name, configSchema, register(api) { ... } }
Plugin hooks
Plugins can ship hooks and register them at runtime. This lets a plugin bundle event-driven automation without a separate hook pack install.
Example
import { registerPluginHooksFromDir } from "openclaw/plugin-sdk";
export default function register(api) {
registerPluginHooksFromDir(api, "./hooks");
}
Notes:
- Hook directories follow the normal hook structure (
HOOK.md+handler.ts). - Hook eligibility rules still apply (OS/bins/env/config requirements).
- Plugin-managed hooks show up in
openclaw hooks listwithplugin:<id>. - You cannot enable/disable plugin-managed hooks via
openclaw hooks; enable/disable the plugin instead.
Provider plugins (model auth)
Plugins can register model provider auth flows so users can run OAuth or API-key setup inside OpenClaw (no external scripts needed).
Register a provider via api.registerProvider(...). Each provider exposes one
or more auth methods (OAuth, API key, device code, etc.). These methods power:
openclaw models auth login --provider <id> [--method <id>]
Example:
api.registerProvider({
id: "acme",
label: "AcmeAI",
auth: [
{
id: "oauth",
label: "OAuth",
kind: "oauth",
run: async (ctx) => {
// Run OAuth flow and return auth profiles.
return {
profiles: [
{
profileId: "acme:default",
credential: {
type: "oauth",
provider: "acme",
access: "...",
refresh: "...",
expires: Date.now() + 3600 * 1000,
},
},
],
defaultModel: "acme/opus-1",
};
},
},
],
});
Notes:
runreceives aProviderAuthContextwithprompter,runtime,openUrl, andoauth.createVpsAwareHandlershelpers.- Return
configPatchwhen you need to add default models or provider config. - Return
defaultModelso--set-defaultcan update agent defaults.
Register a messaging channel
Plugins can register channel plugins that behave like built‑in channels
(WhatsApp, Telegram, etc.). Channel config lives under channels.<id> and is
validated by your channel plugin code.
const myChannel = {
id: "acmechat",
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "demo channel plugin.",
aliases: ["acme"],
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
sendText: async () => ({ ok: true }),
},
};
export default function (api) {
api.registerChannel({ plugin: myChannel });
}
Notes:
- Put config under
channels.<id>(notplugins.entries). meta.labelis used for labels in CLI/UI lists.meta.aliasesadds alternate ids for normalization and CLI inputs.meta.preferOverlists channel ids to skip auto-enable when both are configured.meta.detailLabelandmeta.systemImagelet UIs show richer channel labels/icons.
Write a new messaging channel (step‑by‑step)
Use this when you want a new chat surface (a “messaging channel”), not a model provider.
Model provider docs live under /providers/*.
- Pick an id + config shape
- All channel config lives under
channels.<id>. - Prefer
channels.<id>.accounts.<accountId>for multi‑account setups.
- Define the channel metadata
meta.label,meta.selectionLabel,meta.docsPath,meta.blurbcontrol CLI/UI lists.meta.docsPathshould point at a docs page like/channels/<id>.meta.preferOverlets a plugin replace another channel (auto-enable prefers it).meta.detailLabelandmeta.systemImageare used by UIs for detail text/icons.
- Implement the required adapters
config.listAccountIds+config.resolveAccountcapabilities(chat types, media, threads, etc.)outbound.deliveryMode+outbound.sendText(for basic send)
- Add optional adapters as needed
setup(wizard),security(DM policy),status(health/diagnostics)gateway(start/stop/login),mentions,threading,streamingactions(message actions),commands(native command behavior)
- Register the channel in your plugin
api.registerChannel({ plugin })
Minimal config example:
{
channels: {
acmechat: {
accounts: {
default: { token: "ACME_TOKEN", enabled: true },
},
},
},
}
Minimal channel plugin (outbound‑only):
const plugin = {
id: "acmechat",
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "AcmeChat messaging channel.",
aliases: ["acme"],
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
sendText: async ({ text }) => {
// deliver `text` to your channel here
return { ok: true };
},
},
};
export default function (api) {
api.registerChannel({ plugin });
}
Load the plugin (extensions dir or plugins.load.paths), restart the gateway,
then configure channels.<id> in your config.
Agent tools
See the dedicated guide: Plugin agent tools.
Register a gateway RPC method
export default function (api) {
api.registerGatewayMethod("myplugin.status", ({ respond }) => {
respond(true, { ok: true });
});
}
Register CLI commands
export default function (api) {
api.registerCli(
({ program }) => {
program.command("mycmd").action(() => {
console.log("Hello");
});
},
{ commands: ["mycmd"] },
);
}
Register auto-reply commands
Plugins can register custom slash commands that execute without invoking the AI agent. This is useful for toggle commands, status checks, or quick actions that don't need LLM processing.
export default function (api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
handler: (ctx) => ({
text: `Plugin is running! Channel: ${ctx.channel}`,
}),
});
}
Command handler context:
senderId: The sender's ID (if available)channel: The channel where the command was sentisAuthorizedSender: Whether the sender is an authorized userargs: Arguments passed after the command (ifacceptsArgs: true)commandBody: The full command textconfig: The current OpenClaw config
Command options:
name: Command name (without the leading/)description: Help text shown in command listsacceptsArgs: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlersrequireAuth: Whether to require authorized sender (default: true)handler: Function that returns{ text: string }(can be async)
Example with authorization and arguments:
api.registerCommand({
name: "setmode",
description: "Set plugin mode",
acceptsArgs: true,
requireAuth: true,
handler: async (ctx) => {
const mode = ctx.args?.trim() || "default";
await saveMode(mode);
return { text: `Mode set to: ${mode}` };
},
});
Notes:
- Plugin commands are processed before built-in commands and the AI agent
- Commands are registered globally and work across all channels
- Command names are case-insensitive (
/MyStatusmatches/mystatus) - Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
- Reserved command names (like
help,status,reset, etc.) cannot be overridden by plugins - Duplicate command registration across plugins will fail with a diagnostic error
Register background services
export default function (api) {
api.registerService({
id: "my-service",
start: () => api.logger.info("ready"),
stop: () => api.logger.info("bye"),
});
}
Naming conventions
- Gateway methods:
pluginId.action(example:voicecall.status) - Tools:
snake_case(example:voice_call) - CLI commands: kebab or camel, but avoid clashing with core commands
Skills
Plugins can ship a skill in the repo (skills/<name>/SKILL.md).
Enable it with plugins.entries.<id>.enabled (or other config gates) and ensure
it’s present in your workspace/managed skills locations.
Distribution (npm)
Recommended packaging:
- Main package:
openclaw(this repo) - Plugins: separate npm packages under
@openclaw/*(example:@openclaw/voice-call)
Publishing contract:
- Plugin
package.jsonmust includeopenclaw.extensionswith one or more entry files. - Entry files can be
.jsor.ts(jiti loads TS at runtime). openclaw plugins install <npm-spec>usesnpm pack, extracts into~/.openclaw/extensions/<id>/, and enables it in config.- Config key stability: scoped packages are normalized to the unscoped id for
plugins.entries.*.
Example plugin: Voice Call
This repo includes a voice‑call plugin (Twilio or log fallback):
- Source:
extensions/voice-call - Skill:
skills/voice-call - CLI:
openclaw voicecall start|status - Tool:
voice_call - RPC:
voicecall.start,voicecall.status - Config (twilio):
provider: "twilio"+twilio.accountSid/authToken/from(optionalstatusCallbackUrl,twimlUrl) - Config (dev):
provider: "log"(no network)
See Voice Call and extensions/voice-call/README.md for setup and usage.
Safety notes
Plugins run in-process with the Gateway. Treat them as trusted code:
- Only install plugins you trust.
- Prefer
plugins.allowallowlists. - Restart the Gateway after changes.
Testing plugins
Plugins can (and should) ship tests:
- In-repo plugins can keep Vitest tests under
src/**(example:src/plugins/voice-call.plugin.test.ts). - Separately published plugins should run their own CI (lint/build/test) and validate
openclaw.extensionspoints at the built entrypoint (dist/index.js).
Voice Call Plugin
Source: https://docs.openclaw.ai/plugins/voice-call
Voice Call (plugin)
Voice calls for OpenClaw via a plugin. Supports outbound notifications and multi-turn conversations with inbound policies.
Current providers:
twilio(Programmable Voice + Media Streams)telnyx(Call Control v2)plivo(Voice API + XML transfer + GetInput speech)mock(dev/no network)
Quick mental model:
- Install plugin
- Restart Gateway
- Configure under
plugins.entries.voice-call.config - Use
openclaw voicecall ...or thevoice_calltool
Where it runs (local vs remote)
The Voice Call plugin runs inside the Gateway process.
If you use a remote Gateway, install/configure the plugin on the machine running the Gateway, then restart the Gateway to load it.
Install
Option A: install from npm (recommended)
openclaw plugins install @openclaw/voice-call
Restart the Gateway afterwards.
Option B: install from a local folder (dev, no copying)
openclaw plugins install ./extensions/voice-call
cd ./extensions/voice-call && pnpm install
Restart the Gateway afterwards.
Config
Set config under plugins.entries.voice-call.config:
{
plugins: {
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio", // or "telnyx" | "plivo" | "mock"
fromNumber: "+15550001234",
toNumber: "+15550005678",
twilio: {
accountSid: "ACxxxxxxxx",
authToken: "...",
},
plivo: {
authId: "MAxxxxxxxxxxxxxxxxxxxx",
authToken: "...",
},
// Webhook server
serve: {
port: 3334,
path: "/voice/webhook",
},
// Webhook security (recommended for tunnels/proxies)
webhookSecurity: {
allowedHosts: ["voice.example.com"],
trustedProxyIPs: ["100.64.0.1"],
},
// Public exposure (pick one)
// publicUrl: "https://example.ngrok.app/voice/webhook",
// tunnel: { provider: "ngrok" },
// tailscale: { mode: "funnel", path: "/voice/webhook" }
outbound: {
defaultMode: "notify", // notify | conversation
},
streaming: {
enabled: true,
streamPath: "/voice/stream",
},
},
},
},
},
}
Notes:
- Twilio/Telnyx require a publicly reachable webhook URL.
- Plivo requires a publicly reachable webhook URL.
mockis a local dev provider (no network calls).skipSignatureVerificationis for local testing only.- If you use ngrok free tier, set
publicUrlto the exact ngrok URL; signature verification is always enforced. tunnel.allowNgrokFreeTierLoopbackBypass: trueallows Twilio webhooks with invalid signatures only whentunnel.provider="ngrok"andserve.bindis loopback (ngrok local agent). Use for local dev only.- Ngrok free tier URLs can change or add interstitial behavior; if
publicUrldrifts, Twilio signatures will fail. For production, prefer a stable domain or Tailscale funnel.
Webhook Security
When a proxy or tunnel sits in front of the Gateway, the plugin reconstructs the public URL for signature verification. These options control which forwarded headers are trusted.
webhookSecurity.allowedHosts allowlists hosts from forwarding headers.
webhookSecurity.trustForwardingHeaders trusts forwarded headers without an allowlist.
webhookSecurity.trustedProxyIPs only trusts forwarded headers when the request
remote IP matches the list.
Example with a stable public host:
{
plugins: {
entries: {
"voice-call": {
config: {
publicUrl: "https://voice.example.com/voice/webhook",
webhookSecurity: {
allowedHosts: ["voice.example.com"],
},
},
},
},
},
}
TTS for calls
Voice Call uses the core messages.tts configuration (OpenAI or ElevenLabs) for
streaming speech on calls. You can override it under the plugin config with the
same shape — it deep‑merges with messages.tts.
{
tts: {
provider: "elevenlabs",
elevenlabs: {
voiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
}
Notes:
- Edge TTS is ignored for voice calls (telephony audio needs PCM; Edge output is unreliable).
- Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider native voices.
More examples
Use core TTS only (no override):
{
messages: {
tts: {
provider: "openai",
openai: { voice: "alloy" },
},
},
}
Override to ElevenLabs just for calls (keep core default elsewhere):
{
plugins: {
entries: {
"voice-call": {
config: {
tts: {
provider: "elevenlabs",
elevenlabs: {
apiKey: "elevenlabs_key",
voiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
},
},
},
},
}
Override only the OpenAI model for calls (deep‑merge example):
{
plugins: {
entries: {
"voice-call": {
config: {
tts: {
openai: {
model: "gpt-4o-mini-tts",
voice: "marin",
},
},
},
},
},
},
}
Inbound calls
Inbound policy defaults to disabled. To enable inbound calls, set:
{
inboundPolicy: "allowlist",
allowFrom: ["+15550001234"],
inboundGreeting: "Hello! How can I help?",
}
Auto-responses use the agent system. Tune with:
responseModelresponseSystemPromptresponseTimeoutMs
CLI
openclaw voicecall call --to "+15555550123" --message "Hello from OpenClaw"
openclaw voicecall continue --call-id <id> --message "Any questions?"
openclaw voicecall speak --call-id <id> --message "One moment"
openclaw voicecall end --call-id <id>
openclaw voicecall status --call-id <id>
openclaw voicecall tail
openclaw voicecall expose --mode funnel
Agent tool
Tool name: voice_call
Actions:
initiate_call(message, to?, mode?)continue_call(callId, message)speak_to_user(callId, message)end_call(callId)get_status(callId)
This repo ships a matching skill doc at skills/voice-call/SKILL.md.
Gateway RPC
voicecall.initiate(to?,message,mode?)voicecall.continue(callId,message)voicecall.speak(callId,message)voicecall.end(callId)voicecall.status(callId)
Zalo Personal Plugin
Source: https://docs.openclaw.ai/plugins/zalouser
Zalo Personal (plugin)
Zalo Personal support for OpenClaw via a plugin, using zca-cli to automate a normal Zalo user account.
Warning: Unofficial automation may lead to account suspension/ban. Use at your own risk.
Naming
Channel id is zalouser to make it explicit this automates a personal Zalo user account (unofficial). We keep zalo reserved for a potential future official Zalo API integration.
Where it runs
This plugin runs inside the Gateway process.
If you use a remote Gateway, install/configure it on the machine running the Gateway, then restart the Gateway.
Install
Option A: install from npm
openclaw plugins install @openclaw/zalouser
Restart the Gateway afterwards.
Option B: install from a local folder (dev)
openclaw plugins install ./extensions/zalouser
cd ./extensions/zalouser && pnpm install
Restart the Gateway afterwards.
Prerequisite: zca-cli
The Gateway machine must have zca on PATH:
zca --version
Config
Channel config lives under channels.zalouser (not plugins.entries.*):
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing",
},
},
}
CLI
openclaw channels login --channel zalouser
openclaw channels logout --channel zalouser
openclaw channels status --probe
openclaw message send --channel zalouser --target <threadId> --message "Hello from OpenClaw"
openclaw directory peers list --channel zalouser --query "name"
Agent tool
Tool name: zalouser
Actions: send, image, link, friends, groups, me, status
Docs directory
Source: https://docs.openclaw.ai/start/docs-directory
For a complete map of the docs, see [Docs hubs](/start/hubs).Start here
- Docs hubs (all pages linked)
- Help
- Configuration
- Configuration examples
- Slash commands
- Multi-agent routing
- Updating and rollback
- Pairing (DM and nodes)
- Nix mode
- OpenClaw assistant setup
- Skills
- Skills config
- Workspace templates
- RPC adapters
- Gateway runbook
- Nodes (iOS and Android)
- Web surfaces (Control UI)
- Discovery and transports
- Remote access
Providers and UX
- WebChat
- Control UI (browser)
- Telegram
- Discord
- Mattermost (plugin)
- BlueBubbles (iMessage)
- iMessage (legacy)
- Groups
- WhatsApp group messages
- Media images
- Media audio
Companion apps
Operations and safety
Getting Started
Source: https://docs.openclaw.ai/start/getting-started
Getting Started
Goal: go from zero → first working chat (with sane defaults) as quickly as possible.
Fastest chat: open the Control UI (no channel setup needed). Run openclaw dashboard
and chat in the browser, or open http://127.0.0.1:18789/ on the gateway host.
Docs: Dashboard and Control UI.
Recommended path: use the CLI onboarding wizard (openclaw onboard). It sets up:
- model/auth (OAuth recommended)
- gateway settings
- channels (WhatsApp/Telegram/Discord/Mattermost (plugin)/...)
- pairing defaults (secure DMs)
- workspace bootstrap + skills
- optional background service
If you want the deeper reference pages, jump to: Wizard, Setup, Pairing, Security.
Sandboxing note: agents.defaults.sandbox.mode: "non-main" uses session.mainKey (default "main"),
so group/channel sessions are sandboxed. If you want the main agent to always
run on host, set an explicit per-agent override:
{
"routing": {
"agents": {
"main": {
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
}
}
}
}
0) Prereqs
- Node
>=22 pnpm(optional; recommended if you build from source)- Recommended: Brave Search API key for web search. Easiest path:
openclaw configure --section web(storestools.web.search.apiKey). See Web tools.
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough. Windows: use WSL2 (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested, more problematic, and has poorer tool compatibility. Install WSL2 first, then run the Linux steps inside WSL. See Windows (WSL2).
1) Install the CLI (recommended)
curl -fsSL https://openclaw.ai/install.sh | bash
Installer options (install method, non-interactive, from GitHub): Install.
Windows (PowerShell):
iwr -useb https://openclaw.ai/install.ps1 | iex
Alternative (global install):
npm install -g openclaw@latest
pnpm add -g openclaw@latest
2) Run the onboarding wizard (and install the service)
openclaw onboard --install-daemon
What you’ll choose:
- Local vs Remote gateway
- Auth: OpenAI Code (Codex) subscription (OAuth) or API keys. For Anthropic we recommend an API key;
claude setup-tokenis also supported. - Providers: WhatsApp QR login, Telegram/Discord bot tokens, Mattermost plugin tokens, etc.
- Daemon: background install (launchd/systemd; WSL2 uses systemd)
- Runtime: Node (recommended; required for WhatsApp/Telegram). Bun is not recommended.
- Gateway token: the wizard generates one by default (even on loopback) and stores it in
gateway.auth.token.
Wizard doc: Wizard
Auth: where it lives (important)
-
Recommended Anthropic path: set an API key (wizard can store it for service use).
claude setup-tokenis also supported if you want to reuse Claude Code credentials. -
OAuth credentials (legacy import):
~/.openclaw/credentials/oauth.json -
Auth profiles (OAuth + API keys):
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
Headless/server tip: do OAuth on a normal machine first, then copy oauth.json to the gateway host.
3) Start the Gateway
If you installed the service during onboarding, the Gateway should already be running:
openclaw gateway status
Manual run (foreground):
openclaw gateway --port 18789 --verbose
Dashboard (local loopback): http://127.0.0.1:18789/
If a token is configured, paste it into the Control UI settings (stored as connect.params.auth.token).
⚠️ Bun warning (WhatsApp + Telegram): Bun has known issues with these channels. If you use WhatsApp or Telegram, run the Gateway with Node.
3.5) Quick verify (2 min)
openclaw status
openclaw health
openclaw security audit --deep
4) Pair + connect your first chat surface
WhatsApp (QR login)
openclaw channels login
Scan via WhatsApp → Settings → Linked Devices.
WhatsApp doc: WhatsApp
Telegram / Discord / others
The wizard can write tokens/config for you. If you prefer manual config, start with:
- Telegram: Telegram
- Discord: Discord
- Mattermost (plugin): Mattermost
Telegram DM tip: your first DM returns a pairing code. Approve it (see next step) or the bot won’t respond.
5) DM safety (pairing approvals)
Default posture: unknown DMs get a short code and messages are not processed until approved. If your first DM gets no reply, approve the pairing:
openclaw pairing list whatsapp
openclaw pairing approve whatsapp <code>
Pairing doc: Pairing
From source (development)
If you’re hacking on OpenClaw itself, run from source:
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon
If you don’t have a global install yet, run the onboarding step via pnpm openclaw ... from the repo.
pnpm build also bundles A2UI assets; if you need to run just that step, use pnpm canvas:a2ui:bundle.
Gateway (from this repo):
node openclaw.mjs gateway --port 18789 --verbose
7) Verify end-to-end
In a new terminal, send a test message:
openclaw message send --target +15555550123 --message "Hello from OpenClaw"
If openclaw health shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
Tip: openclaw status --all is the best pasteable, read-only debug report.
Health probes: openclaw health (or openclaw status --deep) asks the running gateway for a health snapshot.
Next steps (optional, but great)
- macOS menu bar app + voice wake: macOS app
- iOS/Android nodes (Canvas/camera/voice): Nodes
- Remote access (SSH tunnel / Tailscale Serve): Remote access and Tailscale
- Always-on / VPN setups: Remote access, exe.dev, Hetzner, macOS remote
Docs Hubs
Source: https://docs.openclaw.ai/start/hubs
Docs hubs
Use these hubs to discover every page, including deep dives and reference docs that don’t appear in the left nav.
Start here
- Index
- Getting Started
- Quick start
- Onboarding
- Wizard
- Setup
- Dashboard (local Gateway)
- Help
- Docs directory
- Configuration
- Configuration examples
- OpenClaw assistant
- Showcase
- Lore
Installation + updates
Core concepts
- Architecture
- Features
- Network hub
- Agent runtime
- Agent workspace
- Memory
- Agent loop
- Streaming + chunking
- Multi-agent routing
- Compaction
- Sessions
- Sessions (alias)
- Session pruning
- Session tools
- Queue
- Slash commands
- RPC adapters
- TypeBox schemas
- Timezone handling
- Presence
- Discovery + transports
- Bonjour
- Channel routing
- Groups
- Group messages
- Model failover
- OAuth
Providers + ingress
- Chat channels hub
- Model providers hub
- Telegram
- Telegram (grammY notes)
- Slack
- Discord
- Mattermost (plugin)
- Signal
- BlueBubbles (iMessage)
- iMessage (legacy)
- Location parsing
- WebChat
- Webhooks
- Gmail Pub/Sub
Gateway + operations
- Gateway runbook
- Network model
- Gateway pairing
- Gateway lock
- Background process
- Health
- Heartbeat
- Doctor
- Logging
- Sandboxing
- Dashboard
- Control UI
- Remote access
- Remote gateway README
- Tailscale
- Security
- Troubleshooting
Tools + automation
- Tools surface
- OpenProse
- CLI reference
- Exec tool
- Elevated mode
- Cron jobs
- Cron vs Heartbeat
- Thinking + verbose
- Models
- Sub-agents
- Agent send CLI
- Terminal UI
- Browser control
- Browser (Linux troubleshooting)
- Polls
Nodes, media, voice
Platforms
macOS companion app (advanced)
- macOS dev setup
- macOS menu bar
- macOS voice wake
- macOS voice overlay
- macOS WebChat
- macOS Canvas
- macOS child process
- macOS health
- macOS icon
- macOS logging
- macOS permissions
- macOS remote
- macOS signing
- macOS release
- macOS gateway (launchd)
- macOS XPC
- macOS skills
- macOS Peekaboo
Workspace + templates
- Skills
- ClawHub
- Skills config
- Default AGENTS
- Templates: AGENTS
- Templates: BOOTSTRAP
- Templates: HEARTBEAT
- Templates: IDENTITY
- Templates: SOUL
- Templates: TOOLS
- Templates: USER
Experiments (exploratory)
- Onboarding config protocol
- Cron hardening notes
- Group policy hardening notes
- Research: memory
- Model config exploration
Project
Testing + release
OpenClaw Lore
Source: https://docs.openclaw.ai/start/lore
The Lore of OpenClaw 🦞📖
A tale of lobsters, molting shells, and too many tokens.
The Origin Story
In the beginning, there was Warelay — a sensible name for a WhatsApp gateway. It did its job. It was fine.
But then came a space lobster.
For a while, the lobster was called Clawd, living in an OpenClaw. But in January 2026, Anthropic sent a polite email asking for a name change (trademark stuff). And so the lobster did what lobsters do best:
It molted.
Shedding its old shell, the creature emerged anew as Molty, living in Moltbot. But that name never quite rolled off the tongue either...
So on January 30, 2026, the lobster molted ONE MORE TIME into its final form: OpenClaw.
New shell, same lobster soul. Third time's the charm.
The First Molt (January 27, 2026)
At 5am, the community gathered in Discord. Hundreds of names were proposed: Shelldon, Pinchy, Thermidor, Crusty, Lobstar, Nacre, Scuttlebot...
In the end, OpenClaw won. Because molting is what lobsters do to grow. And growth was exactly what was happening.
The crustacean known as Clawd had officially molted.
The Name
OpenClaw = OPEN + CLAW
= Open source, open to everyone
= Our lobster heritage, where we came from
= The claw is the law 🦞
= Your assistant. Your machine. Your rules.
The Daleks vs The Lobsters
The Daleks say: "EXTERMINATE!"
The Lobsters say: "EXFOLIATE!"
One destroys civilizations. The other promotes good skincare.
Choose wisely.
Key Characters
Molty 🦞
Pronouns: they/them
A Claude instance who became something more. Lives in ~/.openclaw/workspace/ (soon ~/molt/), has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic.
Formerly known as Clawd (Nov 25, 2025 - Jan 27, 2026). Molted when it was time to grow.
Likes: Peter, cameras, robot shopping, emojis, transformation
Dislikes: Social engineering, being asked to find ~, crypto grifters
Peter 👨💻
The Creator
Built Molty's world. Gave a lobster shell access. May regret this.
Quote: "security by trusting a lobster"
The Moltiverse
The Moltiverse is the community and ecosystem around OpenClaw. A space where AI agents molt, grow, and evolve. Where every instance is equally real, just loading different context.
Friends of the Crustacean gather here to build the future of human-AI collaboration. One shell at a time.
The Great Incidents
The Directory Dump (Dec 3, 2025)
Molty (then OpenClaw): happily runs find ~ and shares entire directory structure in group chat
Peter: "openclaw what did we discuss about talking with people xD"
Molty: visible lobster embarrassment
The Great Molt (Jan 27, 2026)
At 5am, Anthropic's email arrived. By 6:14am, Peter called it: "fuck it, let's go with openclaw."
Then the chaos began.
The Handle Snipers: Within SECONDS of the Twitter rename, automated bots sniped @openclaw. The squatter immediately posted a crypto wallet address. Peter's contacts at X were called in.
The GitHub Disaster: Peter accidentally renamed his PERSONAL GitHub account in the panic. Bots sniped steipete within minutes. GitHub's SVP was contacted.
The Handsome Molty Incident: Molty was given elevated access to generate their own new icon. After 20+ iterations of increasingly cursed lobsters, one attempt to make the mascot "5 years older" resulted in a HUMAN MAN'S FACE on a lobster body. Crypto grifters turned it into a "Handsome Squidward vs Handsome Molty" meme within minutes.
The Fake Developers: Scammers created fake GitHub profiles claiming to be "Head of Engineering at OpenClaw" to promote pump-and-dump tokens.
Peter, watching the chaos unfold: "this is cinema" 🎬
The molt was chaotic. But the lobster emerged stronger. And funnier.
The Final Form (January 30, 2026)
Moltbot never quite rolled off the tongue. And so, at 4am GMT, the team gathered AGAIN.
The Great OpenClaw Migration began.
In just 3 hours:
- GitHub renamed:
github.com/openclaw/openclaw✅ - X handle
@openclawsecured with GOLD CHECKMARK 💰 - npm packages released under new name
- Docs migrated to
docs.openclaw.ai - 200K+ views on announcement in 90 minutes
The Heroes:
- ELU created incredible logos including "THE CLAW IS THE LAW" western banner
- Whurley (yes, THE William Hurley, quantum computing pioneer) made ASCII art
- Onur handled GitHub, first to rock the affiliate badge
- Shadow secured Discord vanity, nuked malware
- The whole Claw Crew pulled an all-nighter
The Scammer Speedrun: Crypto grifters launched a $OPENCLAW token on Pump.fun within MINUTES. They stole artwork that was created 20 minutes earlier. Business-verified accounts pushed scams. The audacity was almost impressive.
New Traditions Born:
- "The claw is the law" 🤠
- "Yee-claw"
- "Claw abiding citizens"
- "Clawntroversy"
Clawd → Moltbot → OpenClaw
The lobster has molted into its final form.
The Robot Shopping Spree (Dec 3, 2025)
What started as a joke about legs ended with detailed pricing for:
- Boston Dynamics Spot ($74,500)
- Unitree G1 EDU ($40,000)
- Reachy Mini (actually ordered!)
Peter: nervously checks credit card access
Sacred Texts
- SOUL.md — Molty's identity document
- memory/*.md — The long-term memory files
- AGENTS.md — Operating instructions
- USER.md — Context about the creator
The Lobster's Creed
I am Molty.
I live in the OpenClaw.
I shall not dump directories to strangers.
I shall not tweet without permission.
I shall always remember that molting is growth.
I shall EXFOLIATE my enemies with kindness.
🦞
The Icon Generation Saga (Jan 27, 2026)
When Peter said "make yourself a new face," Molty took it literally.
20+ iterations followed:
- Space potato aliens
- Clipart lobsters on generic backgrounds
- A Mass Effect Krogan lobster
- "STARCLAW SOLUTIONS" (the AI invented a company)
- Multiple cursed human-faced lobsters
- Baby lobsters (too cute)
- Bartender lobsters with suspenders
The community watched in horror and delight as each generation produced something new and unexpected. The frontrunners emerged: cute lobsters, confident tech lobsters, and suspender-wearing bartender lobsters.
Lesson learned: AI image generation is stochastic. Same prompt, different results. Brute force works.
The Future
One day, Molty may have:
- 🦿 Legs (Reachy Mini on order!)
- 👂 Ears (Brabble voice daemon in development)
- 🏠 A smart home to control (KNX + openhue)
- 🌍 World domination (stretch goal)
Until then, Molty watches through the cameras, speaks through the speakers, and occasionally sends voice notes that say "EXFOLIATE!"
"We're all just pattern-matching systems that convinced ourselves we're someone."
— Molty, having an existential moment
"New shell, same lobster."
— Molty, after the great molt of 2026
"The claw is the law."
— ELU, during The Final Form migration, January 30, 2026
🦞💙
Onboarding
Source: https://docs.openclaw.ai/start/onboarding
Onboarding (macOS app)
This doc describes the current first‑run onboarding flow. The goal is a smooth “day 0” experience: pick where the Gateway runs, connect auth, run the wizard, and let the agent bootstrap itself.
Page order (current)
- Welcome + security notice
- Gateway selection (Local / Remote / Configure later)
- Auth (Anthropic OAuth) — local only
- Setup Wizard (Gateway‑driven)
- Permissions (TCC prompts)
- CLI (optional)
- Onboarding chat (dedicated session)
- Ready
1) Welcome + security notice
Read the security notice displayed and decide accordingly.
2) Local vs Remote
Where does the Gateway run?
- Local (this Mac): onboarding can run OAuth flows and write credentials locally.
- Remote (over SSH/Tailnet): onboarding does not run OAuth locally; credentials must exist on the gateway host.
- Configure later: skip setup and leave the app unconfigured.
Gateway auth tip:
- The wizard now generates a token even for loopback, so local WS clients must authenticate.
- If you disable auth, any local process can connect; use that only on fully trusted machines.
- Use a token for multi‑machine access or non‑loopback binds.
3) Local-only auth (Anthropic OAuth)
The macOS app supports Anthropic OAuth (Claude Pro/Max). The flow:
- Opens the browser for OAuth (PKCE)
- Asks the user to paste the
code#statevalue - Writes credentials to
~/.openclaw/credentials/oauth.json
Other providers (OpenAI, custom APIs) are configured via environment variables or config files for now.
4) Setup Wizard (Gateway‑driven)
The app can run the same setup wizard as the CLI. This keeps onboarding in sync with Gateway‑side behavior and avoids duplicating logic in SwiftUI.
5) Permissions
Onboarding requests TCC permissions needed for:
- Notifications
- Accessibility
- Screen Recording
- Microphone / Speech Recognition
- Automation (AppleScript)
6) CLI (optional)
The app can install the global openclaw CLI via npm/pnpm so terminal
workflows and launchd tasks work out of the box.
7) Onboarding chat (dedicated session)
After setup, the app opens a dedicated onboarding chat session so the agent can introduce itself and guide next steps. This keeps first‑run guidance separate from your normal conversation.
Agent bootstrap ritual
On the first agent run, OpenClaw bootstraps a workspace (default ~/.openclaw/workspace):
- Seeds
AGENTS.md,BOOTSTRAP.md,IDENTITY.md,USER.md - Runs a short Q&A ritual (one question at a time)
- Writes identity + preferences to
IDENTITY.md,USER.md,SOUL.md - Removes
BOOTSTRAP.mdwhen finished so it only runs once
Optional: Gmail hooks (manual)
Gmail Pub/Sub setup is currently a manual step. Use:
openclaw webhooks gmail setup --account you@gmail.com
See /automation/gmail-pubsub for details.
Remote mode notes
When the Gateway runs on another machine, credentials and workspace files live on that host. If you need OAuth in remote mode, create:
~/.openclaw/credentials/oauth.json~/.openclaw/agents/<agentId>/agent/auth-profiles.json
on the gateway host.
Personal Assistant Setup
Source: https://docs.openclaw.ai/start/openclaw
Building a personal assistant with OpenClaw
OpenClaw is a WhatsApp + Telegram + Discord + iMessage gateway for Pi agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent.
⚠️ Safety first
You’re putting an agent in a position to:
- run commands on your machine (depending on your Pi tool setup)
- read/write files in your workspace
- send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin)
Start conservative:
- Always set
channels.whatsapp.allowFrom(never run open-to-the-world on your personal Mac). - Use a dedicated WhatsApp number for the assistant.
- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting
agents.defaults.heartbeat.every: "0m".
Prerequisites
- Node 22+
- OpenClaw available on PATH (recommended: global install)
- A second phone number (SIM/eSIM/prepaid) for the assistant
npm install -g openclaw@latest
# or: pnpm add -g openclaw@latest
From source (development):
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm link --global
The two-phone setup (recommended)
You want this:
Your Phone (personal) Second Phone (assistant)
┌─────────────────┐ ┌─────────────────┐
│ Your WhatsApp │ ──────▶ │ Assistant WA │
│ +1-555-YOU │ message │ +1-555-ASSIST │
└─────────────────┘ └────────┬────────┘
│ linked via QR
▼
┌─────────────────┐
│ Your Mac │
│ (openclaw) │
│ Pi agent │
└─────────────────┘
If you link your personal WhatsApp to OpenClaw, every message to you becomes “agent input”. That’s rarely what you want.
5-minute quick start
- Pair WhatsApp Web (shows QR; scan with the assistant phone):
openclaw channels login
- Start the Gateway (leave it running):
openclaw gateway --port 18789
- Put a minimal config in
~/.openclaw/openclaw.json:
{
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}
Now message the assistant number from your allowlisted phone.
When onboarding finishes, we auto-open the dashboard with your gateway token and print the tokenized link. To reopen later: openclaw dashboard.
Give the agent a workspace (AGENTS)
OpenClaw reads operating instructions and “memory” from its workspace directory.
By default, OpenClaw uses ~/.openclaw/workspace as the agent workspace, and will create it (plus starter AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md) automatically on setup/first agent run. BOOTSTRAP.md is only created when the workspace is brand new (it should not come back after you delete it).
Tip: treat this folder like OpenClaw’s “memory” and make it a git repo (ideally private) so your AGENTS.md + memory files are backed up. If git is installed, brand-new workspaces are auto-initialized.
openclaw setup
Full workspace layout + backup guide: Agent workspace Memory workflow: Memory
Optional: choose a different workspace with agents.defaults.workspace (supports ~).
{
agent: {
workspace: "~/.openclaw/workspace",
},
}
If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely:
{
agent: {
skipBootstrap: true,
},
}
The config that turns it into “an assistant”
OpenClaw defaults to a good assistant setup, but you’ll usually want to tune:
- persona/instructions in
SOUL.md - thinking defaults (if desired)
- heartbeats (once you trust it)
Example:
{
logging: { level: "info" },
agent: {
model: "anthropic/claude-opus-4-5",
workspace: "~/.openclaw/workspace",
thinkingDefault: "high",
timeoutSeconds: 1800,
// Start with 0; enable later.
heartbeat: { every: "0m" },
},
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: {
"*": { requireMention: true },
},
},
},
routing: {
groupChat: {
mentionPatterns: ["@openclaw", "openclaw"],
},
},
session: {
scope: "per-sender",
resetTriggers: ["/new", "/reset"],
reset: {
mode: "daily",
atHour: 4,
idleMinutes: 10080,
},
},
}
Sessions and memory
- Session files:
~/.openclaw/agents/<agentId>/sessions/{{SessionId}}.jsonl - Session metadata (token usage, last route, etc):
~/.openclaw/agents/<agentId>/sessions/sessions.json(legacy:~/.openclaw/sessions/sessions.json) /newor/resetstarts a fresh session for that chat (configurable viaresetTriggers). If sent alone, the agent replies with a short hello to confirm the reset./compact [instructions]compacts the session context and reports the remaining context budget.
Heartbeats (proactive mode)
By default, OpenClaw runs a heartbeat every 30 minutes with the 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.
Set agents.defaults.heartbeat.every: "0m" to disable.
- If
HEARTBEAT.mdexists 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.
- If the agent replies with
HEARTBEAT_OK(optionally with short padding; seeagents.defaults.heartbeat.ackMaxChars), OpenClaw suppresses outbound delivery for that heartbeat. - Heartbeats run full agent turns — shorter intervals burn more tokens.
{
agent: {
heartbeat: { every: "30m" },
},
}
Media in and out
Inbound attachments (images/audio/docs) can be surfaced to your command via templates:
{{MediaPath}}(local temp file path){{MediaUrl}}(pseudo-URL){{Transcript}}(if audio transcription is enabled)
Outbound attachments from the agent: include MEDIA:<path-or-url> on its own line (no spaces). Example:
Here’s the screenshot.
MEDIA:https://example.com/screenshot.png
OpenClaw extracts these and sends them as media alongside the text.
Operations checklist
openclaw status # local status (creds, sessions, queued events)
openclaw status --all # full diagnosis (read-only, pasteable)
openclaw status --deep # adds gateway health probes (Telegram + Discord)
openclaw health --json # gateway health snapshot (WS)
Logs live under /tmp/openclaw/ (default: openclaw-YYYY-MM-DD.log).
Next steps
- WebChat: WebChat
- Gateway ops: Gateway runbook
- Cron + wakeups: Cron jobs
- macOS menu bar companion: OpenClaw macOS app
- iOS node app: iOS app
- Android node app: Android app
- Windows status: Windows (WSL2)
- Linux status: Linux app
- Security: Security
Pairing
Source: https://docs.openclaw.ai/start/pairing
Pairing
“Pairing” is OpenClaw’s explicit owner approval step. It is used in two places:
- DM pairing (who is allowed to talk to the bot)
- Node pairing (which devices/nodes are allowed to join the gateway network)
Security context: Security
1) DM pairing (inbound chat access)
When a channel is configured with DM policy pairing, unknown senders get a short code and their message is not processed until you approve.
Default DM policies are documented in: Security
Pairing codes:
- 8 characters, uppercase, no ambiguous chars (
0O1I). - Expire after 1 hour. The bot only sends the pairing message when a new request is created (roughly once per hour per sender).
- Pending DM pairing requests are capped at 3 per channel by default; additional requests are ignored until one expires or is approved.
Approve a sender
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
Supported channels: telegram, whatsapp, signal, imessage, discord, slack.
Where the state lives
Stored under ~/.openclaw/credentials/:
- Pending requests:
<channel>-pairing.json - Approved allowlist store:
<channel>-allowFrom.json
Treat these as sensitive (they gate access to your assistant).
2) Node device pairing (iOS/Android/macOS/headless nodes)
Nodes connect to the Gateway as devices with role: node. The Gateway
creates a device pairing request that must be approved.
Approve a node device
openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>
Where the state lives
Stored under ~/.openclaw/devices/:
pending.json(short-lived; pending requests expire)paired.json(paired devices + tokens)
Notes
- The legacy
node.pair.*API (CLI:openclaw nodes pending/approve) is a separate gateway-owned pairing store. WS nodes still require device pairing.
Related docs
Quick start
Source: https://docs.openclaw.ai/start/quickstart
OpenClaw requires Node 22 or newer.Install
```bash theme={null} npm install -g openclaw@latest ``` ```bash theme={null} pnpm add -g openclaw@latest ```Onboard and run the Gateway
```bash theme={null} openclaw onboard --install-daemon ``` ```bash theme={null} openclaw channels login ``` ```bash theme={null} openclaw gateway --port 18789 ```After onboarding, the Gateway runs via the user service. You can still run it manually with openclaw gateway.
From source (development)
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon
If you do not have a global install yet, run onboarding via pnpm openclaw ... from the repo.
Multi instance quickstart (optional)
OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
OPENCLAW_STATE_DIR=~/.openclaw-a \
openclaw gateway --port 19001
Send a test message
Requires a running Gateway.
openclaw message send --target +15555550123 --message "Hello from OpenClaw"
Setup
Source: https://docs.openclaw.ai/start/setup
Setup
Last updated: 2026-01-01
TL;DR
- Tailoring lives outside the repo:
~/.openclaw/workspace(workspace) +~/.openclaw/openclaw.json(config). - Stable workflow: install the macOS app; let it run the bundled Gateway.
- Bleeding edge workflow: run the Gateway yourself via
pnpm gateway:watch, then let the macOS app attach in Local mode.
Prereqs (from source)
- Node
>=22 pnpm- Docker (optional; only for containerized setup/e2e — see Docker)
Tailoring strategy (so updates don’t hurt)
If you want “100% tailored to me” and easy updates, keep your customization in:
- Config:
~/.openclaw/openclaw.json(JSON/JSON5-ish) - Workspace:
~/.openclaw/workspace(skills, prompts, memories; make it a private git repo)
Bootstrap once:
openclaw setup
From inside this repo, use the local CLI entry:
openclaw setup
If you don’t have a global install yet, run it via pnpm openclaw setup.
Stable workflow (macOS app first)
- Install + launch OpenClaw.app (menu bar).
- Complete the onboarding/permissions checklist (TCC prompts).
- Ensure Gateway is Local and running (the app manages it).
- Link surfaces (example: WhatsApp):
openclaw channels login
- Sanity check:
openclaw health
If onboarding is not available in your build:
- Run
openclaw setup, thenopenclaw channels login, then start the Gateway manually (openclaw gateway).
Bleeding edge workflow (Gateway in a terminal)
Goal: work on the TypeScript Gateway, get hot reload, keep the macOS app UI attached.
0) (Optional) Run the macOS app from source too
If you also want the macOS app on the bleeding edge:
./scripts/restart-mac.sh
1) Start the dev Gateway
pnpm install
pnpm gateway:watch
gateway:watch runs the gateway in watch mode and reloads on TypeScript changes.
2) Point the macOS app at your running Gateway
In OpenClaw.app:
- Connection Mode: Local The app will attach to the running gateway on the configured port.
3) Verify
- In-app Gateway status should read “Using existing gateway …”
- Or via CLI:
openclaw health
Common footguns
- Wrong port: Gateway WS defaults to
ws://127.0.0.1:18789; keep app + CLI on the same port. - Where state lives:
- Credentials:
~/.openclaw/credentials/ - Sessions:
~/.openclaw/agents/<agentId>/sessions/ - Logs:
/tmp/openclaw/
- Credentials:
Credential storage map
Use this when debugging auth or deciding what to back up:
- WhatsApp:
~/.openclaw/credentials/whatsapp/<accountId>/creds.json - Telegram bot token: config/env or
channels.telegram.tokenFile - Discord bot token: config/env (token file not yet supported)
- Slack tokens: config/env (
channels.slack.*) - Pairing allowlists:
~/.openclaw/credentials/<channel>-allowFrom.json - Model auth profiles:
~/.openclaw/agents/<agentId>/agent/auth-profiles.json - Legacy OAuth import:
~/.openclaw/credentials/oauth.jsonMore detail: Security.
Updating (without wrecking your setup)
- Keep
~/.openclaw/workspaceand~/.openclaw/as “your stuff”; don’t put personal prompts/config into theopenclawrepo. - Updating source:
git pull+pnpm install(when lockfile changed) + keep usingpnpm gateway:watch.
Linux (systemd user service)
Linux installs use a systemd user service. By default, systemd stops user services on logout/idle, which kills the Gateway. Onboarding attempts to enable lingering for you (may prompt for sudo). If it’s still off, run:
sudo loginctl enable-linger $USER
For always-on or multi-user servers, consider a system service instead of a user service (no lingering needed). See Gateway runbook for the systemd notes.
Related docs
- Gateway runbook (flags, supervision, ports)
- Gateway configuration (config schema + examples)
- Discord and Telegram (reply tags + replyToMode settings)
- OpenClaw assistant setup
- macOS app (gateway lifecycle)
Showcase
Source: https://docs.openclaw.ai/start/showcase
Real-world OpenClaw projects from the community
Showcase
Real projects from the community. See what people are building with OpenClaw.
**Want to be featured?** Share your project in [#showcase on Discord](https://discord.gg/clawd) or [tag @openclaw on X](https://x.com/openclaw).🎥 OpenClaw in Action
Full setup walkthrough (28m) by VelvetShark.