# 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) ```bash theme={null} openclaw models status --check ``` Exit codes: * `0`: OK * `1`: expired or missing credentials * `2`: 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.sh` now uses `openclaw models status --json` as the source of truth (falling back to direct file reads if the CLI is unavailable), so keep `openclaw` on `PATH` for 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](/automation/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:`, 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: ```bash theme={null} 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 --force openclaw cron runs --id ``` Schedule a recurring isolated job with delivery: ```bash theme={null} 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](/automation/cron-jobs#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. 1. **Choose a schedule** * One-shot reminder → `schedule.kind = "at"` (CLI: `--at`) * Repeating job → `schedule.kind = "every"` or `schedule.kind = "cron"` * If your ISO timestamp omits a timezone, it is treated as **UTC**. 2. **Choose where it runs** * `sessionTarget: "main"` → run during the next heartbeat with main context. * `sessionTarget: "isolated"` → run a dedicated agent turn in `cron:`. 3. **Choose the payload** * Main session → `payload.kind = "systemEvent"` * Isolated session → `payload.kind = "agentTurn"` 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 via `schedule.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](/gateway/heartbeat). #### Isolated jobs (dedicated cron sessions) Isolated jobs run a dedicated agent turn in session `cron:`. Key behaviors: * Prompt is prefixed with `[cron: ]` for traceability. * Each run starts a **fresh session id** (no prior conversation carry-over). * Default behavior: if `delivery` is 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). * `wakeMode` controls 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`: `last` or 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_OK` with 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`: `now` triggers an immediate heartbeat and `next-heartbeat` waits 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: 1. Job payload override (highest) 2. Hook-specific defaults (e.g., `hooks.gmail.model`) 3. 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) or `none`. * `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:`, `user:`) 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): ```json theme={null} { "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: ```json theme={null} { "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`), or `cron` (`expr`, optional `tz`). * `schedule.at` accepts ISO 8601 (timezone optional; treated as UTC when omitted). * `everyMs` is milliseconds. * `sessionTarget` must be `"main"` or `"isolated"` and must match `payload.kind`. * Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun` (defaults to true for `at`), `delivery`. * `wakeMode` defaults to `"next-heartbeat"` when omitted. ### cron.update params ```json theme={null} { "jobId": "job-123", "patch": { "enabled": false, "schedule": { "kind": "every", "everyMs": 3600000 } } } ``` Notes: * `jobId` is canonical; `id` is accepted for compatibility. * Use `agentId: null` in the patch to clear an agent binding. ### cron.run and cron.remove params ```json theme={null} { "jobId": "job-123", "mode": "force" } ``` ```json theme={null} { "jobId": "job-123" } ``` ## Storage & history * Job store: `~/.openclaw/cron/jobs.json` (Gateway-managed JSON). * Run history: `~/.openclaw/cron/runs/.jsonl` (JSONL, auto-pruned). * Override store path: `cron.store` in config. ## Configuration ```json5 theme={null} { 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): ```bash theme={null} 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): ```bash theme={null} openclaw cron add \ --name "Calendar check" \ --at "20m" \ --session main \ --system-event "Next heartbeat: check calendar." \ --wake now ``` Recurring isolated job (announce to WhatsApp): ```bash theme={null} 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): ```bash theme={null} 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: ```bash theme={null} 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): ```bash theme={null} # 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 --agent ops openclaw cron edit --clear-agent ``` Manual run (debug): ```bash theme={null} openclaw cron run --force ``` Edit an existing job (patch fields): ```bash theme={null} openclaw cron edit \ --message "Updated prompt" \ --model "opus" \ --thinking low ``` Run history: ```bash theme={null} openclaw cron runs --id --limit 50 ``` Immediate system event without creating a job: ```bash theme={null} openclaw system event --mode now --text "Next heartbeat: check battery." ``` ## Gateway API surface * `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove` * `cron.run` (force or due), `cron.runs` For immediate system events without a job, use [`openclaw system event`](/cli/system). ## Troubleshooting ### “Nothing runs” * Check cron is enabled: `cron.enabled` and `OPENCLAW_SKIP_CRON`. * Check the Gateway is running continuously (cron runs inside the Gateway process). * For `cron` schedules: confirm timezone (`--tz`) vs the host timezone. ### Telegram delivers to the wrong place * For forum topics, use `-100…:topic:` 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_OK` and no message is delivered. * **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring. ### Heartbeat example: HEARTBEAT.md checklist ```md theme={null} # 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 ```json5 theme={null} { agents: { defaults: { heartbeat: { every: "30m", // interval target: "last", // where to deliver alerts activeHours: { start: "08:00", end: "22:00" }, // optional }, }, }, } ``` See [Heartbeat](/gateway/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:` without polluting main history. * **Model overrides**: Use a cheaper or more powerful model per job. * **Delivery control**: Isolated jobs default to `announce` (summary); choose `none` as 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**: `--at` for precise future timestamps. ### Cron example: Daily morning briefing ```bash theme={null} 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 ```bash theme={null} 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](/automation/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**: 1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes. 2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. ### Example: Efficient automation setup **HEARTBEAT.md** (checked every 30 min): ```md theme={null} # 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): ```bash theme={null} # 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** (`lobster` CLI) in tool mode and returns a **JSON envelope**. * If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag. * 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](/tools/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:` | | 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 ```bash theme={null} 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 ```bash theme={null} 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.md` small 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](/gateway/heartbeat) - full heartbeat configuration * [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference * [System](/cli/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 * `gcloud` installed and logged in ([install guide](https://docs.cloud.google.com/sdk/docs/install-sdk)). * `gog` (gogcli) installed and authorized for the Gmail account ([gogcli.sh](https://gogcli.sh/)). * OpenClaw hooks enabled (see [Webhooks](/automation/webhook)). * `tailscale` logged in ([tailscale.com](https://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): ```json5 theme={null} { 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`: ```json5 theme={null} { 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: ```json5 theme={null} { hooks: { gmail: { model: "openrouter/meta-llama/llama-3.3-70b-instruct:free", thinking: "off", }, }, } ``` Notes: * Per-hook `model`/`thinking` in the mapping still overrides these defaults. * Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts). * If `agents.defaults.models` is 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](/automation/webhook)). ## Wizard (recommended) Use the OpenClaw helper to wire everything together (installs deps on macOS via brew): ```bash theme={null} openclaw webhooks gmail setup \ --account openclaw@gmail.com ``` Defaults: * Uses Tailscale Funnel for the public push endpoint. * Writes `hooks.gmail` config for `openclaw 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 ` 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=true` and `hooks.gmail.account` is set, the Gateway starts `gog gmail watch serve` on boot and auto-renews the watch. * Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to 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): ```bash theme={null} openclaw webhooks gmail run ``` ## One-time setup 1. Select the GCP project **that owns the OAuth client** used by `gog`. ```bash theme={null} gcloud auth login gcloud config set project ``` Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client. 2. Enable APIs: ```bash theme={null} gcloud services enable gmail.googleapis.com pubsub.googleapis.com ``` 3. Create a topic: ```bash theme={null} gcloud pubsub topics create gog-gmail-watch ``` 4. Allow Gmail push to publish: ```bash theme={null} 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 ```bash theme={null} gog gmail watch start \ --account openclaw@gmail.com \ --label INBOX \ --topic projects//topics/gog-gmail-watch ``` Save the `history_id` from the output (for debugging). ## Run the push handler Local example (shared token auth): ```bash theme={null} gog gmail watch serve \ --account openclaw@gmail.com \ --bind 127.0.0.1 \ --port 8788 \ --path /gmail-pubsub \ --token \ --hook-url http://127.0.0.1:18789/hooks/gmail \ --hook-token OPENCLAW_HOOK_TOKEN \ --include-body \ --max-bytes 20000 ``` Notes: * `--token` protects the push endpoint (`x-gog-token` or `?token=`). * `--hook-url` points to OpenClaw `/hooks/gmail` (mapped; isolated run + summary to main). * `--include-body` and `--max-bytes` control 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): ```bash theme={null} cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate ``` Use the generated URL as the push endpoint: ```bash theme={null} gcloud pubsub subscriptions create gog-gmail-watch-push \ --topic gog-gmail-watch \ --push-endpoint "https:///gmail-pubsub?token=" ``` Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run: ```bash theme={null} gog gmail watch serve --verify-oidc --oidc-email ``` ## Test Send a message to the watched inbox: ```bash theme={null} gog gmail send \ --account openclaw@gmail.com \ --to openclaw@gmail.com \ --subject "watch test" \ --body "ping" ``` Check watch state and history: ```bash theme={null} gog gmail watch status --account openclaw@gmail.com gog gmail history --account openclaw@gmail.com --since ``` ## Troubleshooting * `Invalid topicName`: project mismatch (topic not in the OAuth client project). * `User not authorized`: missing `roles/pubsub.publisher` on the topic. * Empty messages: Gmail push only provides `historyId`; fetch via `gog gmail history`. ## Cleanup ```bash theme={null} 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 ```bash theme={null} # 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`, or `msteams` * `--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, `maxSelections` must be within option count, ignores `durationHours`. * Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count. * MS Teams: Adaptive Card polls (OpenClaw-managed). No native poll API; `durationHours` is 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 ```json5 theme={null} { hooks: { enabled: true, token: "shared-secret", path: "/hooks", }, } ``` Notes: * `hooks.token` is required when `hooks.enabled=true`. * `hooks.path` defaults to `/hooks`. ## Auth Every request must include the hook token. Prefer headers: * `Authorization: Bearer ` (recommended) * `x-openclaw-token: ` * `?token=` (deprecated; logs a warning and will be removed in a future major release) ## Endpoints ### `POST /hooks/wake` Payload: ```json theme={null} { "text": "System line", "mode": "now" } ``` * `text` **required** (string): The description of the event (e.g., "New email received"). * `mode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) 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: ```json theme={null} { "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 } ``` * `message` **required** (string): The prompt or message for the agent to process. * `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries. * `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:`. Using a consistent key allows for a multi-turn conversation within the hook context. * `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check. * `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped. * `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`. * `to` optional (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. * `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted. * `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`). * `timeoutSeconds` optional (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/` (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.mappings` lets you define `match`, `action`, and templates in config. * `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic. * Use `match.source` to keep a generic ingest endpoint (payload-driven routing). * TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime. * Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface (`channel` defaults to `last` and falls back to WhatsApp). * `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook (dangerous; only for trusted internal sources). * `openclaw webhooks gmail setup` writes `hooks.gmail` config for `openclaw webhooks gmail run`. See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow. ## Responses * `200` for `/hooks/wake` * `202` for `/hooks/agent` (async run started) * `401` on auth failure * `400` on invalid payload * `413` on oversized payloads ## Examples ```bash theme={null} 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"}' ``` ```bash theme={null} 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: ```bash theme={null} 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. ```bash theme={null} 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: true` in 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`) ```json theme={null} { "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: ```json theme={null} { "broadcast": { "strategy": "parallel", "120363403215116621@g.us": ["alfred", "baerbel"] } } ``` #### Sequential Agents process in order (one waits for previous to finish): ```json theme={null} { "broadcast": { "strategy": "sequential", "120363403215116621@g.us": ["alfred", "baerbel"] } } ``` ### Complete Example ```json theme={null} { "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 1. **Incoming message** arrives in a WhatsApp group 2. **Broadcast check**: System checks if peer ID is in `broadcast` 3. **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 4. **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...` vs `agent: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: ```json theme={null} { "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: ```json theme={null} { "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: ```json theme={null} { "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: ```json theme={null} { "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:** 1. Agent IDs exist in `agents.list` 2. Peer ID format is correct (e.g., `120363403215116621@g.us`) 3. Agents are not in deny lists **Debug:** ```bash theme={null} 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 ```json theme={null} { "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 ```json theme={null} { "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 ```typescript theme={null} 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 1. **Max agents:** No hard limit, but 10+ agents may be slow 2. **Shared context:** Agents don't see each other's responses (by design) 3. **Message ordering:** Parallel responses may arrive in any order 4. **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 * [Multi-Agent Configuration](/multi-agent-sandbox-tools) * [Routing Configuration](/concepts/channel-routing) * [Session Management](/concepts/sessions) # 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) 1. Create a Discord bot and copy the bot token. 2. In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups). 3. 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). 4. Invite the bot to your server with message permissions (create a private server if you just want DMs). 5. Start the gateway. 6. DM access is pairing by default; approve the pairing code on first contact. Minimal config: ```json5 theme={null} { 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 as `agent::discord:channel:` (display names use `discord:#`). * Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`. * Keep routing deterministic: replies always go back to the channel they arrived on. ## How it works 1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token. 2. Invite the bot to your server with the permissions required to read/send messages where you want to use it. 3. Configure OpenClaw with `channels.discord.token` (or `DISCORD_BOT_TOKEN` as a fallback). 4. Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and `channels.discord.enabled` is not `false`. * If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional). 5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected. 6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel. 7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `openclaw pairing approve discord `. * To keep old “open to anyone” behavior: set `channels.discord.dm.policy="open"` and `channels.discord.dm.allowFrom=["*"]`. * To hard-allowlist: set `channels.discord.dm.policy="allowlist"` and list senders in `channels.discord.dm.allowFrom`. * To ignore all DMs: set `channels.discord.dm.enabled=false` or `channels.discord.dm.policy="disabled"`. 8. Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`. 9. Optional guild rules: set `channels.discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules. 10. Optional native commands: `commands.native` defaults to `"auto"` (on for Discord/Telegram, off for Slack). Override with `channels.discord.commands.native: true|false|"auto"`; `false` clears previously registered commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands. * Full command list + config: [Slash commands](/tools/slash-commands) 11. Optional guild context history: set `channels.discord.historyLimit` (default 20, falls back to `messages.groupChat.historyLimit`) to include the last N guild messages as context when replying to a mention. Set `0` to disable. 12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `channels.discord.actions.*`). * Reaction removal semantics: see [/tools/reactions](/tools/reactions). * The `discord` tool is only exposed when the current channel is Discord. 13. Native commands use isolated session keys (`agent::discord:slash:`) rather than the shared `main` session. 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: ```json5 theme={null} { 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 1. Discord Developer Portal → **Applications** → **New Application** 2. 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. 1. Discord (desktop/web) → **User Settings** → **Advanced** → enable **Developer Mode** 2. 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: ```json5 theme={null} { 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`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. #### Allowlist + channel routing Example “single server, only allow me, only allow #help”: ```json5 theme={null} { 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: true` means the bot only replies when mentioned (recommended for shared channels). * `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages. * Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. * If `channels` is 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 `users` allowlist matches the sender, OpenClaw treats that sender as the owner in the system prompt. For a global owner across channels, set `commands.ownerAllowFrom`. * Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered). * Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. ### 6) Verify it works 1. Start the gateway. 2. In your server channel, send: `@Krill hello` (or whatever your bot name is). 3. If nothing happens: check **Troubleshooting** below. ### Troubleshooting * First: run `openclaw doctor` and `openclaw 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: false` but still no replies**: * `channels.discord.groupPolicy` defaults to **allowlist**; set it to `"open"` or add a guild entry under `channels.discord.guilds` (optionally list channels under `channels.discord.guilds..channels` to restrict). * If you only set `DISCORD_BOT_TOKEN` and never create a `channels.discord` section, the runtime defaults `groupPolicy` to `open`. Add `channels.discord.groupPolicy`, `channels.defaults.groupPolicy`, or a guild/channel allowlist to lock it down. * `requireMention` must live under `channels.discord.guilds` (or a specific channel). `channels.discord.requireMention` at the top level is ignored. * **Permission audits** (`channels status --probe`) only check numeric channel IDs. If you use slugs/names as `channels.discord.guilds.*.channels` keys, 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 ...` is only for forwarded approvals and won’t resolve Discord’s button prompts. If you see `❌ Failed to submit approval: Error: unknown approval id` or the UI never shows up, check: * `channels.discord.execApprovals.enabled: true` in 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](/tools/exec-approvals) and [Slash commands](/tools/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.replyToMode` and 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](/concepts/retry). ## Config ```json5 theme={null} { 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`: set `false` to ignore all DMs (default `true`). * `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`. * `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation. The wizard accepts usernames and resolves them to ids when the bot can search members. * `dm.groupEnabled`: enable group DMs (default `false`). * `dm.groupChannels`: optional allowlist for group DM channel ids or slugs. * `groupPolicy`: controls guild channel handling (`open|disabled|allowlist`); `allowlist` requires 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..slug`: optional friendly slug used for display names. * `guilds..users`: optional per-guild user allowlist (ids or names). * `guilds..tools`: optional per-guild tool policy overrides (`allow`/`deny`/`alsoAllow`) used when the channel override is missing. * `guilds..toolsBySender`: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; `"*"` wildcard supported). * `guilds..channels..allow`: allow/deny the channel when `groupPolicy="allowlist"`. * `guilds..channels..requireMention`: mention gating for the channel. * `guilds..channels..tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). * `guilds..channels..toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported). * `guilds..channels..users`: optional per-channel user allowlist. * `guilds..channels..skills`: skill filter (omit = all skills, empty = none). * `guilds..channels..systemPrompt`: extra system prompt for the channel. Discord channel topics are injected as **untrusted** context (not system prompt). * `guilds..channels..enabled`: set `false` to disable the channel. * `guilds..channels`: channel rules (keys are channel slugs or ids). * `guilds..requireMention`: per-guild mention requirement (overridable per channel). * `guilds..reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`). * `textChunkLimit`: outbound text chunk size (chars). Default: 2000. * `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits 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 to `messages.groupChat.historyLimit`; `0` disables). * `dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `dms[""].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 (set `false` to disable). * `reactions` (covers react + read reactions) * `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search` * `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events` * `channels` (create/edit/delete channels + categories + permissions) * `roles` (role add/remove, default `false`) * `moderation` (timeout/kick/ban, default `false`) * `presence` (bot status/activity, default `false`) * `execApprovals`: Discord-only exec approval DMs (button UI). Supports `enabled`, `approvers`, `agentFilter`, `sessionFilter`. Reaction notifications use `guilds..reactionNotifications`: * `off`: no reaction events. * `own`: reactions on the bot's own messages (default). * `all`: all reactions on all messages. * `allowlist`: reactions from `guilds..users` on 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. ```json5 theme={null} { channels: { discord: { pluralkit: { enabled: true, token: "pk_live_...", // optional; required for private systems }, }, }, } ``` Allowlist notes (PK-enabled): * Use `pk:` in `dm.allowFrom`, `guilds..users`, or per-channel `users`. * 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`, or `all`. 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:]]` — 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`/`groupChannels` accept ids, names, tags, or mentions like `<@id>`. * Prefixes like `discord:`/`user:` (users) and `channel:` (group DMs) are supported. * Use `*` to allow any sender/channel. * When `guilds..channels` is present, channels not listed are denied by default. * When `guilds..channels` is 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/Channel` names (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`, `permissions` * `readMessages`, `sendMessage`, `editMessage`, `deleteMessage` * Read/search/pin tool payloads include normalized `timestampMs` (UTC epoch ms) and `timestampUtc` alongside raw Discord `timestamp`. * `threadCreate`, `threadList`, `threadReply` * `pinMessage`, `unpinMessage`, `listPins` * `searchMessages`, `memberInfo`, `roleInfo`, `roleAdd`, `roleRemove`, `emojiList` * `channelInfo`, `channelList`, `voiceStatus`, `eventList`, `eventCreate` * `timeout`, `kick`, `ban` * `setPresence` (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_TOKEN` env 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: ```bash theme={null} openclaw plugins install @openclaw/feishu ``` Local checkout (when running from a git repo): ```bash theme={null} 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: ```bash theme={null} openclaw onboard ``` The wizard guides you through: 1. Creating a Feishu app and collecting credentials 2. Configuring app credentials in OpenClaw 3. Starting the gateway ✅ **After configuration**, check gateway status: * `openclaw gateway status` * `openclaw logs --follow` ### Method 2: CLI setup If you already completed initial install, add the channel via CLI: ```bash theme={null} openclaw channels add ``` Choose **Feishu**, then enter the App ID and App Secret. ✅ **After configuration**, manage the gateway: * `openclaw gateway status` * `openclaw gateway restart` * `openclaw logs --follow` *** ## Step 1: Create a Feishu app ### 1. Open Feishu Open Platform Visit [Feishu Open Platform](https://open.feishu.cn/app) and sign in. Lark (global) tenants should use [https://open.larksuite.com/app](https://open.larksuite.com/app) and set `domain: "lark"` in the Feishu config. ### 2. Create an app 1. Click **Create enterprise app** 2. Fill in the app name + description 3. Choose an app icon Create enterprise app ### 3. Copy credentials From **Credentials & Basic Info**, copy: * **App ID** (format: `cli_xxx`) * **App Secret** ❗ **Important:** keep the App Secret private. Get credentials ### 4. Configure permissions On **Permissions**, click **Batch import** and paste: ```json theme={null} { "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"] } } ``` Configure permissions ### 5. Enable bot capability In **App Capability** > **Bot**: 1. Enable bot capability 2. Set the bot name Enable bot capability ### 6. Configure event subscription ⚠️ **Important:** before setting event subscription, make sure: 1. You already ran `openclaw channels add` for Feishu 2. The gateway is running (`openclaw gateway status`) In **Event Subscription**: 1. Choose **Use long connection to receive events** (WebSocket) 2. Add the event: `im.message.receive_v1` ⚠️ If the gateway is not running, the long-connection setup may fail to save. Configure event subscription ### 7. Publish the app 1. Create a version in **Version Management & Release** 2. Submit for review and publish 3. Wait for admin approval (enterprise apps usually auto-approve) *** ## Step 2: Configure OpenClaw ### Configure with the wizard (recommended) ```bash theme={null} openclaw channels add ``` Choose **Feishu** and paste your App ID + App Secret. ### Configure via config file Edit `~/.openclaw/openclaw.json`: ```json5 theme={null} { channels: { feishu: { enabled: true, dmPolicy: "pairing", accounts: { main: { appId: "cli_xxx", appSecret: "xxx", botName: "My AI assistant", }, }, }, }, } ``` ### Configure via environment variables ```bash theme={null} 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..domain`). ```json5 theme={null} { channels: { feishu: { domain: "lark", accounts: { main: { appId: "cli_xxx", appSecret: "xxx", }, }, }, }, } ``` *** ## Step 3: Start + test ### 1. Start the gateway ```bash theme={null} 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: ```bash theme={null} openclaw pairing approve feishu ``` 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**: ```bash theme={null} openclaw pairing list feishu openclaw pairing approve feishu ``` * **Allowlist mode**: set `channels.feishu.allowFrom` with allowed Open IDs ### Group chats **1. Group policy** (`channels.feishu.groupPolicy`): * `"open"` = allow everyone in groups (default) * `"allowlist"` = only allow `groupAllowFrom` * `"disabled"` = disable group messages **2. Mention requirement** (`channels.feishu.groups..requireMention`): * `true` = require @mention (default) * `false` = respond without mentions *** ## Group configuration examples ### Allow all groups, require @mention (default) ```json5 theme={null} { channels: { feishu: { groupPolicy: "open", // Default requireMention: true }, }, } ``` ### Allow all groups, no @mention required ```json5 theme={null} { channels: { feishu: { groups: { oc_xxx: { requireMention: false }, }, }, }, } ``` ### Allow specific users in groups only ```json5 theme={null} { 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)** 1. Start the gateway and @mention the bot in the group 2. Run `openclaw logs --follow` and look for `chat_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)** 1. Start the gateway and DM the bot 2. Run `openclaw logs --follow` and look for `open_id` **Method 2** Check pairing requests for user Open IDs: ```bash theme={null} 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 1. Ensure the bot is added to the group 2. Ensure you @mention the bot (default behavior) 3. Check `groupPolicy` is not set to `"disabled"` 4. Check logs: `openclaw logs --follow` ### Bot does not receive messages 1. Ensure the app is published and approved 2. Ensure event subscription includes `im.message.receive_v1` 3. Ensure **long connection** is enabled 4. Ensure app permissions are complete 5. Ensure the gateway is running: `openclaw gateway status` 6. Check logs: `openclaw logs --follow` ### App Secret leak 1. Reset the App Secret in Feishu Open Platform 2. Update the App Secret in your config 3. Restart the gateway ### Message send failures 1. Ensure the app has `im:message:send_as_bot` permission 2. Ensure the app is published 3. Check logs for detailed errors *** ## Advanced configuration ### Multiple accounts ```json5 theme={null} { 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](/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..appId` | App ID | - | | `channels.feishu.accounts..appSecret` | App Secret | - | | `channels.feishu.accounts..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..requireMention` | Require @mention | `true` | | `channels.feishu.groups..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) 1. Create a Google Cloud project and enable the **Google Chat API**. * Go to: [Google Chat API Credentials](https://console.cloud.google.com/apis/api/chat.googleapis.com/credentials) * Enable the API if it is not already enabled. 2. 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**). 3. 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**. 4. Store the downloaded JSON file on your gateway host (e.g., `~/.openclaw/googlechat-service-account.json`). 5. Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat): * 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`) * 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 status` to find your gateway's public URL.* * Under **Visibility**, check **Make this Chat app available to specific people and groups in \**. * Enter your email address (e.g. `user@example.com`) in the text box. * Click **Save** at the bottom. 6. **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. 7. 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"`. 8. Set the webhook audience type + value (matches your Chat app config). 9. 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: 1. Go to [Google Chat](https://chat.google.com/). 2. Click the **+** (plus) icon next to **Direct Messages**. 3. 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. 4. Select your bot from the results. 5. Click **Add** or **Chat** to start a 1:1 conversation. 6. 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`. 1. **Check what address your gateway is bound to:** ```bash theme={null} ss -tlnp | grep 18789 ``` Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`). 2. **Expose the dashboard to the tailnet only (port 8443):** ```bash theme={null} # 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 ``` 3. **Expose only the webhook path publicly:** ```bash theme={null} # 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 ``` 4. **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. 5. **Verify the configuration:** ```bash theme={null} tailscale serve status tailscale funnel status ``` Your public webhook URL will be: `https://..ts.net/googlechat` Your private dashboard stays tailnet-only: `https://..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 reset` and `tailscale serve reset`. ### Option B: Reverse Proxy (Caddy) If you use a reverse proxy like Caddy, only proxy the specific path: ```caddy theme={null} 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 1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header. 2. 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. 3. Messages are routed by space: * DMs use session key `agent::googlechat:dm:`. * Spaces use session key `agent::googlechat:group:`. 4. DM access is pairing by default. Unknown senders receive a pairing code; approve with: * `openclaw pairing approve googlechat ` 5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app’s user name. ## Targets Use these identifiers for delivery and allowlists: * Direct messages: `users/` or `users/` (email addresses are accepted). * Spaces: `spaces/`. ## Config highlights ```json5 theme={null} { 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 `/googlechat` if `webhookPath` isn’t set. * Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled. * `typingIndicator` supports `none`, `message` (default), and `reaction` (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: 1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with: ```bash theme={null} openclaw config get channels.googlechat ``` If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)). 2. **Plugin not enabled**: Check plugin status: ```bash theme={null} openclaw plugins list | grep googlechat ``` If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config. 3. **Gateway not restarted**: After adding config, restart the gateway: ```bash theme={null} openclaw gateway restart ``` Verify the channel is running: ```bash theme={null} openclaw channels status # Should show: Google Chat default: enabled, configured, ... ``` ### Other issues * Check `openclaw channels status --probe` for 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 `botUser` to the app's user resource name and verify `requireMention`. * Use `openclaw logs --follow` while sending a test message to see if requests reach the gateway. Related docs: * [Gateway configuration](/gateway/configuration) * [Security](/gateway/security) * [Reactions](/tools/reactions) # 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:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`. * **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`. * **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls). * **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; 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.streamMode` uses `sendMessageDraft` in 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](/channels/bluebubbles) for new iMessage setups. > > The `imsg` channel 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) 1. Ensure Messages is signed in on this Mac. 2. Install `imsg`: * `brew install steipete/tap/imsg` 3. Configure OpenClaw with `channels.imessage.cliPath` and `channels.imessage.dbPath`. 4. Start the gateway and approve any macOS prompts (Automation + Full Disk Access). Minimal config: ```json5 theme={null} { channels: { imessage: { enabled: true, cliPath: "/usr/local/bin/imsg", dbPath: "/Users//Library/Messages/chat.db", }, }, } ``` ## What it is * iMessage channel backed by `imsg` on macOS. * Deterministic routing: replies always go back to iMessage. * DMs share the agent's main session; groups are isolated (`agent::imessage:group:`). * If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `channels.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: ```json5 theme={null} { 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.cliPath` can point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runs `imsg rpc`). ## Setup (fast path) 1. Ensure Messages is signed in on this Mac. 2. 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. 1. Create a dedicated Apple ID (example: `my-cool-bot@icloud.com`). * Apple may require a phone number for verification / 2FA. 2. Create a macOS user (example: `openclawhome`) and sign into it. 3. Open Messages in that macOS user and sign into iMessage using the bot Apple ID. 4. Enable Remote Login (System Settings → General → Sharing → Remote Login). 5. Install `imsg`: * `brew install steipete/tap/imsg` 6. Set up SSH so `ssh @localhost true` works without a password. 7. Point `channels.imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as 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 `` with your actual macOS username: ```bash theme={null} #!/usr/bin/env bash set -euo pipefail # Run an interactive SSH once first to accept host keys: # ssh @localhost true exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T @localhost \ "/usr/local/bin/imsg" "$@" ``` Example config: ```json5 theme={null} { channels: { imessage: { enabled: true, accounts: { bot: { name: "Bot", enabled: true, cliPath: "/path/to/imsg-bot", dbPath: "/Users//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: ```bash theme={null} #!/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`: ```json5 theme={null} { 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): ```json5 theme={null} { 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`): ```bash theme={null} #!/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.net` works without prompts. * `remoteHost` should 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`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) 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 imessage` * `openclaw pairing approve imessage ` * Pairing is the default token exchange for iMessage DMs. Details: [Pairing](/start/pairing) Groups: * `channels.imessage.groupPolicy = open | allowlist | disabled`. * `channels.imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. * Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.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) * `imsg` streams 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::imessage:group:` session key) * group allowlisting / mention gating behavior Example: ```json5 theme={null} { 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](/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/gateway/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](/gateway/configuration) Provider options: * `channels.imessage.enabled`: enable/disable channel startup. * `channels.imessage.cliPath`: path to `imsg`. * `channels.imessage.dbPath`: Messages DB path. * `channels.imessage.remoteHost`: SSH host for SCP attachment transfer when `cliPath` points 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, or `chat_id:*`). `open` requires `"*"`. 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[""].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) or `newline` to split on blank lines (paragraph boundaries) before length chunking. Related global options: * `agents.list[].groupChat.mentionPatterns` (or `messages.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](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing. * [Telegram](/channels/telegram) — Bot API via grammY; supports groups. * [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs. * [Slack](/channels/slack) — Bolt SDK; workspace apps. * [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (plugin, installed separately). * [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook. * [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately). * [Signal](/channels/signal) — signal-cli; privacy-focused. * [BlueBubbles](/channels/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)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups). * [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately). * [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately). * [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately). * [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately). * [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately). * [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately). * [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately). * [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately). * [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately). * [WebChat](/web/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](/concepts/groups). * DM pairing and allowlists are enforced for safety; see [Security](/gateway/security). * Telegram internals: [grammY notes](/channels/grammy). * Troubleshooting: [Channel troubleshooting](/channels/troubleshooting). * Model providers are documented separately; see [Model Providers](/providers/models). # 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: ```bash theme={null} openclaw plugins install @openclaw/line ``` Local checkout (when running from a git repo): ```bash theme={null} openclaw plugins install ./extensions/line ``` ## Setup 1. Create a LINE Developers account and open the Console: [https://developers.line.biz/console/](https://developers.line.biz/console/) 2. Create (or pick) a Provider and add a **Messaging API** channel. 3. Copy the **Channel access token** and **Channel secret** from the channel settings. 4. Enable **Use webhook** in the Messaging API settings. 5. 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..webhookPath` and update the URL accordingly. ## Configure Minimal config: ```json5 theme={null} { channels: { line: { enabled: true, channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN", channelSecret: "LINE_CHANNEL_SECRET", dmPolicy: "pairing", }, }, } ``` Env vars (default account only): * `LINE_CHANNEL_ACCESS_TOKEN` * `LINE_CHANNEL_SECRET` Token/secret files: ```json5 theme={null} { channels: { line: { tokenFile: "/path/to/line-token.txt", secretFile: "/path/to/line-secret.txt", }, }, } ``` Multiple accounts: ```json5 theme={null} { 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. ```bash theme={null} openclaw pairing list line openclaw pairing approve line ``` Allowlists and policies: * `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` * `channels.line.allowFrom`: allowlisted LINE user IDs for DMs * `channels.line.groupPolicy`: `allowlist | open | disabled` * `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups * Per-group overrides: `channels.line.groups..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. ```json5 theme={null} { 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 `channelSecret` matches the LINE console. * **No inbound events:** confirm the webhook path matches `channels.line.webhookPath` and that the gateway is reachable from LINE. * **Media download errors:** raise `channels.line.mediaMaxMb` if 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.location` with `geo_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 use `live_period`. * **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line. * **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is 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): ```bash theme={null} openclaw plugins install @openclaw/matrix ``` Local checkout (when running from a git repo): ```bash theme={null} 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](/plugin) ## Setup 1. Install the Matrix plugin: * From npm: `openclaw plugins install @openclaw/matrix` * From a local checkout: `openclaw plugins install ./extensions/matrix` 2. Create a Matrix account on a homeserver: * Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/) * Or host it yourself. 3. Get an access token for the bot account: * Use the Matrix login API with `curl` at your home server: ```bash theme={null} 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.org` with 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. 4. Configure credentials: * Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_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.userId` should be the full Matrix ID (example: `@bot:example.org`). 5. Restart the gateway (or finish onboarding). 6. 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/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE, so set `channels.matrix.encryption: true` and verify the device. Minimal config (access token, user ID auto-fetched): ```json5 theme={null} { channels: { matrix: { enabled: true, homeserver: "https://matrix.example.org", accessToken: "syt_***", dm: { policy: "pairing" }, }, }, } ``` E2EE config (end to end encryption enabled): ```json5 theme={null} { 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-nodejs` and run `pnpm rebuild @matrix-org/matrix-sdk-crypto-nodejs` or fetch the binary with `node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js`. Crypto state is stored per account + access token in `~/.openclaw/matrix/accounts//__//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 matrix` * `openclaw pairing approve matrix ` * Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`. * `channels.matrix.dm.allowFrom` accepts 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). Use `channels.defaults.groupPolicy` to 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): ```json5 theme={null} { channels: { matrix: { groupPolicy: "allowlist", groups: { "!roomId:example.org": { allow: true }, "#alias:example.org": { allow: true }, }, groupAllowFrom: ["@owner:example.org"], }, }, } ``` * `requireMention: false` enables auto-reply in that room. * `groups."*"` can set defaults for mention gating across rooms. * `groupAllowFrom` restricts which senders can trigger the bot in rooms (full Matrix user IDs). * Per-room `users` allowlists 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.autoJoin` and `channels.matrix.autoJoinAllowlist`. * To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist). * Legacy key: `channels.matrix.rooms` (same shape as `groups`). ## Threads * Reply threading is supported. * `channels.matrix.threadReplies` controls whether replies stay in threads: * `off`, `inbound` (default), `always` * `channels.matrix.replyToMode` controls 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](/gateway/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) or `newline` to 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). `open` requires `"*"`. 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](https://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): ```bash theme={null} openclaw plugins install @openclaw/mattermost ``` Local checkout (when running from a git repo): ```bash theme={null} 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](/plugin) ## Quick setup 1. Install the Mattermost plugin. 2. Create a Mattermost bot account and copy the **bot token**. 3. Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). 4. Configure OpenClaw and start the gateway. Minimal config: ```json5 theme={null} { 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: ```json5 theme={null} { channels: { mattermost: { chatmode: "onchar", oncharPrefixes: [">", "!"], }, }, } ``` Notes: * `onchar` still responds to explicit @mentions. * `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred. ## Access control (DMs) * Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). * Approve via: * `openclaw pairing list mattermost` * `openclaw pairing approve mattermost ` * Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.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:` for a channel * `user:` for a DM * `@username` for a DM (resolved via the Mattermost API) Bare IDs are treated as channels. ## Multi-account Mattermost supports multiple accounts under `channels.mattermost.accounts`: ```json5 theme={null} { 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 `default` account. # 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](#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): ```bash theme={null} openclaw plugins install @openclaw/msteams ``` Local checkout (when running from a git repo): ```bash theme={null} 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](/plugin) ## Quick setup (beginner) 1. Install the Microsoft Teams plugin. 2. Create an **Azure Bot** (App ID + client secret + tenant ID). 3. Configure OpenClaw with those credentials. 4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel. 5. Install the Teams app package and start the gateway. Minimal config: ```json5 theme={null} { channels: { msteams: { enabled: true, appId: "", appPassword: "", tenantId: "", 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: ```json5 theme={null} { channels: { msteams: { configWrites: false } }, } ``` ## Access control (DMs + groups) **DM access** * Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved. * `channels.msteams.allowFrom` accepts 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 add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset. * `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`). * Set `groupPolicy: "open"` to allow any member (still mention‑gated by default). * To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`. Example: ```json5 theme={null} { 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/Channel` entries 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: ```json5 theme={null} { channels: { msteams: { groupPolicy: "allowlist", teams: { "My Team": { channels: { General: { requireMention: true }, }, }, }, }, }, } ``` ## How it works 1. Install the Microsoft Teams plugin. 2. Create an **Azure Bot** (App ID + secret + tenant ID). 3. Build a **Teams app package** that references the bot and includes the RSC permissions below. 4. Upload/install the Teams app into a team (or personal scope for DMs). 5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway. 6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default. ## Azure Bot Setup (Prerequisites) Before configuring OpenClaw, you need to create an Azure Bot resource. ### Step 1: Create Azure Bot 1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot) 2. 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. 3. Click **Review + create** → **Create** (wait \~1-2 minutes) ### Step 2: Get Credentials 1. Go to your Azure Bot resource → **Configuration** 2. Copy **Microsoft App ID** → this is your `appId` 3. Click **Manage Password** → go to the App Registration 4. Under **Certificates & secrets** → **New client secret** → copy the **Value** → this is your `appPassword` 5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId` ### Step 3: Configure Messaging Endpoint 1. In Azure Bot → **Configuration** 2. Set **Messaging endpoint** to your webhook URL: * Production: `https://your-domain.com/api/messages` * Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below) ### Step 4: Enable Teams Channel 1. In Azure Bot → **Channels** 2. Click **Microsoft Teams** → Configure → Save 3. Accept the Terms of Service ## Local Development (Tunneling) Teams can't reach `localhost`. Use a tunnel for local development: **Option A: ngrok** ```bash theme={null} 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** ```bash theme={null} 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](https://dev.teams.microsoft.com/apps): 1. Click **+ New app** 2. Fill in basic info (name, description, developer info) 3. Go to **App features** → **Bot** 4. Select **Enter a bot ID manually** and paste your Azure Bot App ID 5. Check scopes: **Personal**, **Team**, **Group Chat** 6. Click **Distribute** → **Download app package** 7. 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)** 1. In Azure Portal → your Azure Bot resource → **Test in Web Chat** 2. Send a message - you should see a response 3. This confirms your webhook endpoint works before Teams setup **Option B: Teams (after app installation)** 1. Install the Teams app (sideload or org catalog) 2. Find the bot in Teams and send a DM 3. Check gateway logs for incoming activity ## Setup (minimal text-only) 1. **Install the Microsoft Teams plugin** * From npm: `openclaw plugins install @openclaw/msteams` * From a local checkout: `openclaw plugins install ./extensions/msteams` 2. **Bot registration** * Create an Azure Bot (see above) and note: * App ID * Client secret (App password) * Tenant ID (single-tenant) 3. **Teams app manifest** * Include a `bot` entry with `botId = `. * Scopes: `personal`, `team`, `groupChat`. * `supportsFiles: true` (required for personal scope file handling). * Add RSC permissions (below). * Create icons: `outline.png` (32x32) and `color.png` (192x192). * Zip all three files together: `manifest.json`, `outline.png`, `color.png`. 4. **Configure OpenClaw** ```json theme={null} { "msteams": { "enabled": true, "appId": "", "appPassword": "", "tenantId": "", "webhook": { "port": 3978, "path": "/api/messages" } } } ``` You can also use environment variables instead of config keys: * `MSTEAMS_APP_ID` * `MSTEAMS_APP_PASSWORD` * `MSTEAMS_TENANT_ID` 5. **Bot endpoint** * Set the Azure Bot Messaging Endpoint to: * `https://:3978/api/messages` (or your chosen path/port). 6. **Run the gateway** * The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials. ## History context * `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt. * Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). * DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[""].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 @mention * `ChannelMessage.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. ```json theme={null} { "$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[].botId` **must** match the Azure Bot App ID. * `webApplicationInfo.id` **must** match the Azure Bot App ID. * `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`). * `bots[].supportsFiles: true` is required for file handling in personal scope. * `authorization.permissions.resourceSpecific` must 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): 1. Update your `manifest.json` with the new settings 2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`) 3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`) 4. 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 5. **For team channels:** Reinstall the app in each team for new permissions to take effect 6. **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. 1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**: * `ChannelMessage.Read.All` (channel attachments + history) * `Chat.Read.All` or `ChatMessage.Read.All` (group chats) 2. **Grant admin consent** for the tenant. 3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**. 4. **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` (default `3978`) * `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) or `newline` to 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](#reply-style-threads-vs-posts)). * `channels.msteams.teams..replyStyle`: per-team override. * `channels.msteams.teams..requireMention`: per-team override. * `channels.msteams.teams..tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing. * `channels.msteams.teams..toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported). * `channels.msteams.teams..channels..replyStyle`: per-channel override. * `channels.msteams.teams..channels..requireMention`: per-channel override. * `channels.msteams.teams..channels..tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). * `channels.msteams.teams..channels..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](#sending-files-in-group-chats)). ## Routing & Sessions * Session keys follow the standard agent format (see [/concepts/session](/concepts/session)): * Direct messages share the main session (`agent::`). * Channel/group messages use conversation id: * `agent::msteams:channel:` * `agent::msteams:group:` ## 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`: * `thread` in a Threads-style channel → replies appear nested awkwardly * `top-level` in 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: ```json theme={null} { "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 1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration: * `Sites.ReadWrite.All` (Application) - upload files to SharePoint * `Chat.Read.All` (Application) - optional, enables per-user sharing links 2. **Grant admin consent** for the tenant. 3. **Get your SharePoint site ID:** ```bash theme={null} # 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" ``` 4. **Configure OpenClaw:** ```json5 theme={null} { 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: ...` * 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:** ```json theme={null} { "action": "send", "channel": "msteams", "target": "user:", "card": { "type": "AdaptiveCard", "version": "1.5", "body": [{ "type": "TextBlock", "text": "Hello!" }] } } ``` **CLI:** ```bash theme={null} 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](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below. ## Target formats MSTeams targets use prefixes to distinguish between users and conversations: | Target type | Format | Example | | ------------------- | -------------------------------- | --------------------------------------------------- | | User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | | User (by name) | `user:` | `user:John Smith` (requires Graph API) | | Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | | Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) | **CLI examples:** ```bash theme={null} # 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:** ```json theme={null} { "action": "send", "channel": "msteams", "target": "user:John Smith", "message": "Hello!" } ``` ```json theme={null} { "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/configuration` for `dmPolicy` and 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 `groupId` query 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:** 1. Use standard channels for bot interactions 2. Use DMs - users can always message the bot directly 3. 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=false` or 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 for `color.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](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 1. Verify `webApplicationInfo.id` matches your bot's App ID exactly 2. Re-upload the app and reinstall in the team/chat 3. Check if your org admin has blocked RSC permissions 4. Confirm you're using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats ## References * [Create Azure Bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) - Azure Bot setup guide * [Teams Developer Portal](https://dev.teams.microsoft.com/apps) - create/manage Teams apps * [Teams app manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) * [Receive channel messages with RSC](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-messages-with-rsc) * [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent) * [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph) * [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) # 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) 1. Use a **separate Signal number** for the bot (recommended). 2. Install `signal-cli` (Java required). 3. Link the bot device and start the daemon: * `signal-cli link -n "OpenClaw"` 4. Configure OpenClaw and start the gateway. Minimal config: ```json5 theme={null} { 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::signal:group:`). ## Config writes By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with: ```json5 theme={null} { channels: { signal: { configWrites: false } }, } ``` ## The number model (important) * The gateway connects to a **Signal device** (the `signal-cli` account). * 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) 1. Install `signal-cli` (Java required). 2. Link a bot account: * `signal-cli link -n "OpenClaw"` then scan the QR in Signal. 3. Configure Signal and start the gateway. Example: ```json5 theme={null} { 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`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) 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: ```json5 theme={null} { 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 signal` * `openclaw pairing approve signal ` * Pairing is the default token exchange for Signal DMs. Details: [Pairing](/start/pairing) * UUID-only senders (from `sourceUuid`) are stored as `uuid:` in `channels.signal.allowFrom`. Groups: * `channels.signal.groupPolicy = open | allowlist | disabled`. * `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. ## How it works (behavior) * `signal-cli` runs 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.ignoreAttachments` to skip downloading media. * Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). ## Typing + read receipts * **Typing indicators**: OpenClaw sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running. * **Read receipts**: when `channels.signal.sendReadReceipts` is true, OpenClaw forwards read receipts for allowed DMs. * Signal-cli does not expose read receipts for groups. ## Reactions (message tool) * Use `message action=react` with `channel=signal`. * Targets: sender E.164 or UUID (use `uuid:` from pairing output; bare UUID works too). * `messageId` is the Signal timestamp for the message you’re reacting to. * Group reactions require `targetAuthor` or `targetAuthorUuid`. 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: targetAuthor=uuid: messageId=1737630212345 emoji=✅ ``` Config: * `channels.signal.actions.reactions`: enable/disable reaction actions (default true). * `channels.signal.reactionLevel`: `off | ack | minimal | extensive`. * `off`/`ack` disables agent reactions (message tool `react` will error). * `minimal`/`extensive` enables agent reactions and sets the guidance level. * Per-account overrides: `channels.signal.accounts..actions.reactions`, `channels.signal.accounts..reactionLevel`. ## Delivery targets (CLI/cron) * DMs: `signal:+15551234567` (or plain E.164). * UUID DMs: `uuid:` (or bare UUID). * Groups: `signal:group:`. * Usernames: `username:` (if supported by your Signal account). ## Configuration reference (Signal) Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.signal.enabled`: enable/disable channel startup. * `channels.signal.account`: E.164 for the bot account. * `channels.signal.cliPath`: path to `signal-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 if `httpUrl` unset). * `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 or `uuid:`). `open` requires `"*"`. 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[""].historyLimit`. * `channels.signal.textChunkLimit`: outbound chunk size (chars). * `channels.signal.chunkMode`: `length` (default) or `newline` to 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) 1. Create a Slack app and enable **Socket Mode**. 2. Create an **App Token** (`xapp-...`) and **Bot Token** (`xoxb-...`). 3. Set tokens for OpenClaw and start the gateway. Minimal config: ```json5 theme={null} { channels: { slack: { enabled: true, appToken: "xapp-...", botToken: "xoxb-...", }, }, } ``` ### Setup 1. Create a Slack app (From scratch) in [https://api.slack.com/apps](https://api.slack.com/apps). 2. **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-...`). 3. **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`). 4. Optional: **OAuth & Permissions** → add **User Token Scopes** (see the read-only list below). Reinstall the app and copy the **User OAuth Token** (`xoxp-...`). 5. **Event Subscriptions** → enable events and subscribe to: * `message.*` (includes edits/deletes/thread broadcasts) * `app_mention` * `reaction_added`, `reaction_removed` * `member_joined_channel`, `member_left_channel` * `channel_rename` * `pin_added`, `pin_removed` 6. Invite the bot to channels you want it to read. 7. Slash Commands → create `/openclaw` if you use `channels.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 set `channels.slack.commands.native: true` (global `commands.native` is `"auto"` which leaves Slack off). 8. 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`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. ### OpenClaw config (minimal) Set tokens via env vars (recommended): * `SLACK_APP_TOKEN=xapp-...` * `SLACK_BOT_TOKEN=xoxb-...` Or via config: ```json5 theme={null} { 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..userToken`. Example with bot + app + user tokens: ```json5 theme={null} { channels: { slack: { enabled: true, appToken: "xapp-...", botToken: "xoxb-...", userToken: "xoxp-...", }, }, } ``` Example with userTokenReadOnly explicitly set (allow user token writes): ```json5 theme={null} { 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: false` and no bot token is available, OpenClaw falls back to the user token. ### History context * `channels.slack.historyLimit` (or `channels.slack.accounts.*.historyLimit`) controls how many recent channel/group messages are wrapped into the prompt. * Falls back to `messages.groupChat.historyLimit`. Set `0` to 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 1. Create a Slack app and **disable Socket Mode** (optional if you only use HTTP). 2. **Basic Information** → copy the **Signing Secret**. 3. **OAuth & Permissions** → install the app and copy the **Bot User OAuth Token** (`xoxb-...`). 4. **Event Subscriptions** → enable events and set the **Request URL** to your gateway webhook path (default `/slack/events`). 5. **Interactivity & Shortcuts** → enable and set the same **Request URL**. 6. **Slash Commands** → set the same **Request URL** for your command(s). Example request URL: `https://gateway-host/slack/events` ### OpenClaw config (minimal) ```json5 theme={null} { channels: { slack: { enabled: true, mode: "http", botToken: "xoxb-...", signingSecret: "your-signing-secret", webhookPath: "/slack/events", }, }, } ``` Multi-account HTTP mode: set `channels.slack.accounts..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. ```json theme={null} { "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/](https://docs.slack.dev/apis/web-api/using-the-conversations-api/) for the overview. ### Bot token scopes (required) * `chat:write` (send/update/delete messages via `chat.postMessage`) [https://docs.slack.dev/reference/methods/chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage) * `im:write` (open DMs via `conversations.open` for user DMs) [https://docs.slack.dev/reference/methods/conversations.open](https://docs.slack.dev/reference/methods/conversations.open) * `channels:history`, `groups:history`, `im:history`, `mpim:history` [https://docs.slack.dev/reference/methods/conversations.history](https://docs.slack.dev/reference/methods/conversations.history) * `channels:read`, `groups:read`, `im:read`, `mpim:read` [https://docs.slack.dev/reference/methods/conversations.info](https://docs.slack.dev/reference/methods/conversations.info) * `users:read` (user lookup) [https://docs.slack.dev/reference/methods/users.info](https://docs.slack.dev/reference/methods/users.info) * `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`) [https://docs.slack.dev/reference/methods/reactions.get](https://docs.slack.dev/reference/methods/reactions.get) [https://docs.slack.dev/reference/methods/reactions.add](https://docs.slack.dev/reference/methods/reactions.add) * `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`) [https://docs.slack.dev/reference/scopes/pins.read](https://docs.slack.dev/reference/scopes/pins.read) [https://docs.slack.dev/reference/scopes/pins.write](https://docs.slack.dev/reference/scopes/pins.write) * `emoji:read` (`emoji.list`) [https://docs.slack.dev/reference/scopes/emoji.read](https://docs.slack.dev/reference/scopes/emoji.read) * `files:write` (uploads via `files.uploadV2`) [https://docs.slack.dev/messaging/working-with-files/#upload](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:history` * `channels:read`, `groups:read`, `im:read`, `mpim:read` * `users:read` * `reactions:read` * `pins:read` * `emoji:read` * `search:read` ### Not needed today (but likely future) * `mpim:write` (only if we add group-DM open/DM start via `conversations.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.public](https://docs.slack.dev/reference/scopes/chat.write.public) * `users:read.email` (only if we need email fields from `users.info`) [https://docs.slack.dev/changelog/2017-04-narrowing-email-access](https://docs.slack.dev/changelog/2017-04-narrowing-email-access) * `files:read` (only if we start listing/reading file metadata) ## Config Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens: ```json theme={null} { "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_TOKEN` * `SLACK_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`: ```json5 theme={null} { 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 (Slack `im`) * `group`: group DMs / MPIMs (Slack `mpim`) * `channel`: standard channels (public/private) Precedence: 1. `replyToModeByChatType.` 2. `replyToMode` 3. 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: ```json5 theme={null} { channels: { slack: { replyToMode: "off", replyToModeByChatType: { direct: "all" }, }, }, } ``` Thread group DMs but keep channels in the root: ```json5 theme={null} { channels: { slack: { replyToMode: "off", replyToModeByChatType: { group: "first" }, }, }, } ``` Make channels thread, keep DMs in the root: ```json5 theme={null} { 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:]]` — reply to a specific message id. ## Sessions + routing * DMs share the `main` session (like WhatsApp/Telegram). * Channels map to `agent::slack:channel:` sessions. * Slash commands use `agent::slack:slash:` sessions (prefix configurable via `channels.slack.slashCommand.sessionPrefix`). * If Slack doesn’t provide `channel_type`, OpenClaw infers it from the channel ID prefix (`D`, `C`, `G`) and defaults to `channel` to keep session keys stable. * Native command registration uses `commands.native` (global default `"auto"` → Slack off) and can be overridden per-workspace with `channels.slack.commands.native`. Text commands require standalone `/...` messages and can be disabled with `commands.text: false`. Slack slash commands are managed in the Slack app and are not removed automatically. Use `commands.useAccessGroups: false` to bypass access-group checks for commands. * Full command list + config: [Slash commands](/tools/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 `. * To allow anyone: set `channels.slack.dm.policy="open"` and `channels.slack.dm.allowFrom=["*"]`. * `channels.slack.dm.allowFrom` accepts 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.groupPolicy` controls channel handling (`open|disabled|allowlist`). * `allowlist` requires channels to be listed in `channels.slack.channels`. * If you only set `SLACK_BOT_TOKEN`/`SLACK_APP_TOKEN` and never create a `channels.slack` section, the runtime defaults `groupPolicy` to `open`. Add `channels.slack.groupPolicy`, `channels.defaults.groupPolicy`, or a channel allowlist to lock it down. * The configure wizard accepts `#channel` names 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.` or `channels.slack.channels.`): * `allow`: allow/deny the channel when `groupPolicy="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`: set `false` to disable the channel. ## Delivery targets Use these with cron/CLI sends: * `user:` for DMs * `channel:` 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: false` allows 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` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions. * Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. * Reaction notifications follow `channels.slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`). * Bot-authored messages are ignored by default; enable via `channels.slack.allowBots` or `channels.slack.channels..allowBots`. * Warning: If you allow replies to other bots (`channels.slack.allowBots=true` or `channels.slack.channels..allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.slack.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. * For the Slack tool, reaction removal semantics are in [/tools/reactions](/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) 1. Create a bot with **@BotFather** ([direct link](https://t.me/BotFather)). Confirm the handle is exactly `@BotFather`, then copy the token. 2. 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). 3. Start the gateway. 4. DM access is pairing by default; approve the pairing code on first contact. Minimal config: ```json5 theme={null} { 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::telegram:group:`). ## Setup (fast path) ### 1) Create a bot token (BotFather) 1. Open Telegram and chat with **@BotFather** ([direct link](https://t.me/BotFather)). Confirm the handle is exactly `@BotFather`. 2. Run `/newbot`, then follow the prompts (name + username ending in `bot`). 3. 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: ```json5 theme={null} { 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`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. 3. Start the gateway. Telegram starts when a token is resolved (config first, env fallback). 4. DM access defaults to pairing. Approve the code when the bot is first contacted. 5. For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists. ## Token + privacy + permissions (Telegram side) ### Token creation (BotFather) * `/newbot` creates 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 `/setprivacy` **or** * 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 `sendReadReceipts` option. ## 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_id` on inbound messages). * `channels.telegram.streamMode` not 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: ```json5 theme={null} { channels: { telegram: { customCommands: [ { command: "backup", description: "Git backup" }, { command: "generate", description: "Create an image" }, ], }, }, } ``` ## Troubleshooting * `setMyCommands failed` in logs usually means outbound HTTPS/DNS is blocked to `api.telegram.org`. * If you see `sendMessage` or `sendChatAction` failures, check IPv6 routing and DNS. More help: [Channel troubleshooting](/channels/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 match `a-z`, `0-9`, `_` (1–32 chars). * Custom commands **cannot override native commands**. Conflicts are ignored and logged. * If `commands.native` is 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` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). * DM history can be limited with `channels.telegram.dmHistoryLimit` (user turns). Per-user overrides: `channels.telegram.dms[""].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) ```json5 theme={null} { 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..topics.`. To allow all groups with always-respond: ```json5 theme={null} { channels: { telegram: { groups: { "*": { requireMention: false }, // all groups, always respond }, }, }, } ``` To keep mention-only for all groups (default behavior): ```json5 theme={null} { 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 migrate `channels.telegram.groups` automatically. * You run `/config set` or `/config unset` in a Telegram chat (requires `commands.config: true`). Disable with: ```json5 theme={null} { channels: { telegram: { configWrites: false } }, } ``` ## Topics (forum supergroups) Telegram forum topics include a `message_thread_id` per message. OpenClaw: * Appends `:topic:` to the Telegram group session key so each topic is isolated. * Sends typing indicators and replies with `message_thread_id` so responses stay in the topic. * General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it. * Exposes `MessageThreadId` + `IsForum` in template context for routing/templating. * Topic-specific configuration is available under `channels.telegram.groups..topics.` (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. ```json5 theme={null} { channels: { telegram: { capabilities: { inlineButtons: "allowlist", }, }, }, } ``` For per-account configuration: ```json5 theme={null} { channels: { telegram: { accounts: { main: { capabilities: { inlineButtons: "allowlist", }, }, }, }, }, } ``` Scopes: * `off` — inline buttons disabled * `dm` — only DMs (group targets blocked) * `group` — only groups (DM targets blocked) * `all` — DMs + groups * `allowlist` — DMs + groups, but only senders allowed by `allowFrom`/`groupAllowFrom` (same rules as control commands) Default: `allowlist`. Legacy: `capabilities: ["inlineButtons"]` = `inlineButtons: "all"`. ### Sending buttons Use the message tool with the `buttons` parameter: ```json5 theme={null} { 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..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 telegram` * `openclaw pairing approve telegram ` * Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing) * `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID. The wizard accepts `@username` and resolves it to the numeric ID when possible. #### Finding your Telegram user ID Safer (no third-party bot): 1. Start the gateway and DM your bot. 2. Run `openclaw logs --follow` and look for `from.id`. Alternate (official Bot API): 1. DM your bot. 2. Fetch updates with your bot token and read `message.from.id`: ```bash theme={null} curl "https://api.telegram.org/bot/getUpdates" ``` Third-party (less private): * DM `@userinfobot` or `@getidsbot` and use the returned user id. ### Group access Two independent controls: **1. Which groups are allowed** (group allowlist via `channels.telegram.groups`): * No `groups` config = all groups allowed * With `groups` config = 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 in `channels.telegram.groupAllowFrom` can message * `"disabled"` = no group messages accepted at all Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`). 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.webhookUrl` and `channels.telegram.webhookSecret` (optionally `channels.telegram.webhookPath`). * The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default. * If your public URL is different, use a reverse proxy and point `channels.telegram.webhookUrl` at the public endpoint. ## Reply threading Telegram supports optional threaded replies via tags: * `[[reply_to_current]]` -- reply to the triggering message. * `[[reply_to:]]` -- 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): ```json5 theme={null} { 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 `` 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 sticker * `setName` — name of the sticker set * `fileId` — Telegram file ID (send the same sticker back) * `fileUniqueId` — stable ID for cache lookup * `cachedDescription` — 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:** 1. **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"). 2. **Cache storage:** The description is saved along with the sticker's file ID, emoji, and set name. 3. **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:** ```json theme={null} { "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: ```json5 theme={null} { channels: { telegram: { actions: { sticker: true, }, }, }, } ``` **Send a sticker:** ```json5 theme={null} { action: "sticker", channel: "telegram", to: "123456789", fileId: "CAACAgIAAxkBAAI...", } ``` Parameters: * `fileId` (required) — the Telegram file ID of the sticker. Obtain this from `Sticker.fileId` when receiving a sticker, or from a `sticker-search` result. * `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: ```json5 theme={null} { action: "sticker-search", channel: "telegram", query: "cat waving", limit: 5, } ``` Returns matching stickers from the cache: ```json5 theme={null} { 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:** ```json5 theme={null} { 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 to `channels.telegram.textChunkLimit`). 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 stream` streams reasoning into the draft bubble while the reply is generating, then sends the final answer without reasoning. * If `channels.telegram.streamMode` is `off`, reasoning stream is disabled. More context: [Streaming + chunking](/concepts/streaming). ## 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](/concepts/retry). ## Agent tool (messages + reactions) * Tool: `telegram` with `sendMessage` action (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`). * Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`). * Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`). * Reaction removal semantics: see [/tools/reactions](/tools/reactions). * Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled), and `channels.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: 1. Receives the `message_reaction` update from Telegram API 2. Converts it to a **system event** with format: `"Telegram reaction added: {emoji} by {user} on msg {id}"` 3. Enqueues the system event using the **same session key** as regular messages 4. 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:** ```json5 theme={null} { channels: { telegram: { reactionNotifications: "all", // See all reactions reactionLevel: "minimal", // Agent can react sparingly }, }, } ``` **Requirements:** * Telegram bots must explicitly request `message_reaction` in `allowed_updates` (configured automatically by OpenClaw) * For webhook mode, reactions are included in the webhook `allowed_updates` * For polling mode, reactions are included in the `getUpdates` `allowed_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) * `openclaw channels status` shows a warning when config expects unmentioned group messages. * `openclaw channels status --probe` can 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.groups` is 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 `/activation` command updates session state but doesn't persist to config * For persistent behavior, add group to `channels.telegram.groups` with `requireMention: 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 `AbortSignal` instances; foreign signals can abort `fetch` calls 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.org` to 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/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway. * Quick check: `dig +short api.telegram.org A` and `dig +short api.telegram.org AAAA` to confirm what DNS returns. ## Configuration reference (Telegram) Full configuration: [Configuration](/gateway/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). `open` requires `"*"`. * `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..requireMention`: mention gating default. * `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none). * `channels.telegram.groups..allowFrom`: per-group sender allowlist override. * `channels.telegram.groups..systemPrompt`: extra system prompt for the group. * `channels.telegram.groups..enabled`: disable the group when `false`. * `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group). * `channels.telegram.groups..topics..requireMention`: per-topic mention gating override. * `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist). * `channels.telegram.accounts..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) or `newline` to 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 (requires `channels.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: `own` when not set). * `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when 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 with `channels.telegram.commands.native`. * `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`. # Channel Troubleshooting Source: https://docs.openclaw.ai/channels/troubleshooting # Channel troubleshooting Start with: ```bash theme={null} 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](/channels/discord#troubleshooting) * Telegram: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting) * WhatsApp: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick) ## Telegram quick fixes * Logs show `HttpError: Network request for 'sendMessage' failed` or `sendChatAction` → check IPv6 DNS. If `api.telegram.org` resolves to IPv6 first and the host lacks IPv6 egress, force IPv4 or enable IPv6. See [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting). * Logs show `setMyCommands failed` → check outbound HTTPS and DNS reachability to `api.telegram.org` (common on locked-down VPS or proxies). # WhatsApp Source: https://docs.openclaw.ai/channels/whatsapp # WhatsApp (web channel) Status: WhatsApp Web via Baileys only. Gateway owns the session(s). ## Quick setup (beginner) 1. Use a **separate phone number** if possible (recommended). 2. Configure WhatsApp in `~/.openclaw/openclaw.json`. 3. Run `openclaw channels login` to scan the QR code (Linked Devices). 4. Start the gateway. Minimal config: ```json5 theme={null} { 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: ```json5 theme={null} { 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):** ```json5 theme={null} { 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 ` ### 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):** ```json theme={null} { "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) * Austria: [hot.at](https://www.hot.at) * UK: [giffgaff](https://www.giffgaff.com) — free SIM, no contract * **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 ` (`` = `accountId`). * Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted). * Credentials stored in `~/.openclaw/credentials/whatsapp//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 `) deletes WhatsApp auth state (but keeps shared `oauth.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.dmPolicy` controls direct chat access (default: `pairing`). * Pairing: unknown senders get a pairing code (approve via `openclaw pairing approve whatsapp `; codes expire after 1 hour). * Open: requires `channels.whatsapp.allowFrom` to include `"*"`. * Your linked WhatsApp number is implicitly trusted, so self messages skip ⁠`channels.whatsapp.dmPolicy` and `channels.whatsapp.allowFrom` checks. ### 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: ```json5 theme={null} { channels: { whatsapp: { sendReadReceipts: false } }, } ``` Disable per account: ```json5 theme={null} { 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 ` (list with `openclaw 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](/concepts/multi-agent). **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) * `Body` is the current message body with envelope. * Quoted reply context is **always appended**: ``` [Replying to +1555 id:ABC123] > [/Replying] ``` * Reply metadata also set: * `ReplyToId` = stanzaId * `ReplyToBody` = quoted body or media placeholder * `ReplyToSender` = E.164 when known * Media-only inbound messages use placeholders: * `` ## Groups * Groups map to `agent::whatsapp:group:` sessions. * Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`). * Activation modes: * `mention` (default): requires @mention or regex match. * `always`: always triggers. * `/activation mention|always` is 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)]` * 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:** ```json theme={null} { "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:** ```json theme={null} { "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`; use `channels.whatsapp.ackReaction` instead. ## Agent tool (reactions) * Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`). * Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account). * Reaction removal semantics: see [/tools/reactions](/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`, optional `channels.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: true` for inline looping. * CLI: `openclaw message send --media --gif-playback` * Gateway: `send` params include `gifPlayback: true` ## Voice notes (PTT audio) WhatsApp sends audio as **voice notes** (PTT bubble). * Best results: OGG/Opus. OpenClaw rewrites `audio/ogg` to `audio/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 via `agents.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_OK` skip behavior. * Delivery defaults to the last used channel (or configured target). ## 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..*` (per-account settings + optional `authDir`). * `channels.whatsapp.accounts..mediaMaxMb` (per-account inbound media cap). * `channels.whatsapp.accounts..ackReaction` (per-account ack reaction override). * `channels.whatsapp.groupAllowFrom` (group sender allowlist). * `channels.whatsapp.groupPolicy` (group policy). * `channels.whatsapp.historyLimit` / `channels.whatsapp.accounts..historyLimit` (group history context; `0` disables). * `channels.whatsapp.dmHistoryLimit` (DM history limit in user turns). Per-user overrides: `channels.whatsapp.dms[""].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` (or `messages.groupChat.mentionPatterns`) * `messages.groupChat.historyLimit` * `channels.whatsapp.messagePrefix` (inbound prefix; per-account: `channels.whatsapp.accounts..messagePrefix`; deprecated: `messages.messagePrefix`) * `messages.responsePrefix` (outbound prefix) * `agents.defaults.mediaMaxMb` * `agents.defaults.heartbeat.every` * `agents.defaults.heartbeat.model` (optional override) * `agents.defaults.heartbeat.target` * `agents.defaults.heartbeat.to` * `agents.defaults.heartbeat.session` * `agents.list[].heartbeat.*` (per-agent overrides) * `session.*` (scope, idle, store, mainKey) * `web.enabled` (disable channel startup when false) * `web.heartbeatSeconds` * `web.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](/gateway/troubleshooting). ## Troubleshooting (quick) **Not linked / QR login required** * Symptom: `channels status` shows `linked: false` or warns “Not linked”. * Fix: run `openclaw channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices). **Linked but disconnected / reconnect loop** * Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”. * Fix: `openclaw doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `openclaw 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](/plugin) ## Quick setup (beginner) 1. 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 2. Set the token: * Env: `ZALO_BOT_TOKEN=...` * Or config: `channels.zalo.botToken: "..."`. 3. Restart the gateway (or finish onboarding). 4. DM access is pairing by default; approve the pairing code on first contact. Minimal config: ```json5 theme={null} { 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) 1. Go to **[https://bot.zaloplatforms.com](https://bot.zaloplatforms.com)** and sign in. 2. Create a new bot and configure its settings. 3. Copy the bot token (format: `12345689:abc-xyz`). ### 2) Configure the token (env or config) Example: ```json5 theme={null} { 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`. 3. Restart the gateway. Zalo starts when a token is resolved (env or config). 4. 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 zalo` * `openclaw pairing approve zalo ` * Pairing is the default token exchange. Details: [Pairing](/start/pairing) * `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available). ## Long-polling vs webhook * Default: long-polling (no public URL required). * Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`. * The webhook secret must be 8-256 characters. * Webhook URL must use HTTPS. * Zalo sends events with `X-Bot-Api-Secret-Token` header 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](/gateway/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). `open` requires `"*"`. 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..botToken`: per-account token. * `channels.zalo.accounts..tokenFile`: per-account token file. * `channels.zalo.accounts..name`: display name. * `channels.zalo.accounts..enabled`: enable/disable account. * `channels.zalo.accounts..dmPolicy`: per-account DM policy. * `channels.zalo.accounts..allowFrom`: per-account allowlist. * `channels.zalo.accounts..webhookUrl`: per-account webhook URL. * `channels.zalo.accounts..webhookSecret`: per-account webhook secret. * `channels.zalo.accounts..webhookPath`: per-account webhook path. * `channels.zalo.accounts..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](/plugin) ## 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.md` or the upstream zca-cli docs). ## Quick setup (beginner) 1. Install the plugin (see above). 2. Login (QR, on the Gateway machine): * `openclaw channels login --channel zalouser` * Scan the QR code in the terminal with the Zalo mobile app. 3. Enable the channel: ```json5 theme={null} { channels: { zalouser: { enabled: true, dmPolicy: "pairing", }, }, } ``` 4. Restart the Gateway (or finish onboarding). 5. DM access defaults to pairing; approve the pairing code on first contact. ## What it is * Uses `zca listen` to 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: ```bash theme={null} 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 zalouser` * `openclaw pairing approve zalouser ` ## Group access (optional) * Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to 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: ```json5 theme={null} { channels: { zalouser: { groupPolicy: "allowlist", groups: { "123456789": { allow: true }, "Work Chat": { allow: true }, }, }, }, } ``` ## Multi-account Accounts map to zca profiles. Example: ```json5 theme={null} { channels: { zalouser: { enabled: true, defaultAccount: "default", accounts: { work: { enabled: true, profile: "work" }, }, }, }, } ``` ## Troubleshooting **`zca` not found:** * Install zca-cli and ensure it’s on `PATH` for 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](/concepts/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](/gateway/configuration)). ## Bootstrap files (injected) Inside `agents.defaults.workspace`, OpenClaw expects these user-editable files: * `AGENTS.md` — operating instructions + “memory” * `SOUL.md` — persona, boundaries, tone * `TOOLS.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/emoji * `USER.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: ```json5 theme={null} { 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: `/skills` Skills can be gated by config/env (see `skills` in [Gateway configuration](/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/agent` or `/.pi` settings are consulted. ## Sessions Session transcripts are stored as JSONL at: * `~/.openclaw/agents//sessions/.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](/concepts/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](/concepts/streaming). ## Model refs Model refs in config (for example `agents.defaults.model` and `agents.defaults.models`) are parsed by splitting on the **first** `/`. * Use `provider/model` when 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.workspace` * `channels.whatsapp.allowFrom` (strongly recommended) *** *Next: [Group Chats](/concepts/group-messages)* 🦞 # 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: `agent` and `agent.wait`. * CLI: `agent` command. ## How it works (high-level) 1. `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately. 2. `agentCommand` runs 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 3. `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 4. `subscribeEmbeddedPiSession` bridges pi-agent-core events to OpenClaw `agent` stream: * tool events => `stream: "tool"` * assistant deltas => `stream: "assistant"` * lifecycle events => `stream: "lifecycle"` (`phase: "start" | "end" | "error"`) 5. `agent.wait` uses `waitForAgentJob`: * waits for **lifecycle end/error** for `runId` * returns `{ status: ok|error|timeout, startedAt, endedAt, error? }` ## 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](/concepts/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; `SessionManager` is 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](/concepts/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](/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](/plugin#plugin-hooks) for the hook API and registration details. ## Streaming + partial replies * Assistant deltas are streamed from pi-agent-core and emitted as `assistant` events. * Block streaming can emit partial replies either on `text_end` or `message_end`. * Reasoning streaming can be emitted as a separate stream or as block replies. * See [Streaming](/concepts/streaming) for chunking and block reply behavior. ## Tool execution + messaging tools * Tool start/update/end events are emitted on the `tool` stream. * 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_REPLY` is 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 `compaction` stream events and can trigger a retry. * On retry, in-memory buffers and tool summaries are reset to avoid duplicate output. * See [Compaction](/concepts/compaction) for the compaction pipeline. ## Event streams (today) * `lifecycle`: emitted by `subscribeEmbeddedPiSession` (and as a fallback by `agentCommand`) * `assistant`: streamed deltas from pi-agent-core * `tool`: streamed tool events from pi-agent-core ## Chat channel handling * Assistant deltas are buffered into chat `delta` messages. * A chat `final` is emitted on **lifecycle end/error**. ## Timeouts * `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides. * Agent runtime: `agents.defaults.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer. ## Where things can end early * Agent timeout (abort) * AbortSignal (cancel) * Gateway disconnect or RPC timeout * `agent.wait` timeout (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`](/gateway/sandboxing) (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_PROFILE` is set and not `"default"`, the default becomes `~/.openclaw/workspace-`. * Override in `~/.openclaw/openclaw.json`: ```json5 theme={null} { 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: ```json5 theme={null} { 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](/concepts/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`). 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//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: ```bash theme={null} 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 1. Create a new **private** repository on GitHub. 2. Do not initialize with a README (avoids merge conflicts). 3. Copy the HTTPS remote URL. 4. Add the remote and push: ```bash theme={null} git branch -M main git remote add origin git push -u origin main ``` Option B: GitHub CLI (`gh`) ```bash theme={null} gh auth login gh repo create openclaw-workspace --private --source . --remote origin --push ``` Option C: GitLab web UI 1. Create a new **private** repository on GitLab. 2. Do not initialize with a README (avoids merge conflicts). 3. Copy the HTTPS remote URL. 4. Add the remote and push: ```bash theme={null} git branch -M main git remote add origin git push -u origin main ``` ### 3) Ongoing updates ```bash theme={null} 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: ```gitignore theme={null} .DS_Store .env **/*.key **/*.pem **/secrets* ``` ## Moving the workspace to a new machine 1. Clone the repo to the desired path (default `~/.openclaw/workspace`). 2. Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`. 3. Run `openclaw setup --workspace ` to seed any missing files. 4. If you need sessions, copy `~/.openclaw/agents//sessions/` from the old machine separately. ## Advanced notes * Multi-agent routing can use different workspaces per agent. See [Channel routing](/concepts/channel-routing) for routing configuration. * If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox workspaces under `agents.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: node` with 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** (role `node`) and approval lives in the device pairing store. * Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`. Protocol details: * [Gateway protocol](/gateway/protocol) ### 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?}` * If `OPENCLAW_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token` must 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 in `connect`. ## 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.challenge` nonce and require explicit approval. * Gateway auth (`gateway.auth.*`) still applies to **all** connections, local or remote. Details: [Gateway protocol](/gateway/protocol), [Pairing](/start/pairing), [Security](/gateway/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 ```bash theme={null} 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: `health` over WS (also included in `hello-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::` (default: `agent:main:main`) Groups and channels remain isolated per channel: * Groups: `agent:::group:` * Channels/rooms: `agent:::channel:` Threads: * Slack/Discord threads append `:thread:` to the base key. * Telegram forum topics embed `:topic:` in the group key. Examples: * `agent:main:telegram:group:-1001234567890:topic:42` * `agent:main:discord:channel:123456:thread:987654` ## Routing rules (how an agent is chosen) Routing picks **one agent** for each inbound message: 1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`). 2. **Guild match** (Discord) via `guildId`. 3. **Team match** (Slack) via `teamId`. 4. **Account match** (`accountId` on the channel). 5. **Channel match** (any account on that channel). 6. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`). 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: ```json5 theme={null} { broadcast: { strategy: "parallel", "120363403215116621@g.us": ["alfred", "baerbel"], "+15555550123": ["support", "logger"], }, } ``` See: [Broadcast Groups](/broadcast-groups). ## Config overview * `agents.list`: named agent definitions (workspace, model, etc.). * `bindings`: map inbound channels/accounts/peers to agents. Example: ```json5 theme={null} { 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//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`, and `ReplyToSender` when available. * Quoted context is appended to `Body` as 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](/concepts/compaction) 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 complete` in verbose mode * `/status` showing `🧹 Compactions: ` Before compaction, OpenClaw can run a **silent memory flush** turn to store durable notes to disk. See [Memory](/concepts/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](/concepts/session-pruning) for pruning details. ## Tips * Use `/compact` when 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, `/new` or `/reset` starts 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](/tools/slash-commands), [Token use & costs](/token-use), [Compaction](/concepts/compaction). ## Example output Values vary by model, provider, tool policy, and what’s in your workspace. ### `/context list` ``` 🧠 Context breakdown Workspace: 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](/concepts/system-prompt). ## Injected workspace files (Project Context) By default, OpenClaw injects a fixed set of workspace files (if present): * `AGENTS.md` * `SOUL.md` * `TOOLS.md` * `IDENTITY.md` * `USER.md` * `HEARTBEAT.md` * `BOOTSTRAP.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: 1. **Tool list text** in the system prompt (what you see as “Tooling”). 2. **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`, `/queue` are 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](/tools/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](/concepts/session), [Compaction](/concepts/compaction), [Session pruning](/concepts/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 Legacy Claude, Codex, Gemini, and Opencode paths have been removed. Pi is the only coding agent path. # 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) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). * Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). * Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on` or `/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 `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "". 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: ```json5 theme={null} { 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 `@openclaw` and the raw number with or without `+`/spaces. * WhatsApp still sends canonical mentions via `mentionedJids` when 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 1. Add your WhatsApp account (the one running OpenClaw) to the group. 2. Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. 3. The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person. 4. Session-level directives (`/verbose on`, `/think high`, `/new` or `/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 `@openclaw` ping 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. * Check gateway logs (run with `--verbose`) to see `inbound web message` entries showing `from: ` 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::whatsapp:group:` in the session store (`~/.openclaw/agents//sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet. * Typing indicators in groups follow `agents.defaults.typingMode` (default: `message` when 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 ``` Group message flow 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: { "": { ... } }` (no `"*"` key) | | Only you can trigger in groups | `groupPolicy: "allowlist"`, `groupAllowFrom: ["+1555..."]` | ## Session keys * Group sessions use `agent:::group:` session keys (rooms/channels use `agent:::channel:`). * Telegram forum topics add `:topic:` 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::group:`). 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](/concepts/multi-agent). Example (DMs on host, groups sandboxed + messaging-only tools): ```json5 theme={null} { 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: ```json5 theme={null} { 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](/gateway/configuration#agentsdefaultssandbox) * Debugging why a tool is blocked: [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) * Bind mounts details: [Sandboxing](/gateway/sandboxing#custom-bind-mounts) ## Display labels * UI labels use `displayName` when available, formatted as `:`. * `#room` is reserved for rooms/channels; group chats use `g-` (lowercase, spaces -> `-`, keep `#@+._-`). ## Group policy Control how group/room messages are handled per channel: ```json5 theme={null} { 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: * `groupPolicy` is separate from mention-gating (which requires @mentions). * WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`). * Discord: allowlist uses `channels.discord.guilds..channels`. * Slack: allowlist uses `channels.slack.channels`. * Matrix: allowlist uses `channels.matrix.groups` (room IDs, aliases, or names). Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists 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): 1. `groupPolicy` (open/disabled/allowlist) 2. group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist) 3. 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. ```json5 theme={null} { 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: * `mentionPatterns` are 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 `mentionPatterns` are 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.historyLimit` for the global default and `channels..historyLimit` (or `channels..accounts.*.historyLimit`) for overrides. Set `0` to 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): 1. group/channel `toolsBySender` match 2. group/channel `tools` 3. default (`"*"`) `toolsBySender` match 4. default (`"*"`) `tools` Example (Telegram): ```json5 theme={null} { 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.*`, Slack `channels.*`, MS Teams `teams.*.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): 1. Disable all group replies ```json5 theme={null} { channels: { whatsapp: { groupPolicy: "disabled" } }, } ``` 2. Allow only specific groups (WhatsApp) ```json5 theme={null} { channels: { whatsapp: { groups: { "123@g.us": { requireMention: true }, "456@g.us": { requireMention: false }, }, }, }, } ``` 3. Allow all groups but require mention (explicit) ```json5 theme={null} { channels: { whatsapp: { groups: { "*": { requireMention: true } }, }, }, } ``` 4. Only the owner can trigger in groups (WhatsApp) ```json5 theme={null} { 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=group` * `GroupSubject` (if known) * `GroupMembers` (if known) * `WasMentioned` (mention gating result) * Telegram forum topics also include `MessageThreadId` and `IsForum`. 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:` 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](/concepts/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](/concepts/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`: ```json5 theme={null} { 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_REPLY` so 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](/reference/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.provider` is not set, OpenClaw auto-selects: 1. `local` if a `memorySearch.local.modelPath` is configured and the file exists. 2. `openai` if an OpenAI key can be resolved. 3. `gemini` if a Gemini key can be resolved. 4. 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](https://github.com/tobi/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/qmd` or grab a release) and make sure the `qmd` binary is on the gateway’s `PATH`. * QMD needs an SQLite build that allows extensions (`brew install sqlite` on macOS). * QMD runs fully locally via Bun + `node-llama-cpp` and 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//qmd/` by setting `XDG_CONFIG_HOME` and `XDG_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//qmd/` (config + cache + sqlite DB). * Collections are rewritten from `memory.qmd.paths` (plus default workspace memory files) into `index.yml`, then `qmd update` + `qmd embed` run 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 query` run. * OpenClaw sets `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` automatically 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 point `qmd` at the exact same index by exporting the same XDG vars OpenClaw uses: ```bash theme={null} # 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` (default `qmd`): override the executable path. * `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`. * `paths[]`: add extra directories/files (`path`, optional `pattern`, optional stable `name`). * `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 as [`session.sendPolicy`](/gateway/configuration#session). Default is DM-only (`deny` all, `allow` direct chats); loosen it to surface QMD hits in groups/channels. * Snippets sourced outside the workspace show up as `qmd//` in `memory_search` results; `memory_get` understands 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//qmd/sessions/`, so `memory_search` can recall recent conversations without touching the builtin SQLite index. * `memory_search` snippets now include a `Source: ` footer when `memory.citations` is `auto`/`on`; set `memory.citations = "off"` to keep the path metadata internal (the agent still receives the path for `memory_get`, but the snippet text omits the footer and the system prompt warns the agent not to cite it). **Example** ```json5 theme={null} 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.citations` applies regardless of backend (`auto`/`on`/`off`). * When `qmd` runs, we tag `status().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: ```json5 theme={null} agents: { defaults: { memorySearch: { extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"] } } } ``` Notes: * Paths can be absolute or workspace-relative. * Directories are scanned recursively for `.md` files. * 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: ```json5 theme={null} agents: { defaults: { memorySearch: { provider: "gemini", model: "gemini-embedding-001", remote: { apiKey: "YOUR_GEMINI_API_KEY" } } } } ``` Notes: * `remote.baseUrl` is optional (defaults to the Gemini API base URL). * `remote.headers` lets 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: ```json5 theme={null} 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.fallback` can be `openai`, `gemini`, `local`, or `none`. * 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 = false` to disable. * Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed. * Set `remote.batch.concurrency` to 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: * [https://platform.openai.com/docs/api-reference/batch](https://platform.openai.com/docs/api-reference/batch) * [https://platform.openai.com/pricing](https://platform.openai.com/pricing) Config example: ```json5 theme={null} 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 or `hf:` URI). * Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback. ### How the memory tools work * `memory_search` semantically searches Markdown chunks (\~400 token target, 80-token overlap) from `MEMORY.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_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected. * Both tools are enabled only when `memorySearch.enabled` resolves 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/.sqlite` (configurable via `agents.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: 1. Retrieve a candidate pool from both sides: * **Vector**: top `maxResults * candidateMultiplier` by cosine similarity. * **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better). 2. Convert BM25 rank into a 0..1-ish score: * `textScore = 1 / (1 + max(0, bm25Rank))` 3. Union candidates by chunk id and compute a weighted score: * `finalScore = vectorWeight * vectorScore + textWeight * textScore` Notes: * `vectorWeight` + `textWeight` is 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: ```json5 theme={null} 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: ```json5 theme={null} 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. ```json5 theme={null} 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_search` never blocks on indexing; results can be slightly stale until background sync finishes. * Results still include snippets only; `memory_get` remains 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//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): ```json5 theme={null} 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): ```json5 theme={null} agents: { defaults: { memorySearch: { store: { vector: { enabled: true, extensionPath: "/path/to/sqlite-vec" } } } } } ``` Notes: * `enabled` defaults 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). * `extensionPath` overrides 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-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry. * Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`. * Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason. ### Custom OpenAI-compatible endpoint example ```json5 theme={null} 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 over `models.providers.openai.*`. * `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to 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](/gateway/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): ```json5 theme={null} { 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](/concepts/session). ## 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 for `CommandBody` (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..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` (and `messages.queue.byChannel`). * Modes: `interrupt`, `steer`, `followup`, `collect`, plus backlog variants. Details: [Queueing](/concepts/queue). ## 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: `*.blockStreaming` and `*.blockStreamingCoalesce` (non-Telegram channels require explicit `*.blockStreaming: true`) Details: [Streaming + chunking](/concepts/streaming). ## Reasoning visibility and tokens OpenClaw can expose or hide model reasoning: * `/reasoning on|off|stream` controls 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](/tools/thinking) and [Token use](/token-use). ## Prefixes, threading, and replies Outbound message formatting is centralized in `messages`: * `messages.responsePrefix`, `channels..responsePrefix`, and `channels..accounts..responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix) * Reply threading via `replyToMode` and per-channel defaults Details: [Configuration](/gateway/configuration#messages) 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//sessions`. Auth profiles are **per-agent**. Each agent reads from its own: ``` ~/.openclaw/agents//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](/tools/skills#per-agent-vs-shared-skills). 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](/gateway/sandboxing). ## Paths (quick map) * Config: `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`) * State dir: `~/.openclaw` (or `OPENCLAW_STATE_DIR`) * Workspace: `~/.openclaw/workspace` (or `~/.openclaw/workspace-`) * Agent dir: `~/.openclaw/agents//agent` (or `agents.list[].agentDir`) * Sessions: `~/.openclaw/agents//sessions` ### Single-agent mode (default) If you do nothing, OpenClaw runs a single agent: * `agentId` defaults to **`main`**. * Sessions are keyed as `agent:main:`. * Workspace defaults to `~/.openclaw/workspace` (or `~/.openclaw/workspace-` when `OPENCLAW_PROFILE` is set). * State defaults to `~/.openclaw/agents/main/agent`. ## Agent helper Use the agent wizard to add a new isolated agent: ```bash theme={null} openclaw agents add work ``` Then add `bindings` (or let the wizard do it) to route inbound messages. Verify with: ```bash theme={null} 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.md` and `SOUL.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: ```json5 theme={null} { 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](/broadcast-groups). ## Routing rules (how messages pick an agent) Bindings are **deterministic** and **most-specific wins**: 1. `peer` match (exact DM/group/channel id) 2. `guildId` (Discord) 3. `teamId` (Slack) 4. `accountId` match for a channel 5. channel-level match (`accountId: "*"`) 6. 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 an `agentId` by `(channel, accountId, peer)` and optionally guild/team ids. * Direct chats collapse to `agent::` (per-agent “main”; `session.mainKey`). ## Example: two WhatsApps → two agents `~/.openclaw/openclaw.json` (JSON5): ```js theme={null} { 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. ```json5 theme={null} { 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 `accountId` to 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.peer` binding 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: ```json5 theme={null} { 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: ```json5 theme={null} { 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 `exec` is allowed and the binary exists in the sandbox. * For stricter gating, set `agents.list[].groupChat.mentionPatterns` and 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: ```js theme={null} { 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](/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: ```bash theme={null} openclaw models auth login --provider ``` ## 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//agent/auth-profiles.json` * Runtime cache (managed automatically; don’t edit): `~/.openclaw/agents//agent/auth.json` Legacy import-only file (still supported, but not the main store): * `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use) All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys) ## Anthropic setup-token (subscription auth) Run `claude setup-token` on any machine, then paste it into OpenClaw: ```bash theme={null} openclaw models auth setup-token --provider anthropic ``` If you generated the token elsewhere, paste it manually: ```bash theme={null} openclaw models auth paste-token --provider anthropic ``` Verify: ```bash theme={null} 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: 1. run `claude setup-token` 2. paste the token into OpenClaw 3. 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): 1. generate PKCE verifier/challenge + random `state` 2. open `https://auth.openai.com/oauth/authorize?...` 3. try to capture callback on `http://127.0.0.1:1455/auth/callback` 4. if callback can’t bind (or you’re remote/headless), paste the redirect URL/code 5. exchange at `https://auth.openai.com/oauth/token` 6. extract `accountId` from 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 `expires` is 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): ```bash theme={null} 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 ...@` Example (session override): * `/model Opus@anthropic:work` How to see what profile IDs exist: * `openclaw channels list --json` (shows `auth[]`) Related docs: * [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules) * [/tools/slash-commands](/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 (usually `connect.client.instanceId`) * `host`: human‑friendly host name * `ip`: best‑effort IP address * `version`: client version string * `deviceFamily` / `modelIdentifier`: hardware hints * `mode`: `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` (from `connect.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-presence` against the Gateway. * If you see duplicates: * confirm clients send a stable `client.instanceId` in the handshake * confirm periodic beacons use the same `instanceId` * check whether the connection‑derived entry is missing `instanceId` (duplicates are expected) # 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). * `runEmbeddedPiAgent` enqueues by **session key** (lane `session:`) to guarantee only one active run per session. * Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.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` (aka `steer+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 as `steer`. 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`: ```json5 theme={null} { 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 ` 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 default` or `/queue reset` clears 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; set `agents.defaults.maxConcurrent` to 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_after` when available, otherwise exponential backoff. ### Telegram * Retries on transient errors (429, timeout, connect/reset/closed, temporarily unavailable). * Uses `retry_after` when 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`: ```json5 theme={null} { 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::` (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). Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-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: ```json5 theme={null} // ~/.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.identityLinks` to 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//sessions/sessions.json` (per agent). * Transcripts: `~/.openclaw/agents//sessions/.jsonl` (Telegram topic sessions use `.../-topic-.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`, and `space` to label sessions in UIs. * Session entries include `origin` metadata (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](/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](/concepts/memory) and [Compaction](/concepts/compaction). ## Mapping transports → session keys * Direct chats follow `session.dmScope` (default `main`). * `main`: `agent::` (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::dm:`. * `per-channel-peer`: `agent:::dm:`. * `per-account-channel-peer`: `agent::::dm:` (accountId defaults to `default`). * If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `` so the same person shares a session across channels. * Group chats isolate state: `agent:::group:` (rooms/channels use `agent:::channel:`). * Telegram forum topics append `:topic:` to the group id for isolation. * Legacy `group:` keys are still recognized for migration. * Inbound contexts may still use `group:`; the channel is inferred from `Provider` and normalized to the canonical `agent:::group:` form. * Other sources: * Cron jobs: `cron:` * Webhooks: `hook:` (unless explicitly set by the hook) * Node runs: `node-` ## 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): `idleMinutes` adds 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.idleMinutes` without any `session.reset`/`resetByType` config, OpenClaw stays in idle-only mode for backward compatibility. * Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector). * Per-channel overrides (optional): `resetByChannel` overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over `reset`/`resetByType`). * Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new ` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is 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 `sessionId` per run (no idle reuse). ## Send policy (optional) Block delivery for specific session types without listing individual ids. ```json5 theme={null} { 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) ```json5 theme={null} // ~/.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 `). * `openclaw gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access). * Send `/status` as 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 list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors). * Send `/stop` as 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](/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 envelope * `accountId`: 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 sending `ConversationLabel`, `GroupSubject`, `GroupChannel`, `GroupSpace`, and `SenderName` in the inbound context and calling `recordSessionMetaFromInbound` (or passing the same context to `updateLastRoute`). # 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 than `ttl`. * 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 `ttl` to your model `cacheControlTtl`. * After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again. ## Smart defaults (Anthropic) * **OAuth or setup-token** profiles: enable `cache-ttl` pruning and set heartbeat to `1h`. * **API key** profiles: enable `cache-ttl` pruning, set heartbeat to `30m`, and default `cacheControlTtl` to `1h` on 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 `toolResult` messages. * User + assistant messages are **never** modified. * The last `keepLastAssistants` assistant 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: 1. `models.providers.*.models[].contextWindow` override. 2. Model definition `contextWindow` (from the model registry). 3. Default `200000` tokens. 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` (default `5m`). * 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. * **Hard-clear**: replaces the entire tool result with `hardClear.placeholder`. ## Tool selection * `tools.allow` / `tools.deny` support `*` 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](/concepts/compaction). ## Defaults (when enabled) * `ttl`: `"5m"` * `keepLastAssistants`: `3` * `softTrimRatio`: `0.3` * `hardClearRatio`: `0.5` * `minPrunableToolChars`: `50000` * `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }` * `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }` ## Examples Default (off): ```json5 theme={null} { agent: { contextPruning: { mode: "off" }, }, } ``` Enable TTL-aware pruning: ```json5 theme={null} { agent: { contextPruning: { mode: "cache-ttl", ttl: "5m" }, }, } ``` Restrict pruning to specific tools: ```json5 theme={null} { agent: { contextPruning: { mode: "cache-ttl", tools: { allow: ["exec", "read"], deny: ["*image*"] }, }, }, } ``` See config reference: [Gateway Configuration](/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_list` * `sessions_history` * `sessions_send` * `sessions_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:::group:` or `agent:::channel:` (pass the full key). * Cron jobs use `cron:`. * Hooks use `hook:` unless explicitly set. * Node sessions use `node-` 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?: number` max rows (default: server default, clamp e.g. 200) * `activeMinutes?: number` only sessions updated within N minutes * `messageLimit?: number` 0 = no messages (default 0); >0 = include last N messages Behavior: * `messageLimit > 0` fetches `chat.history` per session and includes the last N messages. * Tool results are filtered out in list output; use `sessions_history` for 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 | other` * `channel`: `whatsapp | telegram | discord | signal | imessage | webchat | internal | unknown` * `displayName` (group display label if available) * `updatedAt` (ms) * `sessionId` * `model`, `contextTokens`, `totalTokens` * `thinkingLevel`, `verboseLevel`, `systemSent`, `abortedLastRun` * `sendPolicy` (session override if set) * `lastChannel`, `lastTo` * `deliveryContext` (normalized `{ channel, to, accountId }` when available) * `transcriptPath` (best-effort path derived from store dir + sessionId) * `messages?` (only when `messageLimit > 0`) ## sessions\_history Fetch transcript for one session. Parameters: * `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`) * `limit?: number` max messages (server clamps) * `includeTools?: boolean` (default false) Behavior: * `includeTools=false` filters `role: "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 or `sessionId` from `sessions_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; call `sessions_history` later. * 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_SKIP` to 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_SKIP` to stay silent. * Any other reply is sent to the target channel. * Announce step includes the original request + round‑1 reply + latest ping‑pong reply. ## Channel Field * For groups, `channel` is the channel recorded on the session entry. * For direct chats, `channel` maps from `lastChannel`. * For cron/hook/node, `channel` is `internal`. * If missing, `channel` is `unknown`. ## Security / Send Policy Policy-based blocking by channel/chat type (not per session id). ```json theme={null} { "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.patch` or 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`, default `keep`) Allowlist: * `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent. Discovery: * Use `agents_list` to discover which agent ids are allowed for `sessions_spawn`. Behavior: * Starts a new `agent::subagent:` session with `deliver: 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_SKIP` during the announce step to stay silent. * Announce replies are normalized to `Status`/`Result`/`Notes`; `Status` comes 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: ```json5 theme={null} { 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](/concepts/session). # 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`: `EmbeddedBlockChunker` applying 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` (`length` default, `newline` splits 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 each `text_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 at `maxChars`. * **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 `maxChars` and will flush if they exceed it. * `minChars` prevents 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 `minChars` is 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 via `agents.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 `sendMessageDraft` in **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: true` on non-Telegram channels. * Final reply is still a normal message. * `/reasoning stream` writes 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.apply` and `update.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.md` * `SOUL.md` * `TOOLS.md` * `IDENTITY.md` * `USER.md` * `HEARTBEAT.md` * `BOOTSTRAP.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](/concepts/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.userTimezone` * `agents.defaults.timeFormat` (`auto` | `12` | `24`) See [Date & Time](/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. ``` ... ... ... ``` 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](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](/automation/webhook) or use `openclaw webhooks` for Gmail helper commands. Hooks can also be bundled inside plugins; see [Plugins](/plugin#plugin-hooks). 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 `/new` is 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.md` when the gateway starts (requires internal hooks enabled) * **😈 soul-evil**: Swaps injected `SOUL.md` content with `SOUL_EVIL.md` during a purge window or by random chance List available hooks: ```bash theme={null} openclaw hooks list ``` Enable a hook: ```bash theme={null} openclaw hooks enable session-memory ``` Check hook status: ```bash theme={null} openclaw hooks check ``` Get detailed information: ```bash theme={null} 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): 1. **Workspace hooks**: `/hooks/` (per-agent, highest precedence) 2. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces) 3. **Bundled hooks**: `/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: ```bash theme={null} openclaw hooks install ``` Example `package.json`: ```json theme={null} { "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/`. ## Hook Structure ### HOOK.md Format The `HOOK.md` file contains metadata in YAML frontmatter plus Markdown documentation: ```markdown theme={null} --- 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 URL * **`requires`**: Optional requirements * **`bins`**: Required binaries on PATH (e.g., `["git", "node"]`) * **`anyBins`**: At least one of these binaries must be present * **`env`**: Required environment variables * **`config`**: 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: ```typescript theme={null} 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: ```typescript theme={null} { 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 `/new` command is issued * **`command:reset`**: When `/reset` command is issued * **`command:stop`**: When `/stop` command is issued ### Agent Events * **`agent:bootstrap`**: Before workspace bootstrap files are injected (hooks may mutate `context.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 or `undefined` to keep it as-is. See [Agent Loop](/concepts/agent-loop). ### Future Events Planned event types: * **`session:start`**: When a new session begins * **`session:end`**: When a session ends * **`agent:error`**: When an agent encounters an error * **`message:sent`**: When a message is sent * **`message:received`**: When a message is received ## Creating Custom Hooks ### 1. Choose Location * **Workspace hooks** (`/hooks/`): Per-agent, highest precedence * **Managed hooks** (`~/.openclaw/hooks/`): Shared across workspaces ### 2. Create Directory Structure ```bash theme={null} mkdir -p ~/.openclaw/hooks/my-hook cd ~/.openclaw/hooks/my-hook ``` ### 3. Create HOOK.md ```markdown theme={null} --- 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 ```typescript theme={null} 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 ```bash theme={null} # 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) ```json theme={null} { "hooks": { "internal": { "enabled": true, "entries": { "session-memory": { "enabled": true }, "command-logger": { "enabled": false } } } } } ``` ### Per-Hook Configuration Hooks can have custom configuration: ```json theme={null} { "hooks": { "internal": { "enabled": true, "entries": { "my-hook": { "enabled": true, "env": { "MY_CUSTOM_VAR": "value" } } } } } } ``` ### Extra Directories Load hooks from additional directories: ```json theme={null} { "hooks": { "internal": { "enabled": true, "load": { "extraDirs": ["/path/to/more/hooks"] } } } } ``` ### Legacy Config Format (Still Supported) The old config format still works for backwards compatibility: ```json theme={null} { "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 ```bash theme={null} # 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 ```bash theme={null} # Show detailed info about a hook openclaw hooks info session-memory # JSON output openclaw hooks info session-memory --json ``` ### Check Eligibility ```bash theme={null} # Show eligibility summary openclaw hooks check # JSON output openclaw hooks check --json ``` ### Enable/Disable ```bash theme={null} # 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**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) **What it does**: 1. Uses the pre-reset session entry to locate the correct transcript 2. Extracts the last 15 lines of conversation 3. Uses LLM to generate a descriptive filename slug 4. Saves session metadata to a dated memory file **Example output**: ```markdown theme={null} # 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.md` * `2026-01-16-api-design.md` * `2026-01-16-1430.md` (fallback timestamp if slug generation fails) **Enable**: ```bash theme={null} 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**: 1. Captures event details (command action, timestamp, session key, sender ID, source) 2. Appends to log file in JSONL format 3. Runs silently in the background **Example log entries**: ```jsonl theme={null} {"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**: ```bash theme={null} # 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**: ```bash theme={null} 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](/hooks/soul-evil) **Output**: No files written; swaps happen in-memory only. **Enable**: ```bash theme={null} openclaw hooks enable soul-evil ``` **Config**: ```json theme={null} { "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**: 1. Reads `BOOT.md` from your workspace 2. Runs the instructions via the agent runner 3. Sends any requested outbound messages via the message tool **Enable**: ```bash theme={null} openclaw hooks enable boot-md ``` ## Best Practices ### Keep Handlers Fast Hooks run during command processing. Keep them lightweight: ```typescript theme={null} // ✓ 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: ```typescript theme={null} 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: ```typescript theme={null} 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: ```yaml theme={null} metadata: { "openclaw": { "events": ["command:new"] } } # Specific ``` Rather than: ```yaml theme={null} 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: ```bash theme={null} openclaw hooks list --verbose ``` ### Check Registration In your handler, log when it's called: ```typescript theme={null} 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: ```bash theme={null} openclaw hooks info my-hook ``` Look for missing requirements in the output. ## Testing ### Gateway Logs Monitor gateway logs to see hook execution: ```bash theme={null} # macOS ./scripts/clawlog.sh -f # Other platforms tail -f ~/.openclaw/gateway.log ``` ### Test Hooks Directly Test your handlers in isolation: ```typescript theme={null} 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 definitions * **`src/hooks/workspace.ts`**: Directory scanning and loading * **`src/hooks/frontmatter.ts`**: HOOK.md metadata parsing * **`src/hooks/config.ts`**: Eligibility checking * **`src/hooks/hooks-status.ts`**: Status reporting * **`src/hooks/loader.ts`**: Dynamic module loader * **`src/cli/hooks-cli.ts`**: CLI commands * **`src/gateway/server-startup.ts`**: Loads hooks at gateway start * **`src/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 1. Check directory structure: ```bash theme={null} ls -la ~/.openclaw/hooks/my-hook/ # Should show: HOOK.md, handler.ts ``` 2. Verify HOOK.md format: ```bash theme={null} cat ~/.openclaw/hooks/my-hook/HOOK.md # Should have YAML frontmatter with name and metadata ``` 3. List all discovered hooks: ```bash theme={null} openclaw hooks list ``` ### Hook Not Eligible Check requirements: ```bash theme={null} openclaw hooks info my-hook ``` Look for missing: * Binaries (check PATH) * Environment variables * Config values * OS compatibility ### Hook Not Executing 1. Verify hook is enabled: ```bash theme={null} openclaw hooks list # Should show ✓ next to enabled hooks ``` 2. Restart your gateway process so hooks reload. 3. Check gateway logs for errors: ```bash theme={null} ./scripts/clawlog.sh | grep hook ``` ### Handler Errors Check for TypeScript/import errors: ```bash theme={null} # Test import directly node -e "import('./path/to/handler.ts').then(console.log)" ``` ## Migration Guide ### From Legacy Config to Discovery **Before**: ```json theme={null} { "hooks": { "internal": { "enabled": true, "handlers": [ { "event": "command:new", "module": "./hooks/handlers/my-handler.ts" } ] } } } ``` **After**: 1. Create hook directory: ```bash theme={null} mkdir -p ~/.openclaw/hooks/my-hook mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts ``` 2. Create HOOK.md: ```markdown theme={null} --- name: my-hook description: "My custom hook" metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } } --- # My Hook Does something useful. ``` 3. Update config: ```json theme={null} { "hooks": { "internal": { "enabled": true, "entries": { "my-hook": { "enabled": true } } } } } ``` 4. Verify and restart your gateway process: ```bash theme={null} openclaw hooks list # Should show: 🎯 my-hook ✓ ``` **Benefits of migration**: * Automatic discovery * CLI management * Eligibility checking * Better documentation * Consistent structure ## See Also * [CLI Reference: hooks](/cli/hooks) * [Bundled Hooks README](https://github.com/openclaw/openclaw/tree/main/src/hooks/bundled) * [Webhook Hooks](/automation/webhook) * [Configuration](/gateway/configuration#hooks) # 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 ```bash theme={null} openclaw hooks enable soul-evil ``` Then set the config: ```json theme={null} { "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 use `SOUL_EVIL.md` * `purge.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.md` is not in the bootstrap list, the hook does nothing. ## See Also * [Hooks](/hooks) # OpenClaw Source: https://docs.openclaw.ai/index # OpenClaw 🦞

OpenClaw 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.

Install OpenClaw and bring up the Gateway in minutes. Guided setup with `openclaw onboard` and pairing flows. Launch the browser dashboard for chat, config, and sessions. 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 ```mermaid theme={null} 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](/start/quickstart). ## Dashboard Open the browser Control UI after the Gateway starts. * Local default: [http://127.0.0.1:18789/](http://127.0.0.1:18789/) * Remote access: [Web surfaces](/web) and [Tailscale](/gateway/tailscale)

OpenClaw

## 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.allowFrom` and (for groups) mention rules. Example: ```json5 theme={null} { 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](https://github.com/openclaw/openclaw-ansible)** — an automated installer with security-first architecture. ## Quick Start One-command install: ```bash theme={null} curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash ``` > **📦 Full guide: [github.com/openclaw/openclaw-ansible](https://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: 1. **Tailscale** (mesh VPN for secure remote access) 2. **UFW firewall** (SSH + Tailscale ports only) 3. **Docker CE + Compose V2** (for agent sandboxes) 4. **Node.js 22.x + pnpm** (runtime dependencies) 5. **OpenClaw** (host-based, not containerized) 6. **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](/gateway/sandboxing) for details. ## Post-Install Setup After installation completes, switch to the openclaw user: ```bash theme={null} sudo -i -u openclaw ``` The post-install script will guide you through: 1. **Onboarding wizard**: Configure OpenClaw settings 2. **Provider login**: Connect WhatsApp/Telegram/Discord/Signal 3. **Gateway testing**: Verify the installation 4. **Tailscale setup**: Connect to your VPN mesh ### Quick commands ```bash theme={null} # 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 1. **Firewall (UFW)**: Only SSH (22) + Tailscale (41641/udp) exposed publicly 2. **VPN (Tailscale)**: Gateway accessible only via VPN mesh 3. **Docker Isolation**: DOCKER-USER iptables chain prevents external port exposure 4. **Systemd Hardening**: NoNewPrivileges, PrivateTmp, unprivileged user ### Verification Test external attack surface: ```bash theme={null} 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](/multi-agent-sandbox-tools) for sandbox configuration. ## Manual Installation If you prefer manual control over the automation: ```bash theme={null} # 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](/install/updating) for the standard update flow. To re-run the Ansible playbook (e.g., for configuration changes): ```bash theme={null} 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 ```bash theme={null} # 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 ```bash theme={null} # 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: ```bash theme={null} sudo -i -u openclaw openclaw channels login ``` ## Advanced Configuration For detailed security architecture and troubleshooting: * [Security Architecture](https://github.com/openclaw/openclaw-ansible/blob/main/docs/security.md) * [Technical Details](https://github.com/openclaw/openclaw-ansible/blob/main/docs/architecture.md) * [Troubleshooting Guide](https://github.com/openclaw/openclaw-ansible/blob/main/docs/troubleshooting.md) ## Related * [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) — full deployment guide * [Docker](/install/docker) — containerized gateway setup * [Sandboxing](/gateway/sandboxing) — agent sandbox configuration * [Multi-Agent Sandbox & Tools](/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 …`). * `pnpm` is the default for builds and remains fully supported (and used by some docs tooling). * Bun cannot use `pnpm-lock.yaml` and will ignore it. ## Install Default: ```sh theme={null} bun install ``` Note: `bun.lock`/`bun.lockb` are gitignored, so there’s no repo churn either way. If you want *no lockfile writes*: ```sh theme={null} bun install --no-save ``` ## Build / Test (Bun) ```sh theme={null} 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/baileys` `preinstall`: checks Node major >= 20 (we run Node 22+). * `protobufjs` `postinstall`: emits warnings about incompatible version schemes (no build artifacts). If you hit a real runtime issue that requires these scripts, trust them explicitly: ```sh theme={null} 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: ```bash theme={null} openclaw update --channel stable openclaw update --channel beta openclaw update --channel dev ``` * `stable`/`beta` check out the latest matching tag (often the same tag). * `dev` switches to `main` and rebases on the upstream. npm/pnpm global install: ```bash theme={null} 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: * `dev` ensures a git checkout (default `~/openclaw`, override with `OPENCLAW_GIT_DIR`), updates it, and installs the global CLI from that checkout. * `stable`/`beta` installs 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: * `dev` prefers bundled plugins from the git checkout. * `stable` and `beta` restore npm-installed plugin packages. ## Tagging best practices * Tag releases you want git checkouts to land on (`vYYYY.M.D` or `vYYYY.M.D-`). * Keep tags immutable: never move or reuse a tag. * npm dist-tags remain the source of truth for npm installs: * `latest` → stable * `beta` → candidate build * `dev` → 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](/gateway/sandboxing). This guide covers: * Containerized Gateway (full OpenClaw in Docker) * Per-session Agent Sandbox (host gateway + Docker-isolated agent tools) Sandboxing details: [Sandboxing](/gateway/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: ```bash theme={null} ./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 build * `OPENCLAW_EXTRA_MOUNTS` — add extra host bind mounts * `OPENCLAW_HOME_VOLUME` — persist `/home/node` in 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)](/platforms/hetzner). ### Manual flow (compose) ```bash theme={null} 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: ```bash theme={null} docker compose -f docker-compose.yml -f docker-compose.extra.yml ``` ### Control UI token + pairing (Docker) If you see “unauthorized” or “disconnected (1008): pairing required”, fetch a fresh dashboard link and approve the browser device: ```bash theme={null} 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 ``` More detail: [Dashboard](/web/dashboard), [Devices](/cli/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: ```bash theme={null} 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`, rerun `docker-setup.sh` to regenerate the extra compose file. * `docker-compose.extra.yml` is 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: ```bash theme={null} export OPENCLAW_HOME_VOLUME="openclaw_home" ./docker-setup.sh ``` You can combine this with extra mounts: ```bash theme={null} 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`, rerun `docker-setup.sh` to regenerate the extra compose file. * The named volume persists until removed with `docker volume rm `. ### 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: ```bash theme={null} 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`, rerun `docker-setup.sh` to 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: 1. **Persist `/home/node`** so browser downloads and tool caches survive: ```bash theme={null} export OPENCLAW_HOME_VOLUME="openclaw_home" ./docker-setup.sh ``` 2. **Bake system deps into the image** (repeatable + persistent): ```bash theme={null} export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq" ./docker-setup.sh ``` 3. **Install Playwright browsers without `npx`** (avoids npm override conflicts): ```bash theme={null} 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. 4. **Persist Playwright browser downloads**: * Set `PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright` in `docker-compose.yml`. * Ensure `/home/node` persists via `OPENCLAW_HOME_VOLUME`, or mount `/home/node/.cache/ms-playwright` via `OPENCLAW_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): ```bash theme={null} 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: ```dockerfile theme={null} 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): ```bash theme={null} docker compose run --rm openclaw-cli channels login ``` Telegram (bot token): ```bash theme={null} docker compose run --rm openclaw-cli channels add --channel telegram --token "" ``` Discord (bot token): ```bash theme={null} docker compose run --rm openclaw-cli channels add --channel discord --token "" ``` Docs: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](/channels/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 ```bash theme={null} docker compose exec openclaw-gateway node dist/index.js health --token "$OPENCLAW_GATEWAY_TOKEN" ``` ### E2E smoke test (Docker) ```bash theme={null} scripts/e2e/onboard-docker.sh ``` ### QR import smoke test (Docker) ```bash theme={null} pnpm test:docker:qr ``` ### Notes * Gateway bind defaults to `lan` for container use. * Dockerfile CMD uses `--allow-unconfigured`; mounted config with `gateway.mode` not `local` will still start. Override CMD to enforce the guard. * The gateway container is the source of truth for sessions (`~/.openclaw/agents//sessions/`). ## Agent Sandbox (host gateway + Docker tools) Deep dive: [Sandboxing](/gateway/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 (with `workspaceAccess: "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](/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 `/workspace` and mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`) * `"rw"` mounts the agent workspace read/write at `/workspace` * Auto-prune: idle > 24h OR age > 7d * Network: `none` by 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.network` is `"none"` (no egress). * `readOnlyRoot: true` blocks package installs. * `user` must be root for `apt-get` (omit `user` or set `user: "0:0"`). OpenClaw auto-recreates containers when `setupCommand` (or docker config) changes unless the container was **recently used** (within \~5 minutes). Hot containers log a warning with the exact `openclaw sandbox recreate ...` command. ```json5 theme={null} { 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 ```bash theme={null} 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: ```bash theme={null} scripts/sandbox-common-setup.sh ``` This builds `openclaw-sandbox-common:bookworm-slim`. To use it: ```json5 theme={null} { 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: ```bash theme={null} 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: ```json5 theme={null} { agents: { defaults: { sandbox: { browser: { enabled: true }, }, }, }, } ``` Custom browser image: ```json5 theme={null} { agents: { defaults: { sandbox: { browser: { image: "my-openclaw-browser" } }, }, }, } ``` When enabled, the agent receives: * a sandbox browser control URL (for the `browser` tool) * 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: ```bash theme={null} docker build -t my-openclaw-sbx -f Dockerfile.sandbox . ``` ```json5 theme={null} { agents: { defaults: { sandbox: { docker: { image: "my-openclaw-sbx" } }, }, }, } ``` ### Tool policy (allow/deny) * `deny` wins over `allow`. * If `allow` is empty: all tools (except deny) are available. * If `allow` is non-empty: only tools in `allow` are 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 `browser` in sandbox **breaks isolation** (browser runs on host). ## Troubleshooting * Image missing: build with [`scripts/sandbox-setup.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/sandbox-setup.sh) or set `agents.defaults.sandbox.docker.image`. * Container not running: it will auto-create per session on demand. * Permission errors in sandbox: set `docker.user` to 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/profile` and may reset PATH. Set `docker.env.PATH` to 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) ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash ``` Windows (PowerShell): ```powershell theme={null} iwr -useb https://openclaw.ai/install.ps1 | iex ``` Next step (if you skipped onboarding): ```bash theme={null} openclaw onboard --install-daemon ``` ## System requirements * **Node >=22** * macOS, Linux, or Windows via WSL2 * `pnpm` only if you build from source ## Choose your install path ### 1) Installer script (recommended) Installs `openclaw` globally via npm and runs onboarding. ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash ``` Installer flags: ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help ``` Details: [Installer internals](/install/installer). Non-interactive (skip onboarding): ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard ``` ### 2) Global install (manual) If you already have Node: ```bash theme={null} npm install -g openclaw@latest ``` If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries: ```bash theme={null} 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: ```bash theme={null} 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: ```bash theme={null} openclaw onboard --install-daemon ``` ### 3) From source (contributors/dev) ```bash theme={null} 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 * Docker: [Docker](/install/docker) * Nix: [Nix](/install/nix) * Ansible: [Ansible](/install/ansible) * Bun (CLI only): [Bun](/install/bun) ## 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@latest` * `git`: clone/build from GitHub and run from a source checkout ### CLI flags ```bash theme={null} # 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 ` (default: `~/openclaw`) * `--no-git-update` (skip `git pull` when 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|npm` * `OPENCLAW_GIT_DIR=...` * `OPENCLAW_GIT_UPDATE=0|1` * `OPENCLAW_NO_PROMPT=1` * `OPENCLAW_DRY_RUN=1` * `OPENCLAW_NO_ONBOARD=1` * `SHARP_IGNORE_GLOBAL_LIBVIPS=0|1` (default: `1`; avoids `sharp` building against system libvips) ## Troubleshooting: `openclaw` not found (PATH) Quick diagnosis: ```bash theme={null} 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`): ```bash theme={null} # 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 * Updates: [Updating](/install/updating) * Migrate to a new machine: [Migrating](/install/migrating) * Uninstall: [Uninstall](/install/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: ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help ``` Windows (PowerShell) help: ```powershell theme={null} & ([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#nodejs--npm-path-sanity). ## 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@latest` * `git`: clone/build a source checkout and install a wrapper script * On Linux: avoid global npm permission errors by switching npm's prefix to `~/.npm-global` when needed. * If upgrading an existing install: runs `openclaw doctor --non-interactive` (best effort). * For git installs: runs `openclaw doctor --non-interactive` after install/update (best effort). * Mitigates `sharp` native install gotchas by defaulting `SHARP_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: ```bash theme={null} 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 to `PATH` in `~/.bashrc` / `~/.zshrc` when 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: ```bash theme={null} 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@latest` * `git`: clone/build a source checkout and install a wrapper script * Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort). Examples: ```powershell theme={null} iwr -useb https://openclaw.ai/install.ps1 | iex ``` ```powershell theme={null} iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git ``` ```powershell theme={null} iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git -GitDir "C:\\openclaw" ``` Environment variables: * `OPENCLAW_INSTALL_METHOD=git|npm` * `OPENCLAW_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 run `npm config get prefix` and add `\\bin` to 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](https://github.com/openclaw/nix-openclaw)** — a batteries-included Home Manager module. ## Quick Start Paste this to your AI agent (Claude, Cursor, etc.): ```text theme={null} 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](https://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: ```bash theme={null} OPENCLAW_NIX_MODE=1 ``` On macOS, the GUI app does not automatically inherit shell env vars. You can also enable Nix mode via defaults: ```bash theme={null} 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`](https://github.com/openclaw/openclaw/blob/main/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](https://github.com/openclaw/nix-openclaw) — full setup guide * [Wizard](/start/wizard) — non-Nix CLI setup * [Docker](/install/docker) — containerized setup # Uninstall Source: https://docs.openclaw.ai/install/uninstall # Uninstall Two paths: * **Easy path** if `openclaw` is 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: ```bash theme={null} openclaw uninstall ``` Non-interactive (automation / npx): ```bash theme={null} openclaw uninstall --all --yes --non-interactive npx -y openclaw uninstall --all --yes --non-interactive ``` Manual steps (same result): 1. Stop the gateway service: ```bash theme={null} openclaw gateway stop ``` 2. Uninstall the gateway service (launchd/systemd/schtasks): ```bash theme={null} openclaw gateway uninstall ``` 3. Delete state + config: ```bash theme={null} 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. 4. Delete your workspace (optional, removes agent files): ```bash theme={null} rm -rf ~/.openclaw/workspace ``` 5. Remove the CLI install (pick the one you used): ```bash theme={null} npm rm -g openclaw pnpm remove -g openclaw bun remove -g openclaw ``` 6. If you installed the macOS app: ```bash theme={null} rm -rf /Applications/OpenClaw.app ``` Notes: * If you used profiles (`--profile` / `OPENCLAW_PROFILE`), repeat step 3 for each state dir (defaults are `~/.openclaw-`). * 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.`; legacy `com.openclaw.*` may still exist): ```bash theme={null} 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.`. Remove any legacy `com.openclaw.*` plists if present. ### Linux (systemd user unit) Default unit name is `openclaw-gateway.service` (or `openclaw-gateway-.service`): ```bash theme={null} 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 ()`). The task script lives under your state dir. ```powershell theme={null} 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-\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 ...`): 1. Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal). 2. Delete the repo directory. 3. 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. ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash ``` Notes: * Add `--no-onboard` if you don’t want the onboarding wizard to run again. * For **source installs**, use: ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard ``` The installer will `git pull --rebase` **only** if the repo is clean. * For **global installs**, the script uses `npm install -g openclaw@latest` under the hood. * Legacy note: `clawdbot` remains 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` ## Update (global install) Global install (pick one): ```bash theme={null} npm i -g openclaw@latest ``` ```bash theme={null} pnpm add -g openclaw@latest ``` We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs). To switch update channels (git + npm installs): ```bash theme={null} openclaw update --channel beta openclaw update --channel dev openclaw update --channel stable ``` Use `--tag ` for a one-off install tag/version. See [Development channels](/install/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: ```bash theme={null} openclaw doctor openclaw gateway restart openclaw health ``` Notes: * If your Gateway runs as a service, `openclaw gateway restart` is 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: ```bash theme={null} 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-restart` to 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: 1. Runs the same source-update flow as `openclaw update` (git checkout only). 2. Writes a restart sentinel with a structured report (stdout/stderr tail). 3. 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: ```bash theme={null} openclaw update ``` Manual (equivalent-ish): ```bash theme={null} git pull pnpm install pnpm build pnpm ui:build # auto-installs UI deps on first run openclaw doctor openclaw health ``` Notes: * `pnpm build` matters when you run the packaged `openclaw` binary ([`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs)) or use Node to run `dist/`. * 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 doctor` so 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](/gateway/doctor) ## Start / stop / restart the Gateway CLI (works regardless of OS): ```bash theme={null} 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` (use `bot.molt.`; legacy `com.openclaw.*` still works) * Linux systemd user service: `systemctl --user restart openclaw-gateway[-].service` * Windows (WSL2): `systemctl --user restart openclaw-gateway[-].service` * `launchctl`/`systemctl` only work if the service is installed; otherwise run `openclaw gateway install`. Runbook + exact service labels: [Gateway runbook](/gateway) ## Rollback / pinning (when something breaks) ### Pin (global install) Install a known-good version (replace `` with the last working one): ```bash theme={null} npm i -g openclaw@ ``` ```bash theme={null} pnpm add -g openclaw@ ``` Tip: to see the current published version, run `npm view openclaw version`. Then restart + re-run doctor: ```bash theme={null} openclaw doctor openclaw gateway restart ``` ### Pin (source) by date Pick a commit from a date (example: “state of main as of 2026-01-01”): ```bash theme={null} git fetch origin git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)" ``` Then reinstall deps + restart: ```bash theme={null} pnpm install pnpm build openclaw gateway restart ``` If you want to go back to latest later: ```bash theme={null} git checkout main git pull ``` ## If you’re stuck * Run `openclaw doctor` again and read the output carefully (it often tells you the fix). * Check: [Troubleshooting](/gateway/troubleshooting) * Ask in Discord: [https://discord.gg/clawd](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[].sandbox` overrides `agents.defaults.sandbox`) * **Tool restrictions** (`tools.allow` / `tools.deny`, plus `agents.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//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](/gateway/sandboxing). For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) and `openclaw sandbox explain`. *** ## Configuration Examples ### Example 1: Personal + Restricted Family Agent ```json theme={null} { "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:** * `main` agent: Runs on host, full tool access * `family` agent: Runs in Docker (one container per agent), only `read` tool *** ### Example 2: Work Agent with Shared Sandbox ```json theme={null} { "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 ```json theme={null} { "tools": { "profile": "coding" }, "agents": { "list": [ { "id": "support", "tools": { "profile": "messaging", "allow": ["slack"] } } ] } } ``` **Result:** * default agents get coding tools * `support` agent is messaging-only (+ Slack tool) *** ### Example 3: Different Sandbox Modes per Agent ```json theme={null} { "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}.*` overrides `agents.defaults.sandbox.{docker,browser,prune}.*` for that agent (ignored when sandbox scope resolves to `"shared"`). ### Tool Restrictions The filtering order is: 1. **Tool profile** (`tools.profile` or `agents.list[].tools.profile`) 2. **Provider tool profile** (`tools.byProvider[provider].profile` or `agents.list[].tools.byProvider[provider].profile`) 3. **Global tool policy** (`tools.allow` / `tools.deny`) 4. **Provider tool policy** (`tools.byProvider[provider].allow/deny`) 5. **Agent-specific tool policy** (`agents.list[].tools.allow/deny`) 6. **Agent provider policy** (`agents.list[].tools.byProvider[provider].allow/deny`) 7. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`) 8. **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`, `process` * `group:fs`: `read`, `write`, `edit`, `apply_patch` * `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` * `group:memory`: `memory_search`, `memory_get` * `group:ui`: `browser`, `canvas` * `group:automation`: `cron`, `gateway` * `group:messaging`: `message` * `group:nodes`: `nodes` * `group: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 `exec` for 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):** ```json theme={null} { "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):** ```json theme={null} { "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 ```json theme={null} { "tools": { "allow": ["read"], "deny": ["exec", "write", "edit", "apply_patch", "process"] } } ``` ### Safe Execution Agent (no file modifications) ```json theme={null} { "tools": { "allow": ["read", "exec", "process"], "deny": ["write", "edit", "apply_patch", "browser", "gateway"] } } ``` ### Communication-only Agent ```json theme={null} { "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: 1. **Check agent resolution:** ```exec theme={null} openclaw agents list --bindings ``` 2. **Verify sandbox containers:** ```exec theme={null} docker ps --filter "name=openclaw-sbx-" ``` 3. **Test tool restrictions:** * Send a message requiring restricted tools * Verify the agent cannot use denied tools 4. **Monitor logs:** ```exec theme={null} 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.mode` that 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 * [Multi-Agent Routing](/concepts/multi-agent) * [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox) * [Session Management](/concepts/session) # 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](/start/getting-started) + [Pairing](/gateway/pairing). * Gateway: [Runbook](/gateway) + [Configuration](/gateway/configuration). * Protocols: [Gateway protocol](/gateway/protocol) (nodes + control plane). ## System control System control (launchd/systemd) lives on the Gateway host. See [Gateway](/gateway). ## Connection Runbook Android node app ⇄ (mDNS/NSD + WebSocket) ⇄ **Gateway** Android connects directly to the Gateway WebSocket (default `ws://: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 ```bash theme={null} 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.json` on the gateway host. * Restart the Gateway / macOS menubar app. ### 2) Verify discovery (optional) From the gateway machine: ```bash theme={null} dns-sd -B _openclaw-gw._tcp local. ``` More debugging notes: [Bonjour](/gateway/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: 1. Set up a DNS-SD zone (example `openclaw.internal.`) on the gateway host and publish `_openclaw-gw._tcp` records. 2. Configure Tailscale split DNS for your chosen domain pointing at that DNS server. Details and example CoreDNS config: [Bonjour](/gateway/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: ```bash theme={null} openclaw nodes pending openclaw nodes approve ``` Pairing details: [Gateway pairing](/gateway/pairing). ### 5) Verify the node is connected * Via nodes status: ```bash theme={null} openclaw nodes status ``` * Via Gateway: ```bash theme={null} 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`). 1. Create `~/.openclaw/workspace/canvas/index.html` on the gateway host. 2. Navigate the node to it (LAN): ```bash theme={null} openclaw nodes invoke --node "" --command canvas.navigate --params '{"url":"http://.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://:18793/__openclaw__/canvas/`. This server injects a live-reload client into HTML and reloads on file changes. The A2UI host lives at `http://:18793/__openclaw__/a2ui/`. Canvas commands (foreground only): * `canvas.eval`, `canvas.snapshot`, `canvas.navigate` (use `{"url":""}` or `{"url":"/"}` to return to the default scaffold). `canvas.snapshot` returns `{ format, base64 }` (default `format="jpeg"`). * A2UI: `canvas.a2ui.push`, `canvas.a2ui.reset` (`canvas.a2ui.pushJSONL` legacy alias) Camera commands (foreground only; permission-gated): * `camera.snap` (jpg) * `camera.clip` (mp4) See [Camera node](/nodes/camera) 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 * macOS: [macOS](/platforms/macos) * iOS: [iOS](/platforms/ios) * Android: [Android](/platforms/android) * Windows: [Windows](/platforms/windows) * Linux: [Linux](/platforms/linux) ## VPS & hosting * VPS hub: [VPS hosting](/vps) * Fly.io: [Fly.io](/platforms/fly) * Hetzner (Docker): [Hetzner](/platforms/hetzner) * GCP (Compute Engine): [GCP](/platforms/gcp) * exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev) ## Common links * Install guide: [Getting Started](/start/getting-started) * Gateway runbook: [Gateway](/gateway) * Gateway configuration: [Configuration](/gateway/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.gateway` or `bot.molt.`; legacy `com.openclaw.*`) * Linux/WSL2: systemd user service (`openclaw-gateway[-].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.invoke` commands 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) 1. Start the Gateway: ```bash theme={null} openclaw gateway --port 18789 ``` 2. In the iOS app, open Settings and pick a discovered gateway (or enable Manual Host and enter host/port). 3. Approve the pairing request on the gateway host: ```bash theme={null} openclaw nodes pending openclaw nodes approve ``` 4. Verify connection: ```bash theme={null} 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](/gateway/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: ```bash theme={null} openclaw nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://: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.navigate` and `{"url":""}`. ### Canvas eval / snapshot ```bash theme={null} 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\"; })()"}' ``` ```bash theme={null} 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; check `canvasHost` in [Gateway configuration](/gateway/configuration). * Pairing prompt never appears: run `openclaw nodes pending` and approve manually. * Reconnect fails after reinstall: the Keychain pairing token was cleared; re-pair the node. ## Related docs * [Pairing](/gateway/pairing) * [Discovery](/gateway/discovery) * [Bonjour](/gateway/bonjour) # 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) 1. Install Node 22+ 2. `npm i -g openclaw@latest` 3. `openclaw onboard --install-daemon` 4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 @` 5. Open `http://127.0.0.1:18789/` and paste your token Step-by-step VPS guide: [exe.dev](/platforms/exe-dev) ## Install * [Getting Started](/start/getting-started) * [Install & updates](/install/updating) * Optional flows: [Bun (experimental)](/install/bun), [Nix](/install/nix), [Docker](/install/docker) ## Gateway * [Gateway runbook](/gateway) * [Configuration](/gateway/configuration) ## 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](/gateway). Minimal setup: Create `~/.config/systemd/user/openclaw-gateway[-].service`: ``` [Unit] Description=OpenClaw Gateway (profile: , v) 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[-].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.` when using `--profile`/`OPENCLAW_PROFILE`; legacy `com.openclaw.*` still unloads). ```bash theme={null} launchctl kickstart -k gui/$UID/bot.molt.gateway launchctl bootout gui/$UID/bot.molt.gateway ``` Replace the label with `bot.molt.` 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.run` executes 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: ```json theme={null} { "version": 1, "defaults": { "security": "deny", "ask": "on-miss" }, "agents": { "main": { "security": "allowlist", "ask": "on-miss", "allowlist": [{ "pattern": "/opt/homebrew/bin/rg" }] } } } ``` Notes: * `allowlist` entries are glob patterns for resolved binary paths. * Choosing “Always Allow” in the prompt adds that command to the allowlist. * `system.run` environment overrides are filtered (drops `PATH`, `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. ```bash theme={null} 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) 1. Install and launch **OpenClaw\.app**. 2. Complete the permissions checklist (TCC prompts). 3. Ensure **Local** mode is active and the Gateway is running. 4. Install the CLI if you want terminal access. ## Build & dev workflow (native) * `cd apps/macos && swift build` * `swift 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. ```bash theme={null} cd apps/macos swift run openclaw-mac connect --json swift run openclaw-mac discover --timeout 3000 --json ``` Connect options: * `--url `: override config * `--mode `: resolve from config (default: config or local) * `--probe`: force a fresh health probe * `--timeout `: request timeout (default: `15000`) * `--json`: structured output for diffing Discovery options: * `--include-local`: include gateways that would be filtered as “local” * `--timeout `: 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 :127.0.0.1:` 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](/platforms/mac/remote)). For setup steps, see [macOS remote access](/platforms/mac/remote). For protocol details, see [Gateway protocol](/gateway/protocol). ## Related docs * [Gateway runbook](/gateway) * [Gateway (macOS)](/platforms/mac/bundled-gateway) * [macOS permissions](/platforms/mac/permissions) * [Canvas](/platforms/mac/canvas) # 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](/start/getting-started) (use inside WSL) * [Install & updates](/install/updating) * Official WSL2 guide (Microsoft): [https://learn.microsoft.com/windows/wsl/install](https://learn.microsoft.com/windows/wsl/install) ## Gateway * [Gateway runbook](/gateway) * [Configuration](/gateway/configuration) ## 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**): ```powershell theme={null} $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): ```powershell theme={null} New-NetFirewallRule -DisplayName "WSL SSH $ListenPort" -Direction Inbound ` -Protocol TCP -LocalPort $ListenPort -Action Allow ``` Refresh the portproxy after WSL restarts: ```powershell theme={null} 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`); use `openclaw status --all` to confirm. * Use `listenaddress=0.0.0.0` for LAN access; `127.0.0.1` keeps 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): ```powershell theme={null} 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: ```bash theme={null} sudo tee /etc/wsl.conf >/dev/null <<'EOF' [boot] systemd=true EOF ``` Then from PowerShell: ```powershell theme={null} wsl --shutdown ``` Re-open Ubuntu, then verify: ```bash theme={null} systemctl --user status ``` ### 3) Install OpenClaw (inside WSL) Follow the Linux Getting Started flow inside WSL: ```bash theme={null} 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](/start/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: 1. See what’s already loaded: ```bash theme={null} openclaw plugins list ``` 2. Install an official plugin (example: Voice Call): ```bash theme={null} openclaw plugins install @openclaw/voice-call ``` 3. Restart the Gateway, then configure under `plugins.entries..config`. See [Voice Call](/plugins/voice-call) for a concrete example plugin. ## Available plugins (official) * Microsoft Teams is plugin-only as of 2026.1.15; install `@openclaw/msteams` if 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](/plugins/voice-call) — `@openclaw/voice-call` * [Zalo Personal](/plugins/zalouser) — `@openclaw/zalouser` * [Matrix](/channels/matrix) — `@openclaw/matrix` * [Nostr](/channels/nostr) — `@openclaw/nostr` * [Zalo](/channels/zalo) — `@openclaw/zalo` * [Microsoft Teams](/channels/msteams) — `@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-copilot` device 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/manifest). Plugins can register: * Gateway RPC methods * Gateway HTTP handlers * Agent tools * CLI commands * Background services * Optional config validation * **Skills** (by listing `skills` directories 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](/plugins/agent-tools). ## Runtime helpers Plugins can access selected core helpers via `api.runtime`. For telephony TTS: ```ts theme={null} const result = await api.runtime.tts.textToSpeechTelephony({ text: "Hello from OpenClaw", cfg: api.config, }); ``` Notes: * Uses core `messages.tts` configuration (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: 1. Config paths * `plugins.load.paths` (file or directory) 2. Workspace extensions * `/.openclaw/extensions/*.ts` * `/.openclaw/extensions/*/index.ts` 3. Global extensions * `~/.openclaw/extensions/*.ts` * `~/.openclaw/extensions/*/index.ts` 4. Bundled extensions (shipped with OpenClaw, **disabled by default**) * `/extensions/*` Bundled plugins must be enabled explicitly via `plugins.entries..enabled` or `openclaw plugins enable `. 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`: ```json theme={null} { "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/`. 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: ```json theme={null} { "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.json` `name` * 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 ```json5 theme={null} { 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/dirs * `entries.`: per‑plugin toggles + config Config changes **require a gateway restart**. Validation rules (strict): * Unknown plugin ids in `entries`, `allow`, `deny`, or `slots` are **errors**. * Unknown `channels.` 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: ```json5 theme={null} { 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.` / `.enabled` / `.config` * Merges optional plugin-provided config field hints under: `plugins.entries..config.` 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: ```json theme={null} { "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 ```bash theme={null} openclaw plugins list openclaw plugins info openclaw plugins install # copy a local file/dir into ~/.openclaw/extensions/ 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 openclaw plugins update --all openclaw plugins enable openclaw plugins disable 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 list` with `plugin:`. * 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 [--method ]` Example: ```ts theme={null} 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: * `run` receives a `ProviderAuthContext` with `prompter`, `runtime`, `openUrl`, and `oauth.createVpsAwareHandlers` helpers. * Return `configPatch` when you need to add default models or provider config. * Return `defaultModel` so `--set-default` can 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.` and is validated by your channel plugin code. ```ts theme={null} 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.` (not `plugins.entries`). * `meta.label` is used for labels in CLI/UI lists. * `meta.aliases` adds alternate ids for normalization and CLI inputs. * `meta.preferOver` lists channel ids to skip auto-enable when both are configured. * `meta.detailLabel` and `meta.systemImage` let 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/*`. 1. Pick an id + config shape * All channel config lives under `channels.`. * Prefer `channels..accounts.` for multi‑account setups. 2. Define the channel metadata * `meta.label`, `meta.selectionLabel`, `meta.docsPath`, `meta.blurb` control CLI/UI lists. * `meta.docsPath` should point at a docs page like `/channels/`. * `meta.preferOver` lets a plugin replace another channel (auto-enable prefers it). * `meta.detailLabel` and `meta.systemImage` are used by UIs for detail text/icons. 3. Implement the required adapters * `config.listAccountIds` + `config.resolveAccount` * `capabilities` (chat types, media, threads, etc.) * `outbound.deliveryMode` + `outbound.sendText` (for basic send) 4. Add optional adapters as needed * `setup` (wizard), `security` (DM policy), `status` (health/diagnostics) * `gateway` (start/stop/login), `mentions`, `threading`, `streaming` * `actions` (message actions), `commands` (native command behavior) 5. Register the channel in your plugin * `api.registerChannel({ plugin })` Minimal config example: ```json5 theme={null} { channels: { acmechat: { accounts: { default: { token: "ACME_TOKEN", enabled: true }, }, }, }, } ``` Minimal channel plugin (outbound‑only): ```ts theme={null} 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.` in your config. ### Agent tools See the dedicated guide: [Plugin agent tools](/plugins/agent-tools). ### Register a gateway RPC method ```ts theme={null} export default function (api) { api.registerGatewayMethod("myplugin.status", ({ respond }) => { respond(true, { ok: true }); }); } ``` ### Register CLI commands ```ts theme={null} 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. ```ts theme={null} 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 sent * `isAuthorizedSender`: Whether the sender is an authorized user * `args`: Arguments passed after the command (if `acceptsArgs: true`) * `commandBody`: The full command text * `config`: The current OpenClaw config Command options: * `name`: Command name (without the leading `/`) * `description`: Help text shown in command lists * `acceptsArgs`: 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 handlers * `requireAuth`: Whether to require authorized sender (default: true) * `handler`: Function that returns `{ text: string }` (can be async) Example with authorization and arguments: ```ts theme={null} 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 (`/MyStatus` matches `/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 ```ts theme={null} 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//SKILL.md`). Enable it with `plugins.entries..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.json` must include `openclaw.extensions` with one or more entry files. * Entry files can be `.js` or `.ts` (jiti loads TS at runtime). * `openclaw plugins install ` uses `npm pack`, extracts into `~/.openclaw/extensions//`, 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` (optional `statusCallbackUrl`, `twimlUrl`) * Config (dev): `provider: "log"` (no network) See [Voice Call](/plugins/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.allow` allowlists. * 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.extensions` points 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 the `voice_call` tool ## 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) ```bash theme={null} openclaw plugins install @openclaw/voice-call ``` Restart the Gateway afterwards. ### Option B: install from a local folder (dev, no copying) ```bash theme={null} 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`: ```json5 theme={null} { 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. * `mock` is a local dev provider (no network calls). * `skipSignatureVerification` is for local testing only. * If you use ngrok free tier, set `publicUrl` to the exact ngrok URL; signature verification is always enforced. * `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only. * Ngrok free tier URLs can change or add interstitial behavior; if `publicUrl` drifts, 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: ```json5 theme={null} { 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`. ```json5 theme={null} { 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): ```json5 theme={null} { messages: { tts: { provider: "openai", openai: { voice: "alloy" }, }, }, } ``` Override to ElevenLabs just for calls (keep core default elsewhere): ```json5 theme={null} { 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): ```json5 theme={null} { 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: ```json5 theme={null} { inboundPolicy: "allowlist", allowFrom: ["+15550001234"], inboundGreeting: "Hello! How can I help?", } ``` Auto-responses use the agent system. Tune with: * `responseModel` * `responseSystemPrompt` * `responseTimeoutMs` ## CLI ```bash theme={null} openclaw voicecall call --to "+15555550123" --message "Hello from OpenClaw" openclaw voicecall continue --call-id --message "Any questions?" openclaw voicecall speak --call-id --message "One moment" openclaw voicecall end --call-id openclaw voicecall status --call-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 ```bash theme={null} openclaw plugins install @openclaw/zalouser ``` Restart the Gateway afterwards. ### Option B: install from a local folder (dev) ```bash theme={null} 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`: ```bash theme={null} zca --version ``` ## Config Channel config lives under `channels.zalouser` (not `plugins.entries.*`): ```json5 theme={null} { channels: { zalouser: { enabled: true, dmPolicy: "pairing", }, }, } ``` ## CLI ```bash theme={null} openclaw channels login --channel zalouser openclaw channels logout --channel zalouser openclaw channels status --probe openclaw message send --channel zalouser --target --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)](/start/hubs) * [Help](/help) * [Configuration](/gateway/configuration) * [Configuration examples](/gateway/configuration-examples) * [Slash commands](/tools/slash-commands) * [Multi-agent routing](/concepts/multi-agent) * [Updating and rollback](/install/updating) * [Pairing (DM and nodes)](/start/pairing) * [Nix mode](/install/nix) * [OpenClaw assistant setup](/start/openclaw) * [Skills](/tools/skills) * [Skills config](/tools/skills-config) * [Workspace templates](/reference/templates/AGENTS) * [RPC adapters](/reference/rpc) * [Gateway runbook](/gateway) * [Nodes (iOS and Android)](/nodes) * [Web surfaces (Control UI)](/web) * [Discovery and transports](/gateway/discovery) * [Remote access](/gateway/remote) ## Providers and UX * [WebChat](/web/webchat) * [Control UI (browser)](/web/control-ui) * [Telegram](/channels/telegram) * [Discord](/channels/discord) * [Mattermost (plugin)](/channels/mattermost) * [BlueBubbles (iMessage)](/channels/bluebubbles) * [iMessage (legacy)](/channels/imessage) * [Groups](/concepts/groups) * [WhatsApp group messages](/concepts/group-messages) * [Media images](/nodes/images) * [Media audio](/nodes/audio) ## Companion apps * [macOS app](/platforms/macos) * [iOS app](/platforms/ios) * [Android app](/platforms/android) * [Windows (WSL2)](/platforms/windows) * [Linux app](/platforms/linux) ## Operations and safety * [Sessions](/concepts/session) * [Cron jobs](/automation/cron-jobs) * [Webhooks](/automation/webhook) * [Gmail hooks (Pub/Sub)](/automation/gmail-pubsub) * [Security](/gateway/security) * [Troubleshooting](/gateway/troubleshooting) # 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](/web/dashboard) and [Control UI](/web/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](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/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: ```json theme={null} { "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` (stores `tools.web.search.apiKey`). See [Web tools](/tools/web). 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)](/platforms/windows). ## 1) Install the CLI (recommended) ```bash theme={null} curl -fsSL https://openclaw.ai/install.sh | bash ``` Installer options (install method, non-interactive, from GitHub): [Install](/install). Windows (PowerShell): ```powershell theme={null} iwr -useb https://openclaw.ai/install.ps1 | iex ``` Alternative (global install): ```bash theme={null} npm install -g openclaw@latest ``` ```bash theme={null} pnpm add -g openclaw@latest ``` ## 2) Run the onboarding wizard (and install the service) ```bash theme={null} 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-token` is 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](/start/wizard) ### Auth: where it lives (important) * **Recommended Anthropic path:** set an API key (wizard can store it for service use). `claude setup-token` is 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//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: ```bash theme={null} openclaw gateway status ``` Manual run (foreground): ```bash theme={null} 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) ```bash theme={null} openclaw status openclaw health openclaw security audit --deep ``` ## 4) Pair + connect your first chat surface ### WhatsApp (QR login) ```bash theme={null} openclaw channels login ``` Scan via WhatsApp → Settings → Linked Devices. WhatsApp doc: [WhatsApp](/channels/whatsapp) ### Telegram / Discord / others The wizard can write tokens/config for you. If you prefer manual config, start with: * Telegram: [Telegram](/channels/telegram) * Discord: [Discord](/channels/discord) * Mattermost (plugin): [Mattermost](/channels/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: ```bash theme={null} openclaw pairing list whatsapp openclaw pairing approve whatsapp ``` Pairing doc: [Pairing](/start/pairing) ## From source (development) If you’re hacking on OpenClaw itself, run from source: ```bash theme={null} 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): ```bash theme={null} node openclaw.mjs gateway --port 18789 --verbose ``` ## 7) Verify end-to-end In a new terminal, send a test message: ```bash theme={null} 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](/platforms/macos) * iOS/Android nodes (Canvas/camera/voice): [Nodes](/nodes) * Remote access (SSH tunnel / Tailscale Serve): [Remote access](/gateway/remote) and [Tailscale](/gateway/tailscale) * Always-on / VPN setups: [Remote access](/gateway/remote), [exe.dev](/platforms/exe-dev), [Hetzner](/platforms/hetzner), [macOS remote](/platforms/mac/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](/start/getting-started) * [Quick start](/start/quickstart) * [Onboarding](/start/onboarding) * [Wizard](/start/wizard) * [Setup](/start/setup) * [Dashboard (local Gateway)](http://127.0.0.1:18789/) * [Help](/help) * [Docs directory](/start/docs-directory) * [Configuration](/gateway/configuration) * [Configuration examples](/gateway/configuration-examples) * [OpenClaw assistant](/start/openclaw) * [Showcase](/start/showcase) * [Lore](/start/lore) ## Installation + updates * [Docker](/install/docker) * [Nix](/install/nix) * [Updating / rollback](/install/updating) * [Bun workflow (experimental)](/install/bun) ## Core concepts * [Architecture](/concepts/architecture) * [Features](/concepts/features) * [Network hub](/network) * [Agent runtime](/concepts/agent) * [Agent workspace](/concepts/agent-workspace) * [Memory](/concepts/memory) * [Agent loop](/concepts/agent-loop) * [Streaming + chunking](/concepts/streaming) * [Multi-agent routing](/concepts/multi-agent) * [Compaction](/concepts/compaction) * [Sessions](/concepts/session) * [Sessions (alias)](/concepts/sessions) * [Session pruning](/concepts/session-pruning) * [Session tools](/concepts/session-tool) * [Queue](/concepts/queue) * [Slash commands](/tools/slash-commands) * [RPC adapters](/reference/rpc) * [TypeBox schemas](/concepts/typebox) * [Timezone handling](/concepts/timezone) * [Presence](/concepts/presence) * [Discovery + transports](/gateway/discovery) * [Bonjour](/gateway/bonjour) * [Channel routing](/concepts/channel-routing) * [Groups](/concepts/groups) * [Group messages](/concepts/group-messages) * [Model failover](/concepts/model-failover) * [OAuth](/concepts/oauth) ## Providers + ingress * [Chat channels hub](/channels) * [Model providers hub](/providers/models) * [WhatsApp](/channels/whatsapp) * [Telegram](/channels/telegram) * [Telegram (grammY notes)](/channels/grammy) * [Slack](/channels/slack) * [Discord](/channels/discord) * [Mattermost](/channels/mattermost) (plugin) * [Signal](/channels/signal) * [BlueBubbles (iMessage)](/channels/bluebubbles) * [iMessage (legacy)](/channels/imessage) * [Location parsing](/channels/location) * [WebChat](/web/webchat) * [Webhooks](/automation/webhook) * [Gmail Pub/Sub](/automation/gmail-pubsub) ## Gateway + operations * [Gateway runbook](/gateway) * [Network model](/gateway/network-model) * [Gateway pairing](/gateway/pairing) * [Gateway lock](/gateway/gateway-lock) * [Background process](/gateway/background-process) * [Health](/gateway/health) * [Heartbeat](/gateway/heartbeat) * [Doctor](/gateway/doctor) * [Logging](/gateway/logging) * [Sandboxing](/gateway/sandboxing) * [Dashboard](/web/dashboard) * [Control UI](/web/control-ui) * [Remote access](/gateway/remote) * [Remote gateway README](/gateway/remote-gateway-readme) * [Tailscale](/gateway/tailscale) * [Security](/gateway/security) * [Troubleshooting](/gateway/troubleshooting) ## Tools + automation * [Tools surface](/tools) * [OpenProse](/prose) * [CLI reference](/cli) * [Exec tool](/tools/exec) * [Elevated mode](/tools/elevated) * [Cron jobs](/automation/cron-jobs) * [Cron vs Heartbeat](/automation/cron-vs-heartbeat) * [Thinking + verbose](/tools/thinking) * [Models](/concepts/models) * [Sub-agents](/tools/subagents) * [Agent send CLI](/tools/agent-send) * [Terminal UI](/tui) * [Browser control](/tools/browser) * [Browser (Linux troubleshooting)](/tools/browser-linux-troubleshooting) * [Polls](/automation/poll) ## Nodes, media, voice * [Nodes overview](/nodes) * [Camera](/nodes/camera) * [Images](/nodes/images) * [Audio](/nodes/audio) * [Location command](/nodes/location-command) * [Voice wake](/nodes/voicewake) * [Talk mode](/nodes/talk) ## Platforms * [Platforms overview](/platforms) * [macOS](/platforms/macos) * [iOS](/platforms/ios) * [Android](/platforms/android) * [Windows (WSL2)](/platforms/windows) * [Linux](/platforms/linux) * [Web surfaces](/web) ## macOS companion app (advanced) * [macOS dev setup](/platforms/mac/dev-setup) * [macOS menu bar](/platforms/mac/menu-bar) * [macOS voice wake](/platforms/mac/voicewake) * [macOS voice overlay](/platforms/mac/voice-overlay) * [macOS WebChat](/platforms/mac/webchat) * [macOS Canvas](/platforms/mac/canvas) * [macOS child process](/platforms/mac/child-process) * [macOS health](/platforms/mac/health) * [macOS icon](/platforms/mac/icon) * [macOS logging](/platforms/mac/logging) * [macOS permissions](/platforms/mac/permissions) * [macOS remote](/platforms/mac/remote) * [macOS signing](/platforms/mac/signing) * [macOS release](/platforms/mac/release) * [macOS gateway (launchd)](/platforms/mac/bundled-gateway) * [macOS XPC](/platforms/mac/xpc) * [macOS skills](/platforms/mac/skills) * [macOS Peekaboo](/platforms/mac/peekaboo) ## Workspace + templates * [Skills](/tools/skills) * [ClawHub](/tools/clawhub) * [Skills config](/tools/skills-config) * [Default AGENTS](/reference/AGENTS.default) * [Templates: AGENTS](/reference/templates/AGENTS) * [Templates: BOOTSTRAP](/reference/templates/BOOTSTRAP) * [Templates: HEARTBEAT](/reference/templates/HEARTBEAT) * [Templates: IDENTITY](/reference/templates/IDENTITY) * [Templates: SOUL](/reference/templates/SOUL) * [Templates: TOOLS](/reference/templates/TOOLS) * [Templates: USER](/reference/templates/USER) ## Experiments (exploratory) * [Onboarding config protocol](/experiments/onboarding-config-protocol) * [Cron hardening notes](/experiments/plans/cron-add-hardening) * [Group policy hardening notes](/experiments/plans/group-policy-hardening) * [Research: memory](/experiments/research/memory) * [Model config exploration](/experiments/proposals/model-config) ## Project * [Credits](/reference/credits) ## Testing + release * [Testing](/reference/test) * [Release checklist](/reference/RELEASING) * [Device models](/reference/device-models) # 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 `@openclaw` secured 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) 1. Welcome + security notice 2. **Gateway selection** (Local / Remote / Configure later) 3. **Auth (Anthropic OAuth)** — local only 4. **Setup Wizard** (Gateway‑driven) 5. **Permissions** (TCC prompts) 6. **CLI** (optional) 7. **Onboarding chat** (dedicated session) 8. 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#state` value * 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.md` when finished so it only runs once ## Optional: Gmail hooks (manual) Gmail Pub/Sub setup is currently a manual step. Use: ```bash theme={null} openclaw webhooks gmail setup --account you@gmail.com ``` See [/automation/gmail-pubsub](/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//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 ```bash theme={null} npm install -g openclaw@latest # or: pnpm add -g openclaw@latest ``` From source (development): ```bash theme={null} 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 1. Pair WhatsApp Web (shows QR; scan with the assistant phone): ```bash theme={null} openclaw channels login ``` 2. Start the Gateway (leave it running): ```bash theme={null} openclaw gateway --port 18789 ``` 3. Put a minimal config in `~/.openclaw/openclaw.json`: ```json5 theme={null} { 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. ```bash theme={null} openclaw setup ``` Full workspace layout + backup guide: [Agent workspace](/concepts/agent-workspace) Memory workflow: [Memory](/concepts/memory) Optional: choose a different workspace with `agents.defaults.workspace` (supports `~`). ```json5 theme={null} { agent: { workspace: "~/.openclaw/workspace", }, } ``` If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely: ```json5 theme={null} { 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: ```json5 theme={null} { 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//sessions/{{SessionId}}.jsonl` * Session metadata (token usage, last route, etc): `~/.openclaw/agents//sessions/sessions.json` (legacy: `~/.openclaw/sessions/sessions.json`) * `/new` or `/reset` starts a fresh session for that chat (configurable via `resetTriggers`). 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.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls. * If the file is missing, the heartbeat still runs and the model decides what to do. * If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), OpenClaw suppresses outbound delivery for that heartbeat. * Heartbeats run full agent turns — shorter intervals burn more tokens. ```json5 theme={null} { 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:` 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 ```bash theme={null} 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](/web/webchat) * Gateway ops: [Gateway runbook](/gateway) * Cron + wakeups: [Cron jobs](/automation/cron-jobs) * macOS menu bar companion: [OpenClaw macOS app](/platforms/macos) * iOS node app: [iOS app](/platforms/ios) * Android node app: [Android app](/platforms/android) * Windows status: [Windows (WSL2)](/platforms/windows) * Linux status: [Linux app](/platforms/linux) * Security: [Security](/gateway/security) # Pairing Source: https://docs.openclaw.ai/start/pairing # Pairing “Pairing” is OpenClaw’s explicit **owner approval** step. It is used in two places: 1. **DM pairing** (who is allowed to talk to the bot) 2. **Node pairing** (which devices/nodes are allowed to join the gateway network) Security context: [Security](/gateway/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](/gateway/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 ```bash theme={null} openclaw pairing list telegram openclaw pairing approve telegram ``` Supported channels: `telegram`, `whatsapp`, `signal`, `imessage`, `discord`, `slack`. ### Where the state lives Stored under `~/.openclaw/credentials/`: * Pending requests: `-pairing.json` * Approved allowlist store: `-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 ```bash theme={null} openclaw devices list openclaw devices approve openclaw devices reject ``` ### 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 * Security model + prompt injection: [Security](/gateway/security) * Updating safely (run doctor): [Updating](/install/updating) * Channel configs: * Telegram: [Telegram](/channels/telegram) * WhatsApp: [WhatsApp](/channels/whatsapp) * Signal: [Signal](/channels/signal) * BlueBubbles (iMessage): [BlueBubbles](/channels/bluebubbles) * iMessage (legacy): [iMessage](/channels/imessage) * Discord: [Discord](/channels/discord) * Slack: [Slack](/channels/slack) # 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`. Switching between npm and git installs later is easy. Install the other flavor and run `openclaw doctor` to update the gateway service entrypoint. ## From source (development) ```bash theme={null} 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) ```bash theme={null} OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \ OPENCLAW_STATE_DIR=~/.openclaw-a \ openclaw gateway --port 19001 ``` ## Send a test message Requires a running Gateway. ```bash theme={null} 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](/install/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: ```bash theme={null} openclaw setup ``` From inside this repo, use the local CLI entry: ```bash theme={null} openclaw setup ``` If you don’t have a global install yet, run it via `pnpm openclaw setup`. ## Stable workflow (macOS app first) 1. Install + launch **OpenClaw\.app** (menu bar). 2. Complete the onboarding/permissions checklist (TCC prompts). 3. Ensure Gateway is **Local** and running (the app manages it). 4. Link surfaces (example: WhatsApp): ```bash theme={null} openclaw channels login ``` 5. Sanity check: ```bash theme={null} openclaw health ``` If onboarding is not available in your build: * Run `openclaw setup`, then `openclaw 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: ```bash theme={null} ./scripts/restart-mac.sh ``` ### 1) Start the dev Gateway ```bash theme={null} 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: ```bash theme={null} 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//sessions/` * Logs: `/tmp/openclaw/` ## Credential storage map Use this when debugging auth or deciding what to back up: * **WhatsApp**: `~/.openclaw/credentials/whatsapp//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/-allowFrom.json` * **Model auth profiles**: `~/.openclaw/agents//agent/auth-profiles.json` * **Legacy OAuth import**: `~/.openclaw/credentials/oauth.json` More detail: [Security](/gateway/security#credential-storage-map). ## Updating (without wrecking your setup) * Keep `~/.openclaw/workspace` and `~/.openclaw/` as “your stuff”; don’t put personal prompts/config into the `openclaw` repo. * Updating source: `git pull` + `pnpm install` (when lockfile changed) + keep using `pnpm 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: ```bash theme={null} 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](/gateway) for the systemd notes. ## Related docs * [Gateway runbook](/gateway) (flags, supervision, ports) * [Gateway configuration](/gateway/configuration) (config schema + examples) * [Discord](/channels/discord) and [Telegram](/channels/telegram) (reply tags + replyToMode settings) * [OpenClaw assistant setup](/start/openclaw) * [macOS app](/platforms/macos) (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.