Files
openclaw-skill/openclaw-knowhow-skill/output/openclaw-docs/references/llms-full.md
Selig 4c966a3ad2 Initial commit: OpenClaw Skill Collection
6 custom skills (assign-task, dispatch-webhook, daily-briefing,
task-capture, qmd-brain, tts-voice) with technical documentation.
Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
2026-03-13 10:58:30 +08:00

1.5 MiB
Executable File
Raw Blame History

Auth Monitoring

Source: https://docs.openclaw.ai/automation/auth-monitoring

Auth monitoring

OpenClaw exposes OAuth expiry health via openclaw models status. Use that for automation and alerting; scripts are optional extras for phone workflows.

Preferred: CLI check (portable)

openclaw models status --check

Exit codes:

  • 0: 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 reauth flow over SSH.
  • scripts/termux-quick-auth.sh: onetap 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 dont need phone automation or systemd timers, skip these scripts.

Cron Jobs

Source: https://docs.openclaw.ai/automation/cron-jobs

Cron jobs (Gateway scheduler)

Cron vs Heartbeat? See Cron vs Heartbeat for guidance on when to use each.

Cron is the Gateways 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 dont lose schedules.
  • Two execution styles:
    • Main session: enqueue a system event, then run on the next heartbeat.
    • Isolated: run a dedicated agent turn in cron:<jobId>, with delivery (announce by default or none).
  • Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.

Quick start (actionable)

Create a one-shot reminder, verify it exists, and run it immediately:

openclaw cron add \
  --name "Reminder" \
  --at "2026-02-01T16:00:00Z" \
  --session main \
  --system-event "Reminder: check the cron docs draft" \
  --wake now \
  --delete-after-run

openclaw cron list
openclaw cron run <job-id> --force
openclaw cron runs --id <job-id>

Schedule a recurring isolated job with delivery:

openclaw cron add \
  --name "Morning brief" \
  --cron "0 7 * * *" \
  --tz "America/Los_Angeles" \
  --session isolated \
  --message "Summarize overnight updates." \
  --announce \
  --channel slack \
  --to "channel:C1234567890"

Tool-call equivalents (Gateway cron tool)

For the canonical JSON shapes and examples, see JSON schema for tool calls.

Where cron jobs are stored

Cron jobs are persisted on the Gateway host at ~/.openclaw/cron/jobs.json by default. The Gateway loads the file into memory and writes it back on changes, so manual edits are only safe when the Gateway is stopped. Prefer openclaw cron add/edit or the cron tool call API for changes.

Beginner-friendly overview

Think of a cron job as: when to run + what to do.

  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:<jobId>.
  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 hosts local timezone is used.

Main vs isolated execution

Main session jobs (system events)

Main jobs enqueue a system event and optionally wake the heartbeat runner. They must use payload.kind = "systemEvent".

  • wakeMode: "next-heartbeat" (default): event waits for the next scheduled heartbeat.
  • wakeMode: "now": event triggers an immediate heartbeat run.

This is the best fit when you want the normal heartbeat prompt + main-session context. See Heartbeat.

Isolated jobs (dedicated cron sessions)

Isolated jobs run a dedicated agent turn in session cron:<jobId>.

Key behaviors:

  • Prompt is prefixed with [cron:<jobId> <job name>] for traceability.
  • Each run starts a fresh session id (no prior conversation carry-over).
  • Default behavior: if 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 sessions “last route” (the last place the agent replied).

Target format reminders:

  • Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. channel:<id>, user:<id>) to avoid ambiguity.
  • Telegram topics should use the :topic: form (see below).

Telegram delivery targets (topics / forum threads)

Telegram supports forum topics via message_thread_id. For cron delivery, you can encode the topic/thread into the to field:

  • -1001234567890 (chat id only)
  • -1001234567890:topic:123 (preferred: explicit topic marker)
  • -1001234567890:123 (shorthand: numeric suffix)

Prefixed targets like telegram:... / telegram:group:... are also accepted:

  • telegram:group:-1001234567890:topic:123

JSON schema for tool calls

Use these shapes when calling Gateway cron.* tools directly (agent tool calls or RPC). CLI flags accept human durations like 20m, but tool calls should use an ISO 8601 string for schedule.at and milliseconds for schedule.everyMs.

cron.add params

One-shot, main session job (system event):

{
  "name": "Reminder",
  "schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" },
  "sessionTarget": "main",
  "wakeMode": "now",
  "payload": { "kind": "systemEvent", "text": "Reminder text" },
  "deleteAfterRun": true
}

Recurring, isolated job with delivery:

{
  "name": "Morning brief",
  "schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
  "sessionTarget": "isolated",
  "wakeMode": "next-heartbeat",
  "payload": {
    "kind": "agentTurn",
    "message": "Summarize overnight updates."
  },
  "delivery": {
    "mode": "announce",
    "channel": "slack",
    "to": "channel:C1234567890",
    "bestEffort": true
  }
}

Notes:

  • schedule.kind: at (at), every (everyMs), 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

{
  "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

{ "jobId": "job-123", "mode": "force" }
{ "jobId": "job-123" }

Storage & history

  • Job store: ~/.openclaw/cron/jobs.json (Gateway-managed JSON).
  • Run history: ~/.openclaw/cron/runs/<jobId>.jsonl (JSONL, auto-pruned).
  • Override store path: cron.store in config.

Configuration

{
  cron: {
    enabled: true, // default true
    store: "~/.openclaw/cron/jobs.json",
    maxConcurrentRuns: 1, // default 1
  },
}

Disable cron entirely:

  • cron.enabled: false (config)
  • OPENCLAW_SKIP_CRON=1 (env)

CLI quickstart

One-shot reminder (UTC ISO, auto-delete after success):

openclaw cron add \
  --name "Send reminder" \
  --at "2026-01-12T18:00:00Z" \
  --session main \
  --system-event "Reminder: submit expense report." \
  --wake now \
  --delete-after-run

One-shot reminder (main session, wake immediately):

openclaw cron add \
  --name "Calendar check" \
  --at "20m" \
  --session main \
  --system-event "Next heartbeat: check calendar." \
  --wake now

Recurring isolated job (announce to WhatsApp):

openclaw cron add \
  --name "Morning status" \
  --cron "0 7 * * *" \
  --tz "America/Los_Angeles" \
  --session isolated \
  --message "Summarize inbox + calendar for today." \
  --announce \
  --channel whatsapp \
  --to "+15551234567"

Recurring isolated job (deliver to a Telegram topic):

openclaw cron add \
  --name "Nightly summary (topic)" \
  --cron "0 22 * * *" \
  --tz "America/Los_Angeles" \
  --session isolated \
  --message "Summarize today; send to the nightly topic." \
  --announce \
  --channel telegram \
  --to "-1001234567890:topic:123"

Isolated job with model and thinking override:

openclaw cron add \
  --name "Deep analysis" \
  --cron "0 6 * * 1" \
  --tz "America/Los_Angeles" \
  --session isolated \
  --message "Weekly deep analysis of project progress." \
  --model "opus" \
  --thinking high \
  --announce \
  --channel whatsapp \
  --to "+15551234567"

Agent selection (multi-agent setups):

# Pin a job to agent "ops" (falls back to default if that agent is missing)
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops

# Switch or clear the agent on an existing job
openclaw cron edit <jobId> --agent ops
openclaw cron edit <jobId> --clear-agent

Manual run (debug):

openclaw cron run <jobId> --force

Edit an existing job (patch fields):

openclaw cron edit <jobId> \
  --message "Updated prompt" \
  --model "opus" \
  --thinking low

Run history:

openclaw cron runs --id <jobId> --limit 50

Immediate system event without creating a job:

openclaw system event --mode now --text "Next heartbeat: check battery."

Gateway API surface

  • cron.list, cron.status, cron.add, cron.update, cron.remove
  • cron.run (force or due), cron.runs For immediate system events without a job, use openclaw system event.

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:<id> so its explicit and unambiguous.
  • If you see telegram:... prefixes in logs or stored “last route” targets, thats 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

# Heartbeat checklist

- Check email for urgent messages
- Review calendar for events in next 2 hours
- If a background task finished, summarize results
- If idle for 8+ hours, send a brief check-in

The agent reads this on each heartbeat and handles all items in one turn.

Configuring heartbeat

{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m", // interval
        target: "last", // where to deliver alerts
        activeHours: { start: "08:00", end: "22:00" }, // optional
      },
    },
  },
}

See Heartbeat for full configuration.

Cron: Precise Scheduling

Cron jobs run at exact times and can run in isolated sessions without affecting main context.

When to use cron

  • Exact timing required: "Send this at 9:00 AM every Monday" (not "sometime around 9").
  • Standalone tasks: Tasks that don't need conversational context.
  • Different model/thinking: Heavy analysis that warrants a more powerful model.
  • One-shot reminders: "Remind me in 20 minutes" with --at.
  • Noisy/frequent tasks: Tasks that would clutter main session history.
  • External triggers: Tasks that should run independently of whether the agent is otherwise active.

Cron advantages

  • Exact timing: 5-field cron expressions with timezone support.
  • Session isolation: Runs in cron:<jobId> without polluting main history.
  • Model overrides: Use a cheaper or more powerful model per job.
  • Delivery control: Isolated jobs default to announce (summary); 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

openclaw cron add \
  --name "Morning briefing" \
  --cron "0 7 * * *" \
  --tz "America/New_York" \
  --session isolated \
  --message "Generate today's briefing: weather, calendar, top emails, news summary." \
  --model opus \
  --announce \
  --channel whatsapp \
  --to "+15551234567"

This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces a summary directly to WhatsApp.

Cron example: One-shot reminder

openclaw cron add \
  --name "Meeting reminder" \
  --at "20m" \
  --session main \
  --system-event "Reminder: standup meeting starts in 10 minutes." \
  --wake now \
  --delete-after-run

See Cron jobs for full CLI reference.

Decision Flowchart

Does the task need to run at an EXACT time?
  YES -> Use cron
  NO  -> Continue...

Does the task need isolation from main session?
  YES -> Use cron (isolated)
  NO  -> Continue...

Can this task be batched with other periodic checks?
  YES -> Use heartbeat (add to HEARTBEAT.md)
  NO  -> Use cron

Is this a one-shot reminder?
  YES -> Use cron with --at
  NO  -> Continue...

Does it need a different model or thinking level?
  YES -> Use cron (isolated) with --model/--thinking
  NO  -> Use heartbeat

Combining Both

The most efficient setup uses both:

  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):

# Heartbeat checklist

- Scan inbox for urgent emails
- Check calendar for events in next 2h
- Review any pending tasks
- Light check-in if quiet for 8+ hours

Cron jobs (precise timing):

# Daily morning briefing at 7am
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce

# Weekly project review on Mondays at 9am
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus

# One-shot reminder
openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now

Lobster: Deterministic workflows with approvals

Lobster is the workflow runtime for multi-step tool pipelines that need deterministic execution and explicit approvals. Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.

When Lobster fits

  • Multi-step automation: You need a fixed pipeline of tool calls, not a one-off prompt.
  • Approval gates: Side effects should pause until you approve, then resume.
  • Resumable runs: Continue a paused workflow without re-running earlier steps.

How it pairs with heartbeat and cron

  • Heartbeat/cron decide when a run happens.
  • Lobster defines what steps happen once the run starts.

For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster. For ad-hoc workflows, call Lobster directly.

Operational notes (from the code)

  • Lobster runs as a local subprocess (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 for full usage and examples.

Main Session vs Isolated Session

Both heartbeat and cron can interact with the main session, but differently:

Heartbeat Cron (main) Cron (isolated)
Session Main Main (via system event) cron:<jobId>
History Shared Shared Fresh each run
Context Full Full None (starts clean)
Model Main session model Main session model Can override
Output Delivered if not HEARTBEAT_OK Heartbeat prompt + event Announce summary (default)

When to use main session cron

Use --session main with --system-event when you want:

  • The reminder/event to appear in main session context
  • The agent to handle it during the next heartbeat with full context
  • No separate isolated run
openclaw cron add \
  --name "Check project" \
  --every "4h" \
  --session main \
  --system-event "Time for a project health check" \
  --wake now

When to use isolated cron

Use --session isolated when you want:

  • A clean slate without prior context
  • Different model or thinking settings
  • Announce summaries directly to a channel
  • History that doesn't clutter main session
openclaw cron add \
  --name "Deep analysis" \
  --cron "0 6 * * 0" \
  --session isolated \
  --message "Weekly codebase analysis..." \
  --model opus \
  --thinking high \
  --announce

Cost Considerations

Mechanism Cost Profile
Heartbeat One turn every N minutes; scales with HEARTBEAT.md size
Cron (main) Adds event to next heartbeat (no isolated turn)
Cron (isolated) Full agent turn per job; can use cheaper model

Tips:

  • Keep HEARTBEAT.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.
  • Heartbeat - full heartbeat configuration
  • Cron jobs - full cron CLI and API reference
  • System - system events + heartbeat controls

Gmail PubSub

Source: https://docs.openclaw.ai/automation/gmail-pubsub

Gmail Pub/Sub -> OpenClaw

Goal: Gmail watch -> Pub/Sub push -> gog gmail watch serve -> OpenClaw webhook.

Prereqs

  • gcloud installed and logged in (install guide).
  • gog (gogcli) installed and authorized for the Gmail account (gogcli.sh).
  • OpenClaw hooks enabled (see Webhooks).
  • tailscale logged in (tailscale.com). Supported setup uses Tailscale Funnel for the public HTTPS endpoint. Other tunnel services can work, but are DIY/unsupported and require manual wiring. Right now, Tailscale is what we support.

Example hook config (enable Gmail preset mapping):

{
  hooks: {
    enabled: true,
    token: "OPENCLAW_HOOK_TOKEN",
    path: "/hooks",
    presets: ["gmail"],
  },
}

To deliver the Gmail summary to a chat surface, override the preset with a mapping that sets deliver + optional channel/to:

{
  hooks: {
    enabled: true,
    token: "OPENCLAW_HOOK_TOKEN",
    presets: ["gmail"],
    mappings: [
      {
        match: { path: "gmail" },
        action: "agent",
        wakeMode: "now",
        name: "Gmail",
        sessionKey: "hook:gmail:{{messages[0].id}}",
        messageTemplate: "New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
        model: "openai/gpt-5.2-mini",
        deliver: true,
        channel: "last",
        // to: "+15551234567"
      },
    ],
  },
}

If you want a fixed channel, set channel + to. Otherwise channel: "last" uses the last delivery route (falls back to WhatsApp).

To force a cheaper model for Gmail runs, set model in the mapping (provider/model or alias). If you enforce agents.defaults.models, include it there.

To set a default model and thinking level specifically for Gmail hooks, add hooks.gmail.model / hooks.gmail.thinking in your config:

{
  hooks: {
    gmail: {
      model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
      thinking: "off",
    },
  },
}

Notes:

  • Per-hook model/thinking in the mapping still overrides these defaults.
  • Fallback order: hooks.gmail.modelagents.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).

Use the OpenClaw helper to wire everything together (installs deps on macOS via brew):

openclaw webhooks gmail setup \
  --account openclaw@gmail.com

Defaults:

  • Uses Tailscale Funnel for the public push endpoint.
  • Writes hooks.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 <url> or --tailscale off.

Platform note: on macOS the wizard installs gcloud, gogcli, and tailscale via Homebrew; on Linux install them manually first.

Gateway auto-start (recommended):

  • When hooks.enabled=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):

openclaw webhooks gmail run

One-time setup

  1. Select the GCP project that owns the OAuth client used by gog.
gcloud auth login
gcloud config set project <project-id>

Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.

  1. Enable APIs:
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
  1. Create a topic:
gcloud pubsub topics create gog-gmail-watch
  1. Allow Gmail push to publish:
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
  --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
  --role=roles/pubsub.publisher

Start the watch

gog gmail watch start \
  --account openclaw@gmail.com \
  --label INBOX \
  --topic projects/<project-id>/topics/gog-gmail-watch

Save the history_id from the output (for debugging).

Run the push handler

Local example (shared token auth):

gog gmail watch serve \
  --account openclaw@gmail.com \
  --bind 127.0.0.1 \
  --port 8788 \
  --path /gmail-pubsub \
  --token <shared> \
  --hook-url http://127.0.0.1:18789/hooks/gmail \
  --hook-token OPENCLAW_HOOK_TOKEN \
  --include-body \
  --max-bytes 20000

Notes:

  • --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):

cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate

Use the generated URL as the push endpoint:

gcloud pubsub subscriptions create gog-gmail-watch-push \
  --topic gog-gmail-watch \
  --push-endpoint "https://<public-url>/gmail-pubsub?token=<shared>"

Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:

gog gmail watch serve --verify-oidc --oidc-email <svc@...>

Test

Send a message to the watched inbox:

gog gmail send \
  --account openclaw@gmail.com \
  --to openclaw@gmail.com \
  --subject "watch test" \
  --body "ping"

Check watch state and history:

gog gmail watch status --account openclaw@gmail.com
gog gmail history --account openclaw@gmail.com --since <historyId>

Troubleshooting

  • Invalid topicName: project mismatch (topic not in the OAuth client project).
  • User not authorized: missing roles/pubsub.publisher on the topic.
  • Empty messages: Gmail push only provides historyId; fetch via gog gmail history.

Cleanup

gog gmail watch stop --account openclaw@gmail.com
gcloud pubsub subscriptions delete gog-gmail-watch-push
gcloud pubsub topics delete gog-gmail-watch

Polls

Source: https://docs.openclaw.ai/automation/poll

Polls

Supported channels

  • WhatsApp (web channel)
  • Discord
  • MS Teams (Adaptive Cards)

CLI

# WhatsApp
openclaw message poll --target +15555550123 \
  --poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
openclaw message poll --target 123456789@g.us \
  --poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi

# Discord
openclaw message poll --channel discord --target channel:123456789 \
  --poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
openclaw message poll --channel discord --target channel:123456789 \
  --poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48

# MS Teams
openclaw message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
  --poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"

Options:

  • --channel: whatsapp (default), discord, 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

{
  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 <token> (recommended)
  • x-openclaw-token: <token>
  • ?token=<token> (deprecated; logs a warning and will be removed in a future major release)

Endpoints

POST /hooks/wake

Payload:

{ "text": "System line", "mode": "now" }
  • 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:

{
  "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:<uuid>. 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/<name> (mapped)

Custom hook names are resolved via hooks.mappings (see configuration). A mapping can turn arbitrary payloads into wake or agent actions, with optional templates or code transforms.

Mapping options (summary):

  • hooks.presets: ["gmail"] enables the built-in Gmail mapping.
  • hooks.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 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

curl -X POST http://127.0.0.1:18789/hooks/wake \
  -H 'Authorization: Bearer SECRET' \
  -H 'Content-Type: application/json' \
  -d '{"text":"New email received","mode":"now"}'
curl -X POST http://127.0.0.1:18789/hooks/agent \
  -H 'x-openclaw-token: SECRET' \
  -H 'Content-Type: application/json' \
  -d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'

Use a different model

Add model to the agent payload (or mapping) to override the model for that run:

curl -X POST http://127.0.0.1:18789/hooks/agent \
  -H 'x-openclaw-token: SECRET' \
  -H 'Content-Type: application/json' \
  -d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'

If you enforce agents.defaults.models, make sure the override model is included there.

curl -X POST http://127.0.0.1:18789/hooks/gmail \
  -H 'Authorization: Bearer SECRET' \
  -H 'Content-Type: application/json' \
  -d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'

Security

  • Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
  • Use a dedicated hook token; do not reuse gateway auth tokens.
  • Avoid including sensitive raw payloads in webhook logs.
  • Hook payloads are treated as untrusted and wrapped with safety boundaries by default. If you must disable this for a specific hook, set allowUnsafeExternalContent: 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)
{
  "broadcast": {
    "120363403215116621@g.us": ["alfred", "baerbel", "assistant3"]
  }
}

Result: When OpenClaw would reply in this chat, it will run all three agents.

Processing Strategy

Control how agents process messages:

Parallel (Default)

All agents process simultaneously:

{
  "broadcast": {
    "strategy": "parallel",
    "120363403215116621@g.us": ["alfred", "baerbel"]
  }
}

Sequential

Agents process in order (one waits for previous to finish):

{
  "broadcast": {
    "strategy": "sequential",
    "120363403215116621@g.us": ["alfred", "baerbel"]
  }
}

Complete Example

{
  "agents": {
    "list": [
      {
        "id": "code-reviewer",
        "name": "Code Reviewer",
        "workspace": "/path/to/code-reviewer",
        "sandbox": { "mode": "all" }
      },
      {
        "id": "security-auditor",
        "name": "Security Auditor",
        "workspace": "/path/to/security-auditor",
        "sandbox": { "mode": "all" }
      },
      {
        "id": "docs-generator",
        "name": "Documentation Generator",
        "workspace": "/path/to/docs-generator",
        "sandbox": { "mode": "all" }
      }
    ]
  },
  "broadcast": {
    "strategy": "parallel",
    "120363403215116621@g.us": ["code-reviewer", "security-auditor", "docs-generator"],
    "120363424282127706@g.us": ["support-en", "support-de"],
    "+15555550123": ["assistant", "logger"]
  }
}

How It Works

Message Flow

  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:

{
  "broadcast": {
    "DEV_GROUP": ["formatter", "linter", "tester"]
  }
}

Good: Each agent has one job
Bad: One generic "dev-helper" agent

2. Use Descriptive Names

Make it clear what each agent does:

{
  "agents": {
    "security-scanner": { "name": "Security Scanner" },
    "code-formatter": { "name": "Code Formatter" },
    "test-generator": { "name": "Test Generator" }
  }
}

3. Configure Different Tool Access

Give agents only the tools they need:

{
  "agents": {
    "reviewer": {
      "tools": { "allow": ["read", "exec"] } // Read-only
    },
    "fixer": {
      "tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
    }
  }
}

4. Monitor Performance

With many agents, consider:

  • Using "strategy": "parallel" (default) for speed
  • Limiting broadcast groups to 5-10 agents
  • Using faster models for simpler agents

5. Handle Failures Gracefully

Agents fail independently. One agent's error doesn't block others:

Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error

Compatibility

Providers

Broadcast groups currently work with:

  • WhatsApp (implemented)
  • 🚧 Telegram (planned)
  • 🚧 Discord (planned)
  • 🚧 Slack (planned)

Routing

Broadcast groups work alongside existing routing:

{
  "bindings": [
    {
      "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } },
      "agentId": "alfred"
    }
  ],
  "broadcast": {
    "GROUP_B": ["agent1", "agent2"]
  }
}
  • GROUP_A: Only alfred responds (normal routing)
  • GROUP_B: agent1 AND agent2 respond (broadcast)

Precedence: broadcast takes priority over bindings.

Troubleshooting

Agents Not Responding

Check:

  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:

tail -f ~/.openclaw/logs/gateway.log | grep broadcast

Only One Agent Responding

Cause: Peer ID might be in bindings but not broadcast.

Fix: Add to broadcast config or remove from bindings.

Performance Issues

If slow with many agents:

  • Reduce number of agents per group
  • Use lighter models (sonnet instead of opus)
  • Check sandbox startup time

Examples

Example 1: Code Review Team

{
  "broadcast": {
    "strategy": "parallel",
    "120363403215116621@g.us": [
      "code-formatter",
      "security-scanner",
      "test-coverage",
      "docs-checker"
    ]
  },
  "agents": {
    "list": [
      {
        "id": "code-formatter",
        "workspace": "~/agents/formatter",
        "tools": { "allow": ["read", "write"] }
      },
      {
        "id": "security-scanner",
        "workspace": "~/agents/security",
        "tools": { "allow": ["read", "exec"] }
      },
      {
        "id": "test-coverage",
        "workspace": "~/agents/testing",
        "tools": { "allow": ["read", "exec"] }
      },
      { "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
    ]
  }
}

User sends: Code snippet
Responses:

  • code-formatter: "Fixed indentation and added type hints"
  • security-scanner: "⚠️ SQL injection vulnerability in line 12"
  • test-coverage: "Coverage is 45%, missing tests for error cases"
  • docs-checker: "Missing docstring for function process_data"

Example 2: Multi-Language Support

{
  "broadcast": {
    "strategy": "sequential",
    "+15555550123": ["detect-language", "translator-en", "translator-de"]
  },
  "agents": {
    "list": [
      { "id": "detect-language", "workspace": "~/agents/lang-detect" },
      { "id": "translator-en", "workspace": "~/agents/translate-en" },
      { "id": "translator-de", "workspace": "~/agents/translate-de" }
    ]
  }
}

API Reference

Config Schema

interface OpenClawConfig {
  broadcast?: {
    strategy?: "parallel" | "sequential";
    [peerId: string]: string[];
  };
}

Fields

  • strategy (optional): How to process agents
    • "parallel" (default): All agents process simultaneously
    • "sequential": Agents process in array order
  • [peerId]: WhatsApp group JID, E.164 number, or other peer ID
    • Value: Array of agent IDs that should process messages

Limitations

  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

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:

{
  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:<agentId>:discord:channel:<channelId> (display names use discord:<guildSlug>#<channelSlug>).
  • 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:<id> (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:<channelId> 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 <code>.
    • 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.
  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.
    • The discord tool is only exposed when the current channel is Discord.
  13. Native commands use isolated session keys (agent:<agentId>:discord:slash:<userId>) rather than the shared main session.

Note: Name → id resolution uses guild member search and requires Server Members Intent; if the bot cant search members, use ids or <@id> mentions. Note: Slugs are lowercase with spaces replaced by -. Channel names are slugged without the leading #. Note: Guild context [from:] lines include author.tag + id to make ping-ready replies easy.

Config writes

By default, Discord is allowed to write config updates triggered by /config set|unset (requires commands.config: true).

Disable with:

{
  channels: { discord: { configWrites: false } },
}

How to create your own bot

This is the “Discord Developer Portal” setup for running OpenClaw in a server (guild) channel like #help.

1) Create the Discord app + bot user

  1. Discord Developer Portal → ApplicationsNew Application
  2. In your app:
    • BotAdd 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 BotPrivileged Gateway Intents, enable:

  • Message Content Intent (required to read message text in most guilds; without it youll 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: OAuth2URL 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 youre 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 SettingsAdvanced → 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:

{
  channels: {
    discord: {
      enabled: true,
      token: "YOUR_BOT_TOKEN",
    },
  },
}

Multi-account support: use channels.discord.accounts with per-account tokens and optional name. See gateway/configuration for the shared pattern.

Allowlist + channel routing

Example “single server, only allow me, only allow #help”:

{
  channels: {
    discord: {
      enabled: true,
      dm: { enabled: false },
      guilds: {
        YOUR_GUILD_ID: {
          users: ["YOUR_USER_ID"],
          requireMention: true,
          channels: {
            help: { allow: true, requireMention: true },
          },
        },
      },
      retry: {
        attempts: 3,
        minDelayMs: 500,
        maxDelayMs: 30000,
        jitter: 0.1,
      },
    },
  },
}

Notes:

  • requireMention: 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.<id>.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 didnt 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.<id>.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 cant verify permissions.
  • DMs dont work: channels.discord.dm.enabled=false, channels.discord.dm.policy="disabled", or you havent been approved yet (channels.discord.dm.policy="pairing").
  • Exec approvals in Discord: Discord supports a button UI for exec approvals in DMs (Allow once / Always allow / Deny). /approve <id> ... is only for forwarded approvals and wont resolve Discords 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 and Slash commands for the broader approvals and command flow.

Capabilities & limits

  • DMs and guild text channels (threads are treated as separate channels; voice not supported).
  • Typing indicators sent best-effort; message chunking uses channels.discord.textChunkLimit (default 2000) and splits tall replies by line count (channels.discord.maxLinesPerMessage, default 17).
  • Optional newline chunking: set channels.discord.chunkMode="newline" to split on blank lines (paragraph boundaries) before length chunking.
  • File uploads supported up to the configured channels.discord.mediaMaxMb (default 8 MB).
  • Mention-gated guild replies by default to avoid noisy bots.
  • Reply context is injected when a message references another message (quoted content + ids).
  • Native reply threading is off by default; enable with channels.discord.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.

Config

{
  channels: {
    discord: {
      enabled: true,
      token: "abc.123",
      groupPolicy: "allowlist",
      guilds: {
        "*": {
          channels: {
            general: { allow: true },
          },
        },
      },
      mediaMaxMb: 8,
      actions: {
        reactions: true,
        stickers: true,
        emojiUploads: true,
        stickerUploads: true,
        polls: true,
        permissions: true,
        messages: true,
        threads: true,
        pins: true,
        search: true,
        memberInfo: true,
        roleInfo: true,
        roles: false,
        channelInfo: true,
        channels: true,
        voiceStatus: true,
        events: true,
        moderation: false,
        presence: false,
      },
      replyToMode: "off",
      dm: {
        enabled: true,
        policy: "pairing", // pairing | allowlist | open | disabled
        allowFrom: ["123456789012345678", "steipete"],
        groupEnabled: false,
        groupChannels: ["openclaw-dm"],
      },
      guilds: {
        "*": { requireMention: true },
        "123456789012345678": {
          slug: "friends-of-openclaw",
          requireMention: false,
          reactionNotifications: "own",
          users: ["987654321098765432", "steipete"],
          channels: {
            general: { allow: true },
            help: {
              allow: true,
              requireMention: true,
              users: ["987654321098765432"],
              skills: ["search", "docs"],
              systemPrompt: "Keep answers short.",
            },
          },
        },
      },
    },
  },
}

Ack reactions are controlled globally via messages.ackReaction + messages.ackReactionScope. Use messages.removeAckAfterReply to clear the ack reaction after the bot replies.

  • dm.enabled: 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.<id>.slug: optional friendly slug used for display names.
  • guilds.<id>.users: optional per-guild user allowlist (ids or names).
  • guilds.<id>.tools: optional per-guild tool policy overrides (allow/deny/alsoAllow) used when the channel override is missing.
  • guilds.<id>.toolsBySender: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; "*" wildcard supported).
  • guilds.<id>.channels.<channel>.allow: allow/deny the channel when groupPolicy="allowlist".
  • guilds.<id>.channels.<channel>.requireMention: mention gating for the channel.
  • guilds.<id>.channels.<channel>.tools: optional per-channel tool policy overrides (allow/deny/alsoAllow).
  • guilds.<id>.channels.<channel>.toolsBySender: optional per-sender tool policy overrides within the channel ("*" wildcard supported).
  • guilds.<id>.channels.<channel>.users: optional per-channel user allowlist.
  • guilds.<id>.channels.<channel>.skills: skill filter (omit = all skills, empty = none).
  • guilds.<id>.channels.<channel>.systemPrompt: extra system prompt for the channel. Discord channel topics are injected as untrusted context (not system prompt).
  • guilds.<id>.channels.<channel>.enabled: set false to disable the channel.
  • guilds.<id>.channels: channel rules (keys are channel slugs or ids).
  • guilds.<id>.requireMention: per-guild mention requirement (overridable per channel).
  • guilds.<id>.reactionNotifications: reaction system event mode (off, own, all, allowlist).
  • textChunkLimit: outbound text chunk size (chars). Default: 2000.
  • chunkMode: length (default) splits only when 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["<user_id>"].historyLimit.
  • retry: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
  • pluralkit: resolve PluralKit proxied messages so system members appear as distinct senders.
  • actions: per-action tool gates; omit to allow all (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.<id>.reactionNotifications:

  • off: no reaction events.
  • own: reactions on the bot's own messages (default).
  • all: all reactions on all messages.
  • allowlist: reactions from guilds.<id>.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.

{
  channels: {
    discord: {
      pluralkit: {
        enabled: true,
        token: "pk_live_...", // optional; required for private systems
      },
    },
  },
}

Allowlist notes (PK-enabled):

  • Use pk:<memberId> in dm.allowFrom, guilds.<id>.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:<id>]] — reply to a specific message id from context/history. Current message ids are appended to prompts as [message_id: …]; history entries already include ids.

Behavior is controlled by channels.discord.replyToMode:

  • off: ignore tags.
  • first: only the first outbound chunk/attachment is a reply.
  • all: every outbound chunk/attachment is a reply.

Allowlist matching notes:

  • allowFrom/users/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.<id>.channels is present, channels not listed are denied by default.
  • When guilds.<id>.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 OpenClaws 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 arent 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 platforms WebSocket event subscription so messages can be received without exposing a public webhook URL.


Plugin required

Install the Feishu plugin:

openclaw plugins install @openclaw/feishu

Local checkout (when running from a git repo):

openclaw plugins install ./extensions/feishu

Quickstart

There are two ways to add the Feishu channel:

If you just installed OpenClaw, run the wizard:

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:

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 and sign in.

Lark (global) tenants should use https://open.larksuite.com/app and set domain: "lark" in the Feishu config.

2. Create an app

  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:

{
  "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

openclaw channels add

Choose Feishu and paste your App ID + App Secret.

Configure via config file

Edit ~/.openclaw/openclaw.json:

{
  channels: {
    feishu: {
      enabled: true,
      dmPolicy: "pairing",
      accounts: {
        main: {
          appId: "cli_xxx",
          appSecret: "xxx",
          botName: "My AI assistant",
        },
      },
    },
  },
}

Configure via environment variables

export FEISHU_APP_ID="cli_xxx"
export FEISHU_APP_SECRET="xxx"

Lark (global) domain

If your tenant is on Lark (international), set the domain to lark (or a full domain string). You can set it at channels.feishu.domain or per account (channels.feishu.accounts.<id>.domain).

{
  channels: {
    feishu: {
      domain: "lark",
      accounts: {
        main: {
          appId: "cli_xxx",
          appSecret: "xxx",
        },
      },
    },
  },
}

Step 3: Start + test

1. Start the gateway

openclaw gateway

2. Send a test message

In Feishu, find your bot and send a message.

3. Approve pairing

By default, the bot replies with a pairing code. Approve it:

openclaw pairing approve feishu <CODE>

After approval, you can chat normally.


Overview

  • Feishu bot channel: Feishu bot managed by the gateway
  • Deterministic routing: replies always return to Feishu
  • Session isolation: DMs share a main session; groups are isolated
  • WebSocket connection: long connection via Feishu SDK, no public URL needed

Access control

Direct messages

  • Default: dmPolicy: "pairing" (unknown users get a pairing code)
  • Approve pairing:
    openclaw pairing list feishu
    openclaw pairing approve feishu <CODE>
    
  • Allowlist mode: set channels.feishu.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.<chat_id>.requireMention):

  • true = require @mention (default)
  • false = respond without mentions

Group configuration examples

Allow all groups, require @mention (default)

{
  channels: {
    feishu: {
      groupPolicy: "open",
      // Default requireMention: true
    },
  },
}

Allow all groups, no @mention required

{
  channels: {
    feishu: {
      groups: {
        oc_xxx: { requireMention: false },
      },
    },
  },
}

Allow specific users in groups only

{
  channels: {
    feishu: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["ou_xxx", "ou_yyy"],
    },
  },
}

Get group/user IDs

Group IDs (chat_id)

Group IDs look like oc_xxx.

Method 1 (recommended)

  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:

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

{
  channels: {
    feishu: {
      accounts: {
        main: {
          appId: "cli_xxx",
          appSecret: "xxx",
          botName: "Primary bot",
        },
        backup: {
          appId: "cli_yyy",
          appSecret: "yyy",
          botName: "Backup bot",
          enabled: false,
        },
      },
    },
  },
}

Message limits

  • textChunkLimit: outbound text chunk size (default: 2000 chars)
  • mediaMaxMb: media upload/download limit (default: 30MB)

Streaming

Feishu does not support message editing, so block streaming is enabled by default (blockStreaming: true). The bot waits for the full reply before sending.


Configuration reference

Full configuration: Gateway configuration

Key options:

Setting Description Default
channels.feishu.enabled Enable/disable channel true
channels.feishu.domain API domain (feishu or lark) feishu
channels.feishu.accounts.<id>.appId App ID -
channels.feishu.accounts.<id>.appSecret App Secret -
channels.feishu.accounts.<id>.domain Per-account API domain override feishu
channels.feishu.dmPolicy DM policy pairing
channels.feishu.allowFrom DM allowlist (open_id list) -
channels.feishu.groupPolicy Group policy open
channels.feishu.groupAllowFrom Group allowlist -
channels.feishu.groups.<chat_id>.requireMention Require @mention true
channels.feishu.groups.<chat_id>.enabled Enable group true
channels.feishu.textChunkLimit Message chunk size 2000
channels.feishu.mediaMaxMb Media size limit 30
channels.feishu.blockStreaming Disable streaming true

dmPolicy reference

Value Behavior
"pairing" Default. Unknown users get a pairing code; must be approved
"allowlist" Only users in allowFrom can chat
"open" Allow all users (requires "*" in allowFrom)
"disabled" Disable DMs

Supported message types

Receive

  • Text
  • Images
  • Files
  • Audio
  • Video
  • Stickers

Send

  • Text
  • Images
  • Files
  • Audio
  • ⚠️ Rich text (partial support)

Google Chat

Source: https://docs.openclaw.ai/channels/googlechat

Google Chat (Chat API)

Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).

Quick setup (beginner)

  1. Create a Google Cloud project and enable the Google Chat API.
  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:
    • 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 <Your Domain>.
    • 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.
  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.

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:

    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):

    # 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:

    # 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:

    tailscale serve status
    tailscale funnel status
    

Your public webhook URL will be: https://<node-name>.<tailnet>.ts.net/googlechat

Your private dashboard stays tailnet-only: https://<node-name>.<tailnet>.ts.net:8443/

Use the public URL (without :8443) in the Google Chat app config.

Note: This configuration persists across reboots. To remove it later, run tailscale funnel reset and tailscale serve reset.

Option B: Reverse Proxy (Caddy)

If you use a reverse proxy like Caddy, only proxy the specific path:

your-domain.com {
    reverse_proxy /googlechat* localhost:18789
}

With this config, any request to your-domain.com/ will be ignored or returned as 404, while your-domain.com/googlechat is safely routed to OpenClaw.

Option C: Cloudflare Tunnel

Configure your tunnel's ingress rules to only route the webhook path:

  • Path: /googlechat -> http://localhost:18789/googlechat
  • Default Rule: HTTP 404 (Not Found)

How it works

  1. Google Chat sends webhook POSTs to the gateway. Each request includes an Authorization: Bearer <token> 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:<agentId>:googlechat:dm:<spaceId>.
    • Spaces use session key agent:<agentId>:googlechat:group:<spaceId>.
  4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
    • openclaw pairing approve googlechat <code>
  5. Group spaces require @-mention by default. Use botUser if mention detection needs the apps user name.

Targets

Use these identifiers for delivery and allowlists:

  • Direct messages: users/<userId> or users/<email> (email addresses are accepted).
  • Spaces: spaces/<spaceId>.

Config highlights

{
  channels: {
    googlechat: {
      enabled: true,
      serviceAccountFile: "/path/to/service-account.json",
      audienceType: "app-url",
      audience: "https://gateway.example.com/googlechat",
      webhookPath: "/googlechat",
      botUser: "users/1234567890", // optional; helps mention detection
      dm: {
        policy: "pairing",
        allowFrom: ["users/1234567890", "name@example.com"],
      },
      groupPolicy: "allowlist",
      groups: {
        "spaces/AAAA": {
          allow: true,
          requireMention: true,
          users: ["users/1234567890"],
          systemPrompt: "Short answers only.",
        },
      },
      actions: { reactions: true },
      typingIndicator: "message",
      mediaMaxMb: 20,
    },
  },
}

Notes:

  • Service account credentials can also be passed inline with serviceAccount (JSON string).
  • Default webhook path is /googlechat if webhookPath isnt 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:

    openclaw config get channels.googlechat
    

    If it returns "Config path not found", add the configuration (see Config highlights).

  2. Plugin not enabled: Check plugin status:

    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:

    openclaw gateway restart
    

Verify the channel is running:

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:

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 grammYs 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:<agentId>:<mainKey>); groups use agent:<agentId>:telegram:group:<chatId>; replies route back to the same channel.
  • Config knobs: channels.telegram.botToken, channels.telegram.dmPolicy, channels.telegram.groups (allowlist + mention defaults), channels.telegram.allowFrom, channels.telegram.groupAllowFrom, channels.telegram.groupPolicy, channels.telegram.mediaMaxMb, channels.telegram.linkPreview, channels.telegram.proxy, channels.telegram.webhookSecret, channels.telegram.webhookUrl.
  • Draft streaming: optional channels.telegram.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 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:

{
  channels: {
    imessage: {
      enabled: true,
      cliPath: "/usr/local/bin/imsg",
      dbPath: "/Users/<you>/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:<agentId>:imessage:group:<chat_id>).
  • 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:

{
  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 <bot-macos-user>@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 <bot-macos-user> with your actual macOS username:

#!/usr/bin/env bash
set -euo pipefail

# Run an interactive SSH once first to accept host keys:
#   ssh <bot-macos-user>@localhost true
exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T <bot-macos-user>@localhost \
  "/usr/local/bin/imsg" "$@"

Example config:

{
  channels: {
    imessage: {
      enabled: true,
      accounts: {
        bot: {
          name: "Bot",
          enabled: true,
          cliPath: "/path/to/imsg-bot",
          dbPath: "/Users/<bot-macos-user>/Library/Messages/chat.db",
        },
      },
    },
  },
}

For single-account setups, use flat options (channels.imessage.cliPath, channels.imessage.dbPath) instead of the accounts map.

Remote/SSH variant (optional)

If you want iMessage on another Mac, set channels.imessage.cliPath to a wrapper that runs imsg on the remote macOS host over SSH. OpenClaw only needs stdio.

Example wrapper:

#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"

Remote attachments: When cliPath points to a remote host via SSH, attachment paths in the Messages database reference files on the remote machine. OpenClaw can automatically fetch these over SCP by setting channels.imessage.remoteHost:

{
  channels: {
    imessage: {
      cliPath: "~/imsg-ssh", // SSH wrapper to remote Mac
      remoteHost: "user@gateway-host", // for SCP file transfer
      includeAttachments: true,
    },
  },
}

If remoteHost is not set, OpenClaw attempts to auto-detect it by parsing the SSH command in your wrapper script. Explicit configuration is recommended for reliability.

Remote Mac via Tailscale (example)

If the Gateway runs on a Linux host/VM but iMessage must run on a Mac, Tailscale is the simplest bridge: the Gateway talks to the Mac over the tailnet, runs imsg via SSH, and SCPs attachments back.

Architecture:

┌──────────────────────────────┐          SSH (imsg rpc)          ┌──────────────────────────┐
│ Gateway host (Linux/VM)      │──────────────────────────────────▶│ Mac with Messages + imsg │
│ - openclaw gateway           │          SCP (attachments)        │ - Messages signed in     │
│ - channels.imessage.cliPath  │◀──────────────────────────────────│ - Remote Login enabled   │
└──────────────────────────────┘                                   └──────────────────────────┘
              ▲
              │ Tailscale tailnet (hostname or 100.x.y.z)
              ▼
        user@gateway-host

Concrete config example (Tailscale hostname):

{
  channels: {
    imessage: {
      enabled: true,
      cliPath: "~/.openclaw/scripts/imsg-ssh",
      remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
      includeAttachments: true,
      dbPath: "/Users/bot/Library/Messages/chat.db",
    },
  },
}

Example wrapper (~/.openclaw/scripts/imsg-ssh):

#!/usr/bin/env bash
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"

Notes:

  • Ensure the Mac is signed in to Messages, and Remote Login is enabled.
  • Use SSH keys so ssh bot@mac-mini.tailnet-1234.ts.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 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 <CODE>
  • Pairing is the default token exchange for iMessage DMs. Details: 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:<agentId>:imessage:group:<chat_id> session key)
  • group allowlisting / mention gating behavior

Example:

{
  channels: {
    imessage: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["+15555550123"],
      groups: {
        "42": { requireMention: false },
      },
    },
  },
}

This is useful when you want an isolated personality/model for a specific thread (see Multi-agent routing). For filesystem isolation, see Sandboxing.

Media + limits

  • Optional attachment ingestion via channels.imessage.includeAttachments.
  • Media cap via channels.imessage.mediaMaxMb.

Limits

  • Outbound text is chunked to channels.imessage.textChunkLimit (default 4000).
  • Optional newline chunking: set channels.imessage.chunkMode="newline" to split on blank lines (paragraph boundaries) before length chunking.
  • Media uploads are capped by channels.imessage.mediaMaxMb (default 16).

Addressing / delivery targets

Prefer chat_id for stable routing:

  • chat_id:123 (preferred)
  • chat_guid:...
  • chat_identifier:...
  • direct handles: imessage:+1555 / sms:+1555 / user@example.com

List chats:

imsg chats --limit 20

Configuration reference (iMessage)

Full configuration: Configuration

Provider options:

  • channels.imessage.enabled: enable/disable channel startup.
  • channels.imessage.cliPath: path 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["<handle>"].historyLimit.
  • channels.imessage.groups: per-group defaults + allowlist (use "*" for global defaults).
  • channels.imessage.includeAttachments: ingest attachments into context.
  • channels.imessage.mediaMaxMb: inbound/outbound media cap (MB).
  • channels.imessage.textChunkLimit: outbound chunk size (chars).
  • channels.imessage.chunkMode: length (default) 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 — Most popular; uses Baileys and requires QR pairing.
  • Telegram — Bot API via grammY; supports groups.
  • Discord — Discord Bot API + Gateway; supports servers, channels, and DMs.
  • Slack — Bolt SDK; workspace apps.
  • Feishu — Feishu/Lark bot via WebSocket (plugin, installed separately).
  • Google Chat — Google Chat API app via HTTP webhook.
  • Mattermost — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
  • Signal — signal-cli; privacy-focused.
  • BlueBubblesRecommended for iMessage; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
  • iMessage (legacy) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
  • Microsoft Teams — Bot Framework; enterprise support (plugin, installed separately).
  • LINE — LINE Messaging API bot (plugin, installed separately).
  • Nextcloud Talk — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
  • Matrix — Matrix protocol (plugin, installed separately).
  • Nostr — Decentralized DMs via NIP-04 (plugin, installed separately).
  • Tlon — Urbit-based messenger (plugin, installed separately).
  • Twitch — Twitch chat via IRC connection (plugin, installed separately).
  • Zalo — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
  • Zalo Personal — Zalo personal account via QR login (plugin, installed separately).
  • WebChat — Gateway WebChat UI over WebSocket.

Notes

  • Channels can run simultaneously; configure multiple and OpenClaw will route per chat.
  • Fastest setup is usually Telegram (simple bot token). WhatsApp requires QR pairing and stores more state on disk.
  • Group behavior varies by channel; see Groups.
  • DM pairing and allowlists are enforced for safety; see Security.
  • Telegram internals: grammY notes.
  • Troubleshooting: Channel troubleshooting.
  • Model providers are documented separately; see Model Providers.

LINE

Source: https://docs.openclaw.ai/channels/line

LINE (plugin)

LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webhook receiver on the gateway and uses your channel access token + channel secret for authentication.

Status: supported via plugin. Direct messages, group chats, media, locations, Flex messages, template messages, and quick replies are supported. Reactions and threads are not supported.

Plugin required

Install the LINE plugin:

openclaw plugins install @openclaw/line

Local checkout (when running from a git repo):

openclaw plugins install ./extensions/line

Setup

  1. Create a LINE Developers account and open the 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 LINEs webhook verification (GET) and inbound events (POST). If you need a custom path, set channels.line.webhookPath or channels.line.accounts.<id>.webhookPath and update the URL accordingly.

Configure

Minimal config:

{
  channels: {
    line: {
      enabled: true,
      channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
      channelSecret: "LINE_CHANNEL_SECRET",
      dmPolicy: "pairing",
    },
  },
}

Env vars (default account only):

  • LINE_CHANNEL_ACCESS_TOKEN
  • LINE_CHANNEL_SECRET

Token/secret files:

{
  channels: {
    line: {
      tokenFile: "/path/to/line-token.txt",
      secretFile: "/path/to/line-secret.txt",
    },
  },
}

Multiple accounts:

{
  channels: {
    line: {
      accounts: {
        marketing: {
          channelAccessToken: "...",
          channelSecret: "...",
          webhookPath: "/line/marketing",
        },
      },
    },
  },
}

Access control

Direct messages default to pairing. Unknown senders get a pairing code and their messages are ignored until approved.

openclaw pairing list line
openclaw pairing approve line <CODE>

Allowlists and policies:

  • channels.line.dmPolicy: pairing | allowlist | open | 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.<groupId>.allowFrom

LINE IDs are case-sensitive. Valid IDs look like:

  • User: U + 32 hex chars
  • Group: C + 32 hex chars
  • Room: R + 32 hex chars

Message behavior

  • Text is chunked at 5000 characters.
  • Markdown formatting is stripped; code blocks and tables are converted into Flex cards when possible.
  • Streaming responses are buffered; LINE receives full chunks with a loading animation while the agent works.
  • Media downloads are capped by channels.line.mediaMaxMb (default 10).

Channel data (rich messages)

Use channelData.line to send quick replies, locations, Flex cards, or template messages.

{
  text: "Here you go",
  channelData: {
    line: {
      quickReplies: ["Status", "Help"],
      location: {
        title: "Office",
        address: "123 Main St",
        latitude: 35.681236,
        longitude: 139.767125,
      },
      flexMessage: {
        altText: "Status card",
        contents: {
          /* Flex payload */
        },
      },
      templateMessage: {
        type: "confirm",
        text: "Proceed?",
        confirmLabel: "Yes",
        confirmData: "yes",
        cancelLabel: "No",
        cancelData: "no",
      },
    },
  },
}

The LINE plugin also ships a /card command for Flex message presets:

/card info "Welcome" "Thanks for joining!"

Troubleshooting

  • Webhook verification fails: ensure the webhook URL is HTTPS and the 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):

openclaw plugins install @openclaw/matrix

Local checkout (when running from a git repo):

openclaw plugins install ./extensions/matrix

If you choose Matrix during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.

Details: Plugins

Setup

  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:

  3. Get an access token for the bot account:

    • Use the Matrix login API with curl at your home server:
    curl --request POST \
      --url https://matrix.example.org/_matrix/client/v3/login \
      --header 'Content-Type: application/json' \
      --data '{
      "type": "m.login.password",
      "identifier": {
        "type": "m.id.user",
        "user": "your-user-name"
      },
      "password": "your-password"
    }'
    
    • Replace matrix.example.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/). Beeper requires E2EE, so set channels.matrix.encryption: true and verify the device.

Minimal config (access token, user ID auto-fetched):

{
  channels: {
    matrix: {
      enabled: true,
      homeserver: "https://matrix.example.org",
      accessToken: "syt_***",
      dm: { policy: "pairing" },
    },
  },
}

E2EE config (end to end encryption enabled):

{
  channels: {
    matrix: {
      enabled: true,
      homeserver: "https://matrix.example.org",
      accessToken: "syt_***",
      encryption: true,
      dm: { policy: "pairing" },
    },
  },
}

Encryption (E2EE)

End-to-end encryption is supported via the Rust crypto SDK.

Enable with channels.matrix.encryption: true:

  • If the crypto module loads, encrypted rooms are decrypted automatically.
  • Outbound media is encrypted when sending to encrypted rooms.
  • On first connection, OpenClaw requests device verification from your other sessions.
  • Verify the device in another Matrix client (Element, etc.) to enable key sharing.
  • If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt; OpenClaw logs a warning.
  • If you see missing crypto module errors (for example, @matrix-org/matrix-sdk-crypto-nodejs-*), allow build scripts for @matrix-org/matrix-sdk-crypto-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/<account>/<homeserver>__<user>/<token-hash>/crypto/ (SQLite database). Sync state lives alongside it in bot-storage.json. If the access token (device) changes, a new store is created and the bot must be re-verified for encrypted rooms.

Device verification: When E2EE is enabled, the bot will request verification from your other sessions on startup. Open Element (or another client) and approve the verification request to establish trust. Once verified, the bot can decrypt messages in encrypted rooms.

Routing model

  • Replies always go back to Matrix.
  • DMs share the agent's main session; rooms map to group sessions.

Access control (DMs)

  • Default: channels.matrix.dm.policy = "pairing". Unknown senders get a pairing code.
  • Approve via:
    • openclaw pairing list matrix
    • openclaw pairing approve matrix <CODE>
  • 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):
{
  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

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 for product details and downloads.

Plugin required

Mattermost ships as a plugin and is not bundled with the core install.

Install via CLI (npm registry):

openclaw plugins install @openclaw/mattermost

Local checkout (when running from a git repo):

openclaw plugins install ./extensions/mattermost

If you choose Mattermost during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.

Details: Plugins

Quick setup

  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:

{
  channels: {
    mattermost: {
      enabled: true,
      botToken: "mm-token",
      baseUrl: "https://chat.example.com",
      dmPolicy: "pairing",
    },
  },
}

Environment variables (default account)

Set these on the gateway host if you prefer env vars:

  • MATTERMOST_BOT_TOKEN=...
  • MATTERMOST_URL=https://chat.example.com

Env vars apply only to the default account (default). Other accounts must use config values.

Chat modes

Mattermost responds to DMs automatically. Channel behavior is controlled by chatmode:

  • oncall (default): respond only when @mentioned in channels.
  • onmessage: respond to every channel message.
  • onchar: respond when a message starts with a trigger prefix.

Config example:

{
  channels: {
    mattermost: {
      chatmode: "onchar",
      oncharPrefixes: [">", "!"],
    },
  },
}

Notes:

  • 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 <CODE>
  • 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:<id> for a channel
  • user:<id> 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:

{
  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). Polls are sent via Adaptive Cards.

Plugin required

Microsoft Teams ships as a plugin and is not bundled with the core install.

Breaking change (2026.1.15): MS Teams moved out of core. If you use it, you must install the plugin.

Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.

Install via CLI (npm registry):

openclaw plugins install @openclaw/msteams

Local checkout (when running from a git repo):

openclaw plugins install ./extensions/msteams

If you choose Teams during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically.

Details: Plugins

Quick setup (beginner)

  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:

{
  channels: {
    msteams: {
      enabled: true,
      appId: "<APP_ID>",
      appPassword: "<APP_PASSWORD>",
      tenantId: "<TENANT_ID>",
      webhook: { port: 3978, path: "/api/messages" },
    },
  },
}

Note: group chats are blocked by default (channels.msteams.groupPolicy: "allowlist"). To allow group replies, set channels.msteams.groupAllowFrom (or use groupPolicy: "open" to allow any member, mention-gated).

Goals

  • Talk to OpenClaw via Teams DMs, group chats, or channels.
  • Keep routing deterministic: replies always go back to the channel they arrived on.
  • Default to safe channel behavior (mentions required unless configured otherwise).

Config writes

By default, Microsoft Teams is allowed to write config updates triggered by /config set|unset (requires commands.config: true).

Disable with:

{
  channels: { msteams: { configWrites: false } },
}

Access control (DMs + groups)

DM access

  • Default: channels.msteams.dmPolicy = "pairing". Unknown senders are ignored until approved.
  • channels.msteams.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 mentiongated by default).
  • To allow no channels, set channels.msteams.groupPolicy: "disabled".

Example:

{
  channels: {
    msteams: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["user@org.com"],
    },
  },
}

Teams + channel allowlist

  • Scope group/channel replies by listing teams and channels under channels.msteams.teams.
  • Keys can be team IDs or names; channel keys can be conversation IDs or names.
  • When groupPolicy="allowlist" and a teams allowlist is present, only listed teams/channels are accepted (mentiongated).
  • 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:

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

  1. Click Review + createCreate (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 & secretsNew 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 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

ngrok http 3978
# Copy the https URL, e.g., https://abc123.ngrok.io
# Set messaging endpoint to: https://abc123.ngrok.io/api/messages

Option B: Tailscale Funnel

tailscale funnel 3978
# Use your Tailscale funnel URL as the messaging endpoint

Teams Developer Portal (Alternative)

Instead of manually creating a manifest ZIP, you can use the Teams Developer Portal:

  1. Click + New app
  2. Fill in basic info (name, description, developer info)
  3. Go to App featuresBot
  4. Select Enter a bot ID manually and paste your Azure Bot App ID
  5. Check scopes: Personal, Team, Group Chat
  6. Click DistributeDownload app package
  7. In Teams: AppsManage your appsUpload 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 = <App ID>.
    • 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

    {
      "msteams": {
        "enabled": true,
        "appId": "<APP_ID>",
        "appPassword": "<APP_PASSWORD>",
        "tenantId": "<TENANT_ID>",
        "webhook": { "port": 3978, "path": "/api/messages" }
      }
    }
    

    You can also use environment variables instead of config keys:

    • MSTEAMS_APP_ID
    • MSTEAMS_APP_PASSWORD
    • MSTEAMS_TENANT_ID
  5. Bot endpoint

    • Set the Azure Bot Messaging Endpoint to:
      • https://<host>: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["<user_id>"].historyLimit.

Current Teams RSC Permissions (Manifest)

These are the existing resourceSpecific permissions in our Teams app manifest. They only apply inside the team/chat where the app is installed.

For channels (team scope):

  • ChannelMessage.Read.Group (Application) - receive all channel messages without @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.

{
  "$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.01.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).
  • channels.msteams.teams.<teamId>.replyStyle: per-team override.
  • channels.msteams.teams.<teamId>.requireMention: per-team override.
  • channels.msteams.teams.<teamId>.tools: default per-team tool policy overrides (allow/deny/alsoAllow) used when a channel override is missing.
  • channels.msteams.teams.<teamId>.toolsBySender: default per-team per-sender tool policy overrides ("*" wildcard supported).
  • channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle: per-channel override.
  • channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention: per-channel override.
  • channels.msteams.teams.<teamId>.channels.<conversationId>.tools: per-channel tool policy overrides (allow/deny/alsoAllow).
  • channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender: per-channel per-sender tool policy overrides ("*" wildcard supported).
  • channels.msteams.sharePointSiteId: SharePoint site ID for file uploads in group chats/channels (see Sending files in group chats).

Routing & Sessions

  • Session keys follow the standard agent format (see /concepts/session):
    • Direct messages share the main session (agent:<agentId>:<mainKey>).
    • Channel/group messages use conversation id:
      • agent:<agentId>:msteams:channel:<conversationId>
      • agent:<agentId>:msteams:group:<conversationId>

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:

{
  "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:

    # 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:

    {
      channels: {
        msteams: {
          // ... other config ...
          sharePointSiteId: "contoso.sharepoint.com,guid1,guid2",
        },
      },
    }
    

Sharing behavior

Permission Sharing behavior
Sites.ReadWrite.All only Organization-wide sharing link (anyone in org can access)
Sites.ReadWrite.All + Chat.Read.All Per-user sharing link (only chat members can access)

Per-user sharing is more secure as only the chat participants can access the file. If Chat.Read.All permission is missing, the bot falls back to organization-wide sharing.

Fallback behavior

Scenario Result
Group chat + file + sharePointSiteId configured Upload to SharePoint, send sharing link
Group chat + file + no sharePointSiteId Attempt OneDrive upload (may fail), send text only
Personal chat + file FileConsentCard flow (works without SharePoint)
Any context + image Base64-encoded inline (works without SharePoint)

Files stored location

Uploaded files are stored in a /OpenClawShared/ folder in the configured SharePoint site's default document library.

Polls (Adaptive Cards)

OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).

  • CLI: openclaw message poll --channel msteams --target conversation:<id> ...
  • Votes are recorded by the gateway in ~/.openclaw/msteams-polls.json.
  • The gateway must stay online to record votes.
  • Polls do not auto-post result summaries yet (inspect the store file if needed).

Adaptive Cards (arbitrary)

Send any Adaptive Card JSON to Teams users or conversations using the message tool or CLI.

The card parameter accepts an Adaptive Card JSON object. When card is provided, the message text is optional.

Agent tool:

{
  "action": "send",
  "channel": "msteams",
  "target": "user:<id>",
  "card": {
    "type": "AdaptiveCard",
    "version": "1.5",
    "body": [{ "type": "TextBlock", "text": "Hello!" }]
  }
}

CLI:

openclaw message send --channel msteams \
  --target "conversation:19:abc...@thread.tacv2" \
  --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'

See Adaptive Cards documentation for card schema and examples. For target format details, see Target formats below.

Target formats

MSTeams targets use prefixes to distinguish between users and conversations:

Target type Format Example
User (by ID) user:<aad-object-id> user:40a1a0ed-4ff2-4164-a219-55518990c197
User (by name) user:<display-name> user:John Smith (requires Graph API)
Group/channel conversation:<conversation-id> conversation:19:abc123...@thread.tacv2
Group/channel (raw) <conversation-id> 19:abc123...@thread.tacv2 (if contains @thread)

CLI examples:

# Send to a user by ID
openclaw message send --channel msteams --target "user:40a1a0ed-..." --message "Hello"

# Send to a user by display name (triggers Graph API lookup)
openclaw message send --channel msteams --target "user:John Smith" --message "Hello"

# Send to a group chat or channel
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"

# Send an Adaptive Card to a conversation
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
  --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'

Agent tool examples:

{
  "action": "send",
  "channel": "msteams",
  "target": "user:John Smith",
  "message": "Hello!"
}
{
  "action": "send",
  "channel": "msteams",
  "target": "conversation:19:abc...@thread.tacv2",
  "card": {
    "type": "AdaptiveCard",
    "version": "1.5",
    "body": [{ "type": "TextBlock", "text": "Hello" }]
  }
}

Note: Without the user: prefix, names default to group/team resolution. Always use user: when targeting people by display name.

Proactive messaging

  • Proactive messages are only possible after a user has interacted, because we store conversation references at that point.
  • See /gateway/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 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

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:

{
  channels: {
    signal: {
      enabled: true,
      account: "+15551234567",
      cliPath: "signal-cli",
      dmPolicy: "pairing",
      allowFrom: ["+15557654321"],
    },
  },
}

What it is

  • Signal channel via signal-cli (not embedded libsignal).
  • Deterministic routing: replies always go back to Signal.
  • DMs share the agent's main session; groups are isolated (agent:<agentId>:signal:group:<groupId>).

Config writes

By default, Signal is allowed to write config updates triggered by /config set|unset (requires commands.config: true).

Disable with:

{
  channels: { signal: { configWrites: false } },
}

The number model (important)

  • The gateway connects to a Signal device (the signal-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:

{
  channels: {
    signal: {
      enabled: true,
      account: "+15551234567",
      cliPath: "signal-cli",
      dmPolicy: "pairing",
      allowFrom: ["+15557654321"],
    },
  },
}

Multi-account support: use channels.signal.accounts with per-account config and optional name. See gateway/configuration for the shared pattern.

External daemon mode (httpUrl)

If you want to manage signal-cli yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point OpenClaw at it:

{
  channels: {
    signal: {
      httpUrl: "http://127.0.0.1:8080",
      autoStart: false,
    },
  },
}

This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set channels.signal.startupTimeoutMs.

Access control (DMs + groups)

DMs:

  • Default: channels.signal.dmPolicy = "pairing".
  • Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
  • Approve via:
    • openclaw pairing list signal
    • openclaw pairing approve signal <CODE>
  • Pairing is the default token exchange for Signal DMs. Details: Pairing
  • UUID-only senders (from sourceUuid) are stored as uuid:<id> 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:<id> from pairing output; bare UUID works too).
  • messageId is the Signal timestamp for the message youre 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:<groupId> targetAuthor=uuid:<sender-uuid> messageId=1737630212345 emoji=✅

Config:

  • channels.signal.actions.reactions: enable/disable reaction actions (default true).
  • channels.signal.reactionLevel: off | ack | minimal | extensive.
    • off/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.<id>.actions.reactions, channels.signal.accounts.<id>.reactionLevel.

Delivery targets (CLI/cron)

  • DMs: signal:+15551234567 (or plain E.164).
  • UUID DMs: uuid:<id> (or bare UUID).
  • Groups: signal:group:<groupId>.
  • Usernames: username:<name> (if supported by your Signal account).

Configuration reference (Signal)

Full configuration: Configuration

Provider options:

  • channels.signal.enabled: enable/disable channel startup.
  • channels.signal.account: E.164 for the bot account.
  • channels.signal.cliPath: path 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:<id>). 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["<phone_or_uuid>"].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:

{
  channels: {
    slack: {
      enabled: true,
      appToken: "xapp-...",
      botToken: "xoxb-...",
    },
  },
}

Setup

  1. Create a Slack app (From scratch) in https://api.slack.com/apps.
  2. Socket Mode → toggle on. Then go to Basic InformationApp-Level TokensGenerate 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 for the shared pattern.

OpenClaw config (minimal)

Set tokens via env vars (recommended):

  • SLACK_APP_TOKEN=xapp-...
  • SLACK_BOT_TOKEN=xoxb-...

Or via config:

{
  channels: {
    slack: {
      enabled: true,
      appToken: "xapp-...",
      botToken: "xoxb-...",
    },
  },
}

User token (optional)

OpenClaw can use a Slack user token (xoxp-...) for read operations (history, pins, reactions, emoji, member info). By default this stays read-only: reads prefer the user token when present, and writes still use the bot token unless you explicitly opt in. Even with userTokenReadOnly: false, the bot token stays preferred for writes when it is available.

User tokens are configured in the config file (no env var support). For multi-account, set channels.slack.accounts.<id>.userToken.

Example with bot + app + user tokens:

{
  channels: {
    slack: {
      enabled: true,
      appToken: "xapp-...",
      botToken: "xoxb-...",
      userToken: "xoxp-...",
    },
  },
}

Example with userTokenReadOnly explicitly set (allow user token writes):

{
  channels: {
    slack: {
      enabled: true,
      appToken: "xapp-...",
      botToken: "xoxb-...",
      userToken: "xoxp-...",
      userTokenReadOnly: false,
    },
  },
}

Token usage

  • Read operations (history, reactions list, pins list, emoji list, member info, search) prefer the user token when configured, otherwise the bot token.
  • Write operations (send/edit/delete messages, add/remove reactions, pin/unpin, file uploads) use the bot token by default. If userTokenReadOnly: 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)

{
  channels: {
    slack: {
      enabled: true,
      mode: "http",
      botToken: "xoxb-...",
      signingSecret: "your-signing-secret",
      webhookPath: "/slack/events",
    },
  },
}

Multi-account HTTP mode: set channels.slack.accounts.<id>.mode = "http" and provide a unique webhookPath per account so each Slack app can point to its own URL.

Manifest (optional)

Use this Slack app manifest to create the app quickly (adjust the name/command if you want). Include the user scopes if you plan to configure a user token.

{
  "display_information": {
    "name": "OpenClaw",
    "description": "Slack connector for OpenClaw"
  },
  "features": {
    "bot_user": {
      "display_name": "OpenClaw",
      "always_online": false
    },
    "app_home": {
      "messages_tab_enabled": true,
      "messages_tab_read_only_enabled": false
    },
    "slash_commands": [
      {
        "command": "/openclaw",
        "description": "Send a message to OpenClaw",
        "should_escape": false
      }
    ]
  },
  "oauth_config": {
    "scopes": {
      "bot": [
        "chat:write",
        "channels:history",
        "channels:read",
        "groups:history",
        "groups:read",
        "groups:write",
        "im:history",
        "im:read",
        "im:write",
        "mpim:history",
        "mpim:read",
        "mpim:write",
        "users:read",
        "app_mentions:read",
        "reactions:read",
        "reactions:write",
        "pins:read",
        "pins:write",
        "emoji:read",
        "commands",
        "files:read",
        "files:write"
      ],
      "user": [
        "channels:history",
        "channels:read",
        "groups:history",
        "groups:read",
        "im:history",
        "im:read",
        "mpim:history",
        "mpim:read",
        "users:read",
        "reactions:read",
        "pins:read",
        "emoji:read",
        "search:read"
      ]
    }
  },
  "settings": {
    "socket_mode_enabled": true,
    "event_subscriptions": {
      "bot_events": [
        "app_mention",
        "message.channels",
        "message.groups",
        "message.im",
        "message.mpim",
        "reaction_added",
        "reaction_removed",
        "member_joined_channel",
        "member_left_channel",
        "channel_rename",
        "pin_added",
        "pin_removed"
      ]
    }
  }
}

If you enable native commands, add one slash_commands entry per command you want to expose (matching the /help list). Override with channels.slack.commands.native.

Scopes (current vs optional)

Slack's Conversations API is type-scoped: you only need the scopes for the conversation types you actually touch (channels, groups, im, mpim). See https://docs.slack.dev/apis/web-api/using-the-conversations-api/ for the overview.

Bot token scopes (required)

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)

Config

Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:

{
  "slack": {
    "enabled": true,
    "botToken": "xoxb-...",
    "appToken": "xapp-...",
    "groupPolicy": "allowlist",
    "dm": {
      "enabled": true,
      "policy": "pairing",
      "allowFrom": ["U123", "U456", "*"],
      "groupEnabled": false,
      "groupChannels": ["G123"],
      "replyToMode": "all"
    },
    "channels": {
      "C123": { "allow": true, "requireMention": true },
      "#general": {
        "allow": true,
        "requireMention": true,
        "users": ["U123"],
        "skills": ["search", "docs"],
        "systemPrompt": "Keep answers short."
      }
    },
    "reactionNotifications": "own",
    "reactionAllowlist": ["U123"],
    "replyToMode": "off",
    "actions": {
      "reactions": true,
      "messages": true,
      "pins": true,
      "memberInfo": true,
      "emojiList": true
    },
    "slashCommand": {
      "enabled": true,
      "name": "openclaw",
      "sessionPrefix": "slack:slash",
      "ephemeral": true
    },
    "textChunkLimit": 4000,
    "mediaMaxMb": 20
  }
}

Tokens can also be supplied via env vars:

  • SLACK_BOT_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:

{
  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.<chatType>
  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:

{
  channels: {
    slack: {
      replyToMode: "off",
      replyToModeByChatType: { direct: "all" },
    },
  },
}

Thread group DMs but keep channels in the root:

{
  channels: {
    slack: {
      replyToMode: "off",
      replyToModeByChatType: { group: "first" },
    },
  },
}

Make channels thread, keep DMs in the root:

{
  channels: {
    slack: {
      replyToMode: "first",
      replyToModeByChatType: { direct: "off", group: "off" },
    },
  },
}

Manual threading tags

For fine-grained control, use these tags in agent responses:

  • [[reply_to_current]] — reply to the triggering message (start/continue thread).
  • [[reply_to:<id>]] — reply to a specific message id.

Sessions + routing

  • DMs share the main session (like WhatsApp/Telegram).
  • Channels map to agent:<agentId>:slack:channel:<channelId> sessions.
  • Slash commands use agent:<agentId>:slack:slash:<userId> sessions (prefix configurable via channels.slack.slashCommand.sessionPrefix).
  • If Slack doesnt 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

DM security (pairing)

  • Default: channels.slack.dm.policy="pairing" — unknown DM senders get a pairing code (expires after 1 hour).
  • Approve via: openclaw pairing approve slack <code>.
  • To allow anyone: set channels.slack.dm.policy="open" 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.<id> or channels.slack.channels.<name>):

  • 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:<id> for DMs
  • channel:<id> for channels

Tool actions

Slack tool actions can be gated with channels.slack.actions.*:

Action group Default Notes
reactions enabled React + list reactions
messages enabled Read/send/edit/delete
pins enabled Pin/unpin/list
memberInfo enabled Member info
emojiList enabled Custom emoji list

Security notes

  • Writes default to the bot token so state-changing actions stay scoped to the app's bot permissions and identity.
  • Setting userTokenReadOnly: 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.<id>.allowBots.
  • Warning: If you allow replies to other bots (channels.slack.allowBots=true or channels.slack.channels.<id>.allowBots=true), prevent bot-to-bot reply loops with requireMention, channels.slack.channels.<id>.users allowlists, and/or clear guardrails in AGENTS.md and SOUL.md.
  • For the Slack tool, reaction removal semantics are in /tools/reactions.
  • Attachments are downloaded to the media store when permitted and under the size limit.

Telegram

Source: https://docs.openclaw.ai/channels/telegram

Telegram (Bot API)

Status: production-ready for bot DMs + groups via grammY. Long-polling by default; webhook optional.

Quick setup (beginner)

  1. Create a bot with @BotFather (direct link). 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:

{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
    },
  },
}

What it is

  • A Telegram Bot API channel owned by the Gateway.
  • Deterministic routing: replies go back to Telegram; the model never chooses channels.
  • DMs share the agent's main session; groups stay isolated (agent:<agentId>:telegram:group:<chatId>).

Setup (fast path)

1) Create a bot token (BotFather)

  1. Open Telegram and chat with @BotFather (direct link). 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:

{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
      groups: { "*": { requireMention: true } },
    },
  },
}

Env option: TELEGRAM_BOT_TOKEN=... (works for the default account). If both env and config are set, config takes precedence.

Multi-account support: use channels.telegram.accounts with per-account tokens and optional name. See gateway/configuration for the shared pattern.

  1. Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
  2. DM access defaults to pairing. Approve the code when the bot is first contacted.
  3. 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 + readding 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" (Telegrams 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 Telegrams bot menu on startup. You can add custom commands to the menu via config:

{
  channels: {
    telegram: {
      customCommands: [
        { command: "backup", description: "Git backup" },
        { command: "generate", description: "Create an image" },
      ],
    },
  },
}

Troubleshooting

  • setMyCommands 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.

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, _ (132 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["<user_id>"].historyLimit.

Group activation modes

By default, the bot only responds to mentions in groups (@botname or patterns in agents.list[].groupChat.mentionPatterns). To change this behavior:

{
  channels: {
    telegram: {
      groups: {
        "-1001234567890": { requireMention: false }, // always respond in this group
      },
    },
  },
}

Important: Setting channels.telegram.groups creates an allowlist - only listed groups (or "*") will be accepted. Forum topics inherit their parent group config (allowFrom, requireMention, skills, prompts) unless you add per-topic overrides under channels.telegram.groups.<groupId>.topics.<topicId>.

To allow all groups with always-respond:

{
  channels: {
    telegram: {
      groups: {
        "*": { requireMention: false }, // all groups, always respond
      },
    },
  },
}

To keep mention-only for all groups (default behavior):

{
  channels: {
    telegram: {
      groups: {
        "*": { requireMention: true }, // or omit groups entirely
      },
    },
  },
}

Via command (session-level)

Send in the group:

  • /activation always - respond to all messages
  • /activation mention - require mentions (default)

Note: Commands update session state only. For persistent behavior across restarts, use config.

Getting the group chat ID

Forward any message from the group to @userinfobot or @getidsbot on Telegram to see the chat ID (negative number like -1001234567890).

Tip: For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use /whoami once commands are enabled.

Privacy note: @userinfobot is a third-party bot. If you prefer, add the bot to the group, send a message, and use openclaw logs --follow to read chat.id, or use the Bot API getUpdates.

Config writes

By default, Telegram is allowed to write config updates triggered by channel events or /config set|unset.

This happens when:

  • A group is upgraded to a supergroup and Telegram emits migrate_to_chat_id (chat ID changes). OpenClaw can migrate channels.telegram.groups automatically.
  • You run /config set or /config unset in a Telegram chat (requires commands.config: true).

Disable with:

{
  channels: { telegram: { configWrites: false } },
}

Topics (forum supergroups)

Telegram forum topics include a message_thread_id per message. OpenClaw:

  • Appends :topic:<threadId> to the Telegram group session key so each topic is isolated.
  • Sends typing indicators and replies with message_thread_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.<chatId>.topics.<threadId> (skills, allowlists, auto-reply, system prompts, disable).
  • Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.

Private chats can include message_thread_id in some edge cases. OpenClaw keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.

Inline Buttons

Telegram supports inline keyboards with callback buttons.

{
  channels: {
    telegram: {
      capabilities: {
        inlineButtons: "allowlist",
      },
    },
  },
}

For per-account configuration:

{
  channels: {
    telegram: {
      accounts: {
        main: {
          capabilities: {
            inlineButtons: "allowlist",
          },
        },
      },
    },
  },
}

Scopes:

  • off — inline buttons 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:

{
  action: "send",
  channel: "telegram",
  to: "123456789",
  message: "Choose an option:",
  buttons: [
    [
      { text: "Yes", callback_data: "yes" },
      { text: "No", callback_data: "no" },
    ],
    [{ text: "Cancel", callback_data: "cancel" }],
  ],
}

When a user clicks a button, the callback data is sent back to the agent as a message with the format: callback_data: value

Configuration options

Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported):

  • channels.telegram.capabilities: Global default capability config applied to all Telegram accounts unless overridden.
  • channels.telegram.accounts.<account>.capabilities: Per-account capabilities that override the global defaults for that specific account.

Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups).

Access control (DMs + groups)

DM access

  • Default: channels.telegram.dmPolicy = "pairing". Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
  • Approve via:
    • openclaw pairing list telegram
    • openclaw pairing approve telegram <CODE>
  • Pairing is the default token exchange used for Telegram DMs. Details: Pairing
  • channels.telegram.allowFrom accepts numeric user IDs (recommended) or @username entries. It is not the bot username; use the human senders 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:
    curl "https://api.telegram.org/bot<bot_token>/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:<id>]] -- reply to a specific message id.

Controlled by channels.telegram.replyToMode:

  • first (default), all, off.

Audio messages (voice vs file)

Telegram distinguishes voice notes (round bubble) from audio files (metadata card). OpenClaw defaults to audio files for backward compatibility.

To force a voice note bubble in agent replies, include this tag anywhere in the reply:

  • [[audio_as_voice]] — send audio as a voice note instead of a file.

The tag is stripped from the delivered text. Other channels ignore this tag.

For message tool sends, set asVoice: true with a voice-compatible audio media URL (message is optional when media is present):

{
  action: "send",
  channel: "telegram",
  to: "123456789",
  media: "https://example.com/voice.ogg",
  asVoice: true,
}

Stickers

OpenClaw supports receiving and sending Telegram stickers with intelligent caching.

Receiving stickers

When a user sends a sticker, OpenClaw handles it based on the sticker type:

  • Static stickers (WEBP): Downloaded and processed through vision. The sticker appears as a <media:sticker> placeholder in the message content.
  • Animated stickers (TGS): Skipped (Lottie format not supported for processing).
  • Video stickers (WEBM): Skipped (video format not supported for processing).

Template context field available when receiving stickers:

  • Sticker — object with:
    • emoji — emoji associated with the 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:

{
  "fileId": "CAACAgIAAxkBAAI...",
  "fileUniqueId": "AgADBAADb6cxG2Y",
  "emoji": "👋",
  "setName": "CoolCats",
  "description": "A cartoon cat waving enthusiastically",
  "cachedAt": "2026-01-15T10:30:00.000Z"
}

Benefits:

  • Reduces API costs by avoiding repeated vision calls for the same sticker
  • Faster response times for cached stickers (no vision processing delay)
  • Enables sticker search functionality based on cached descriptions

The cache is populated automatically as stickers are received. There is no manual cache management required.

Sending stickers

The agent can send and search stickers using the sticker and sticker-search actions. These are disabled by default and must be enabled in config:

{
  channels: {
    telegram: {
      actions: {
        sticker: true,
      },
    },
  },
}

Send a sticker:

{
  action: "sticker",
  channel: "telegram",
  to: "123456789",
  fileId: "CAACAgIAAxkBAAI...",
}

Parameters:

  • fileId (required) — the Telegram file ID of the sticker. Obtain this 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:

{
  action: "sticker-search",
  channel: "telegram",
  query: "cat waving",
  limit: 5,
}

Returns matching stickers from the cache:

{
  ok: true,
  count: 2,
  stickers: [
    {
      fileId: "CAACAgIAAxkBAAI...",
      emoji: "👋",
      description: "A cartoon cat waving enthusiastically",
      setName: "CoolCats",
    },
  ],
}

The search uses fuzzy matching across description text, emoji characters, and set names.

Example with threading:

{
  action: "sticker",
  channel: "telegram",
  to: "-1001234567890",
  fileId: "CAACAgIAAxkBAAI...",
  replyTo: 42,
  threadId: 123,
}

Streaming (drafts)

Telegram can stream draft bubbles while the agent is generating a response. OpenClaw uses Bot API sendMessageDraft (not real messages) and then sends the final reply as a normal message.

Requirements (Telegram Bot API 9.3+):

  • Private chats with topics enabled (forum topic mode for the bot).
  • Incoming messages must include message_thread_id (private topic thread).
  • Streaming is ignored for groups/supergroups/channels.

Config:

  • channels.telegram.streamMode: "off" | "partial" | "block" (default: partial)
    • partial: update the draft bubble with the latest streaming text.
    • block: update the draft bubble in larger blocks (chunked).
    • off: disable draft streaming.
  • Optional (only for streamMode: "block"):
    • channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }
      • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped 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.

Retry policy

Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via channels.telegram.retry. See Retry policy.

Agent tool (messages + reactions)

  • Tool: 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.
  • 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:

{
  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 doesnt respond to non-mention messages in a group:

  • If you set channels.telegram.groups.*.requireMention=false, Telegrams Bot API privacy mode must be disabled.
    • BotFather: /setprivacyDisable (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 cant 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

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.<id>.requireMention: mention gating default.
    • channels.telegram.groups.<id>.skills: skill filter (omit = all skills, empty = none).
    • channels.telegram.groups.<id>.allowFrom: per-group sender allowlist override.
    • channels.telegram.groups.<id>.systemPrompt: extra system prompt for the group.
    • channels.telegram.groups.<id>.enabled: disable the group when false.
    • channels.telegram.groups.<id>.topics.<threadId>.*: per-topic overrides (same fields as group).
    • channels.telegram.groups.<id>.topics.<threadId>.requireMention: per-topic mention gating override.
  • channels.telegram.capabilities.inlineButtons: off | dm | group | all | allowlist (default: allowlist).
  • channels.telegram.accounts.<account>.capabilities.inlineButtons: per-account override.
  • channels.telegram.replyToMode: off | first | all (default: first).
  • channels.telegram.textChunkLimit: outbound chunk size (chars).
  • channels.telegram.chunkMode: length (default) 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:

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

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.
  • 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:

{
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551234567"],
    },
  },
}

Goals

  • Multiple WhatsApp accounts (multi-account) in one Gateway process.
  • Deterministic routing: replies return to WhatsApp, no model routing.
  • Model sees enough context to understand quoted replies.

Config writes

By default, WhatsApp is allowed to write config updates triggered by /config set|unset (requires commands.config: true).

Disable with:

{
  channels: { whatsapp: { configWrites: false } },
}

Architecture (who owns what)

  • Gateway owns the Baileys socket and inbox loop.
  • CLI / macOS app talk to the gateway; no direct Baileys use.
  • Active listener is required for outbound sends; otherwise send fails fast.

Getting a phone number (two modes)

WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run OpenClaw on WhatsApp:

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 WiFi and power, and link it via QR.

WhatsApp Business: You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the OpenClaw number there.

Sample config (dedicated number, single-user allowlist):

{
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551234567"],
    },
  },
}

Pairing mode (optional): If you want pairing instead of allowlist, set channels.whatsapp.dmPolicy to pairing. Unknown senders get a pairing code; approve with: openclaw pairing approve whatsapp <code>

Personal number (fallback)

Quick fallback: run OpenClaw on your own number. Message yourself (WhatsApp “Message yourself”) for testing so you dont spam contacts. Expect to read verification codes on your main phone during setup and experiments. Must enable self-chat mode. When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.

Sample config (personal number, self-chat):

{
  "whatsapp": {
    "selfChatMode": true,
    "dmPolicy": "allowlist",
    "allowFrom": ["+15551234567"]
  }
}

Self-chat replies default to [{identity.name}] when set (otherwise [openclaw]) if messages.responsePrefix is unset. Set it explicitly to customize or disable the prefix (use "" to remove it).

Number sourcing tips

  • Local eSIM from your country's mobile carrier (most reliable)
  • Prepaid SIM — cheap, just needs to receive one SMS for verification

Avoid: TextNow, Google Voice, most "free SMS" services — WhatsApp blocks these aggressively.

Tip: The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via creds.json.

Why Not Twilio?

  • Early OpenClaw builds supported Twilios WhatsApp Business integration.
  • WhatsApp Business numbers are a poor fit for a personal assistant.
  • Meta enforces a 24hour reply window; if you havent responded in the last 24 hours, the business number cant initiate new messages.
  • High-volume or “chatty” usage triggers aggressive blocking, because business accounts arent meant to send dozens of personal assistant messages.
  • Result: unreliable delivery and frequent blocks, so support was removed.

Login + credentials

  • Login command: openclaw channels login (QR via Linked Devices).
  • Multi-account login: openclaw channels login --account <id> (<id> = accountId).
  • Default account (when --account is omitted): default if present, otherwise the first configured account id (sorted).
  • Credentials stored in ~/.openclaw/credentials/whatsapp/<accountId>/creds.json.
  • Backup copy at creds.json.bak (restored on corruption).
  • Legacy compatibility: older installs stored Baileys files directly in ~/.openclaw/credentials/.
  • Logout: openclaw channels logout (or --account <id>) deletes WhatsApp auth state (but keeps 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 <code>; 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:

{
  channels: { whatsapp: { sendReadReceipts: false } },
}

Disable per account:

{
  channels: {
    whatsapp: {
      accounts: {
        personal: { sendReadReceipts: false },
      },
    },
  },
}

Notes:

  • Self-chat mode always skips read receipts.

WhatsApp FAQ: sending messages + pairing

Will OpenClaw message random contacts when I link WhatsApp?
No. Default DM policy is pairing, so unknown senders only get a pairing code and their message is not processed. OpenClaw only replies to chats it receives, or to sends you explicitly trigger (agent/CLI).

How does pairing work on WhatsApp?
Pairing is a DM gate for unknown senders:

  • First DM from a new sender returns a short code (message is not processed).
  • Approve with: openclaw pairing approve whatsapp <code> (list 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 agents main session, so use one agent per person. DM access control (dmPolicy/allowFrom) is global per WhatsApp account. See Multi-Agent Routing.

Why do you ask for my phone number in the wizard?
The wizard uses it to set your allowlist/owner so your own DMs are permitted. Its 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]
    <quoted text or <media:...>>
    [/Replying]
    
  • Reply metadata also set:
    • ReplyToId = stanzaId
    • ReplyToBody = quoted body or media placeholder
    • ReplyToSender = E.164 when known
  • Media-only inbound messages use placeholders:
    • <media:image|video|audio|document|sticker>

Groups

  • Groups map to agent:<agentId>:whatsapp:group:<jid> sessions.
  • Group policy: channels.whatsapp.groupPolicy = open|disabled|allowlist (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:

{
  "whatsapp": {
    "ackReaction": {
      "emoji": "👀",
      "direct": true,
      "group": "mentions"
    }
  }
}

Options:

  • emoji (string): Emoji to use for acknowledgment (e.g., "👀", "", "📨"). Empty or omitted = feature disabled.
  • direct (boolean, default: true): Send reactions in direct/DM chats.
  • group (string, default: "mentions"): Group chat behavior:
    • "always": React to all group messages (even without @mention)
    • "mentions": React only when bot is @mentioned
    • "never": Never react in groups

Per-account override:

{
  "whatsapp": {
    "accounts": {
      "work": {
        "ackReaction": {
          "emoji": "✅",
          "direct": false,
          "group": "always"
        }
      }
    }
  }
}

Behavior notes:

  • Reactions are sent immediately upon message receipt, before typing indicators or bot replies.
  • In groups with requireMention: false (activation: always), group: "mentions" will react to all messages (not just @mentions).
  • Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
  • Participant JID is automatically included for group reactions.
  • WhatsApp ignores messages.ackReaction; 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.
  • 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 <mp4> --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.<accountId>.* (per-account settings + optional authDir).
  • channels.whatsapp.accounts.<accountId>.mediaMaxMb (per-account inbound media cap).
  • channels.whatsapp.accounts.<accountId>.ackReaction (per-account ack reaction override).
  • channels.whatsapp.groupAllowFrom (group sender allowlist).
  • channels.whatsapp.groupPolicy (group policy).
  • channels.whatsapp.historyLimit / channels.whatsapp.accounts.<accountId>.historyLimit (group history context; 0 disables).
  • channels.whatsapp.dmHistoryLimit (DM history limit in user turns). Per-user overrides: channels.whatsapp.dms["<phone>"].historyLimit.
  • channels.whatsapp.groups (group allowlist + mention gating defaults; use "*" to allow all)
  • channels.whatsapp.actions.reactions (gate WhatsApp tool reactions).
  • agents.list[].groupChat.mentionPatterns (or messages.groupChat.mentionPatterns)
  • messages.groupChat.historyLimit
  • channels.whatsapp.messagePrefix (inbound prefix; per-account: channels.whatsapp.accounts.<accountId>.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.

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

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:

{
  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 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:

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

  1. Restart the gateway. Zalo starts when a token is resolved (env or config).
  2. 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 <CODE>
  • Pairing is the default token exchange. Details: 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

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.<id>.botToken: per-account token.
  • channels.zalo.accounts.<id>.tokenFile: per-account token file.
  • channels.zalo.accounts.<id>.name: display name.
  • channels.zalo.accounts.<id>.enabled: enable/disable account.
  • channels.zalo.accounts.<id>.dmPolicy: per-account DM policy.
  • channels.zalo.accounts.<id>.allowFrom: per-account allowlist.
  • channels.zalo.accounts.<id>.webhookUrl: per-account webhook URL.
  • channels.zalo.accounts.<id>.webhookSecret: per-account webhook secret.
  • channels.zalo.accounts.<id>.webhookPath: per-account webhook path.
  • channels.zalo.accounts.<id>.proxy: per-account proxy URL.

Zalo Personal

Source: https://docs.openclaw.ai/channels/zalouser

Zalo Personal (unofficial)

Status: experimental. This integration automates a personal Zalo account via zca-cli.

Warning: This is an unofficial integration and may result in account suspension/ban. Use at your own risk.

Plugin required

Zalo Personal ships as a plugin and is not bundled with the core install.

  • Install via CLI: openclaw plugins install @openclaw/zalouser
  • Or from a source checkout: openclaw plugins install ./extensions/zalouser
  • Details: Plugins

Prerequisite: zca-cli

The Gateway machine must have the zca binary available in PATH.

  • Verify: zca --version
  • If missing, install zca-cli (see extensions/zalouser/README.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:
{
  channels: {
    zalouser: {
      enabled: true,
      dmPolicy: "pairing",
    },
  },
}
  1. Restart the Gateway (or finish onboarding).
  2. 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:

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 <code>

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:

{
  channels: {
    zalouser: {
      groupPolicy: "allowlist",
      groups: {
        "123456789": { allow: true },
        "Work Chat": { allow: true },
      },
    },
  },
}

Multi-account

Accounts map to zca profiles. Example:

{
  channels: {
    zalouser: {
      enabled: true,
      defaultAccount: "default",
      accounts: {
        work: { enabled: true, profile: "work" },
      },
    },
  },
}

Troubleshooting

zca not found:

  • Install zca-cli and ensure its on PATH for the Gateway process.

Login doesnt 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 agents only working directory (cwd) for tools and context.

Recommended: use openclaw setup to create ~/.openclaw/openclaw.json if missing and initialize the workspace files.

Full workspace layout + backup guide: Agent workspace

If agents.defaults.sandbox is enabled, non-main sessions can override this with per-session workspaces under agents.defaults.sandbox.workspaceRoot (see Gateway configuration).

Bootstrap files (injected)

Inside agents.defaults.workspace, OpenClaw expects these user-editable files:

  • AGENTS.md — operating instructions + “memory”
  • SOUL.md — persona, boundaries, 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:

{ 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; its guidance for how you want them used.

Skills

OpenClaw loads skills from three locations (workspace wins on name conflict):

  • Bundled (shipped with the install)
  • Managed/local: ~/.openclaw/skills
  • Workspace: <workspace>/skills

Skills can be gated by config/env (see skills in Gateway configuration).

pi-mono integration

OpenClaw reuses pieces of the pi-mono codebase (models/tools), but session management, discovery, and tool wiring are OpenClaw-owned.

  • No pi-coding agent runtime.
  • No ~/.pi/agent or <workspace>/.pi settings are consulted.

Sessions

Session transcripts are stored as JSONL at:

  • ~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl

The session ID is stable and chosen by OpenClaw. Legacy Pi/Tau session folders are not read.

Steering while streaming

When queue mode is steer, inbound messages are injected into the current run. The queue is checked after each tool call; if a queued message is present, remaining tool calls from the current assistant message are skipped (error tool results with "Skipped due to queued user message."), then the queued user message is injected before the next assistant response.

When queue mode is followup or collect, inbound messages are held until the current turn ends, then a new agent turn starts with the queued payloads. See Queue for mode + debounce/cap behavior.

Block streaming sends completed assistant blocks as soon as they finish; it is off by default (agents.defaults.blockStreamingDefault: "off"). Tune the boundary via agents.defaults.blockStreamingBreak (text_end vs message_end; defaults to text_end). Control soft block chunking with agents.defaults.blockStreamingChunk (defaults to 8001200 chars; prefers paragraph breaks, then newlines; sentences last). Coalesce streamed chunks with agents.defaults.blockStreamingCoalesce to reduce single-line spam (idle-based merging before send). Non-Telegram channels require explicit *.blockStreaming: true to enable block replies. Verbose tool summaries are emitted at tool start (no debounce); Control UI streams tool output via agent events when available. More details: Streaming + chunking.

Model refs

Model refs in config (for example agents.defaults.model and agents.defaults.models) are parsed by splitting on the first /.

  • Use provider/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 🦞

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

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 OpenClaws base prompt, skills prompt, bootstrap context, and per-run overrides.
  • Model-specific limits and compaction reserve tokens are enforced.
  • See System prompt for what the model sees.

Hook points (where you can intercept)

OpenClaw has two hook systems:

  • Internal hooks (Gateway hooks): event-driven scripts for commands and lifecycle events.
  • Plugin hooks: extension points inside the agent/tool lifecycle and gateway pipeline.

Internal hooks (Gateway hooks)

  • agent:bootstrap: runs while building bootstrap files before the system prompt is finalized. Use this to add/remove bootstrap context files.
  • Command hooks: /new, /reset, /stop, and other command events (see Hooks doc).

See Hooks for setup and examples.

Plugin hooks (agent + gateway lifecycle)

These run inside the agent loop or gateway pipeline:

  • before_agent_start: inject context or override system prompt before the run starts.
  • agent_end: inspect the final message list and run metadata after completion.
  • before_compaction / after_compaction: observe or annotate compaction cycles.
  • before_tool_call / after_tool_call: intercept tool params/results.
  • tool_result_persist: synchronously transform tool results before they are written to the session transcript.
  • message_received / message_sending / message_sent: inbound + outbound message hooks.
  • session_start / session_end: session lifecycle boundaries.
  • gateway_start / gateway_stop: gateway lifecycle events.

See Plugins for the hook API and registration details.

Streaming + partial replies

  • Assistant deltas are streamed from pi-agent-core and emitted as 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 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 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 (and/or peragent 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-<profile>.
  • Override in ~/.openclaw/openclaw.json:
{
  agent: {
    workspace: "~/.openclaw/workspace",
  },
}

openclaw onboard, openclaw configure, or openclaw setup will create the workspace and seed the bootstrap files if they are missing.

If you already manage the workspace files yourself, you can disable bootstrap file creation:

{ agent: { skipBootstrap: true } }

Extra workspace folders

Older installs may have created ~/openclaw. Keeping multiple workspace directories around can cause confusing auth or state drift, because only one workspace is active at a time.

Recommendation: keep a single active workspace. If you no longer use the extra folders, archive or move them to Trash (for example trash ~/openclaw). If you intentionally keep multiple workspaces, make sure agents.defaults.workspace points to the active one.

openclaw doctor warns when it detects extra workspace directories.

Workspace file map (what each file means)

These are the standard files OpenClaw expects inside the workspace:

  • AGENTS.md

    • Operating instructions for the agent and how it should use memory.
    • Loaded at the start of every session.
    • Good place for rules, priorities, and "how to behave" details.
  • SOUL.md

    • Persona, tone, and boundaries.
    • Loaded every session.
  • USER.md

    • Who the user is and how to address them.
    • Loaded every session.
  • IDENTITY.md

    • The agent's name, vibe, and emoji.
    • Created/updated during the bootstrap ritual.
  • TOOLS.md

    • Notes about your local tools and conventions.
    • Does not control tool availability; it is only guidance.
  • HEARTBEAT.md

    • Optional tiny checklist for heartbeat runs.
    • Keep it short to avoid token burn.
  • BOOT.md

    • Optional startup checklist executed on gateway restart when internal hooks are enabled.
    • Keep it short; use the message tool for outbound sends.
  • BOOTSTRAP.md

    • One-time first-run ritual.
    • Only created for a brand-new workspace.
    • Delete it after the ritual is complete.
  • memory/YYYY-MM-DD.md

    • Daily memory log (one file per day).
    • Recommended to read today + yesterday on session start.
  • MEMORY.md (optional)

    • Curated long-term memory.
    • Only load in the main, private session (not shared/group contexts).

See Memory for the workflow and automatic memory flush.

  • skills/ (optional)

    • Workspace-specific skills.
    • Overrides managed/bundled skills when names collide.
  • canvas/ (optional)

    • Canvas UI files for node displays (for example canvas/index.html).

If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust the limit with agents.defaults.bootstrapMaxChars (default: 20000). openclaw setup can recreate missing defaults without overwriting existing files.

What is NOT in the workspace

These live under ~/.openclaw/ and should NOT be committed to the workspace repo:

  • ~/.openclaw/openclaw.json (config)
  • ~/.openclaw/credentials/ (OAuth tokens, API keys)
  • ~/.openclaw/agents/<agentId>/sessions/ (session transcripts + metadata)
  • ~/.openclaw/skills/ (managed skills)

If you need to migrate sessions or config, copy them separately and keep them out of version control.

Treat the workspace as private memory. Put it in a private git repo so it is backed up and recoverable.

Run these steps on the machine where the Gateway runs (that is where the workspace lives).

1) Initialize the repo

If git is installed, brand-new workspaces are initialized automatically. If this workspace is not already a repo, run:

cd ~/.openclaw/workspace
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git commit -m "Add agent workspace"

2) Add a private remote (beginner-friendly options)

Option A: GitHub web UI

  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:
git branch -M main
git remote add origin <https-url>
git push -u origin main

Option B: GitHub CLI (gh)

gh auth login
gh repo create openclaw-workspace --private --source . --remote origin --push

Option C: GitLab web UI

  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:
git branch -M main
git remote add origin <https-url>
git push -u origin main

3) Ongoing updates

git status
git add .
git commit -m "Update memory"
git push

Do not commit secrets

Even in a private repo, avoid storing secrets in the workspace:

  • API keys, OAuth tokens, passwords, or private credentials.
  • Anything under ~/.openclaw/.
  • Raw dumps of chats or sensitive attachments.

If you must store sensitive references, use placeholders and keep the real secret elsewhere (password manager, environment variables, or ~/.openclaw/).

Suggested .gitignore starter:

.DS_Store
.env
**/*.key
**/*.pem
**/secrets*

Moving the workspace to a new machine

  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 <path> to seed any missing files.
  4. If you need sessions, copy ~/.openclaw/agents/<agentId>/sessions/ from the old machine separately.

Advanced notes

  • Multi-agent routing can use different workspaces per agent. See Channel routing for routing configuration.
  • If agents.defaults.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 longlived 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 agenteditable HTML and A2UI.

Components and flows

Gateway (daemon)

  • Maintains provider connections.
  • Exposes a typed WS API (requests, responses, serverpush 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 devicebased (role node) and approval lives in the device pairing store.
  • Expose commands like canvas.*, camera.*, screen.record, location.get.

Protocol details:

WebChat

  • Static UI that uses the Gateway WS API for chat history and sends.
  • In remote setups, connects through the same SSH/Tailscale tunnel as other clients.

Connection lifecycle (single client)

Client                    Gateway
  |                          |
  |---- req:connect -------->|
  |<------ res (ok) ---------|   (or res error + close)
  |   (payload=hello-ok carries snapshot: presence + health)
  |                          |
  |<------ event:presence ---|
  |<------ event:tick -------|
  |                          |
  |------- req:agent ------->|
  |<------ res:agent --------|   (ack: {runId,status:"accepted"})
  |<------ event:agent ------|   (streaming)
  |<------ res:agent --------|   (final: {runId,status,summary})
  |                          |

Wire protocol (summary)

  • Transport: WebSocket, text frames with JSON payloads.
  • First frame must be connect.
  • After handshake:
    • Requests: {type:"req", id, method, params}{type:"res", id, ok, payload|error}
    • Events: {type:"event", event, payload, seq?, stateVersion?}
  • If OPENCLAW_GATEWAY_TOKEN (or --token) is set, connect.params.auth.token must match or the socket closes.
  • Idempotency keys are required for sideeffecting methods (send, agent) to safely retry; the server keeps a shortlived 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 hosts own tailnet address) can be autoapproved to keep samehost UX smooth.
  • Nonlocal 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, Pairing, Security.

Protocol typing and codegen

  • TypeBox schemas define the protocol.
  • JSON Schema is generated from those schemas.
  • Swift models are generated from the JSON Schema.

Remote access

  • Preferred: Tailscale or VPN.
  • Alternative: SSH tunnel
    ssh -N -L 18789:127.0.0.1:18789 user@host
    
  • The same handshake + auth token apply over the tunnel.
  • TLS + optional pinning can be enabled for WS in remote setups.

Operations snapshot

  • Start: openclaw gateway (foreground, logs to stdout).
  • Health: health over WS (also included in hello-ok).
  • Supervision: launchd/systemd for autorestart.

Invariants

  • Exactly one Gateway controls a single Baileys session per host.
  • Handshake is mandatory; any nonJSON or nonconnect 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: perchannel 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 agents main session:

  • agent:<agentId>:<mainKey> (default: agent:main:main)

Groups and channels remain isolated per channel:

  • Groups: agent:<agentId>:<channel>:group:<id>
  • Channels/rooms: agent:<agentId>:<channel>:channel:<id>

Threads:

  • Slack/Discord threads append :thread:<threadId> to the base key.
  • Telegram forum topics embed :topic:<topicId> in the group key.

Examples:

  • agent:main:telegram:group:-1001234567890:topic: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:

{
  broadcast: {
    strategy: "parallel",
    "120363403215116621@g.us": ["alfred", "baerbel"],
    "+15555550123": ["support", "logger"],
  },
}

See: Broadcast Groups.

Config overview

  • agents.list: named agent definitions (workspace, model, etc.).
  • bindings: map inbound channels/accounts/peers to agents.

Example:

{
  agents: {
    list: [{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }],
  },
  bindings: [
    { match: { channel: "slack", teamId: "T123" }, agentId: "support" },
    { match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" },
  ],
}

Session storage

Session stores live under the state directory (default ~/.openclaw):

  • ~/.openclaw/agents/<agentId>/sessions/sessions.json
  • JSONL transcripts live alongside the store

You can override the store path via session.store and {agentId} templating.

WebChat behavior

WebChat attaches to the selected agent and defaults to the agents main session. Because of this, WebChat lets you see crosschannel 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 sessions JSONL history.

Configuration

See Compaction config & modes for the agents.defaults.compaction settings.

Auto-compaction (default on)

When a session nears or exceeds the models context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.

Youll see:

  • 🧹 Auto-compaction complete in verbose mode
  • /status showing 🧹 Compactions: <count>

Before compaction, OpenClaw can run a silent memory flush turn to store durable notes to disk. See Memory for details and config.

Manual compaction

Use /compact (optionally with instructions) to force a compaction pass:

/compact Focus on decisions and open questions

Context window source

Context window is model-specific. OpenClaw uses the model definition from the configured provider catalog to determine limits.

Compaction vs pruning

  • Compaction: summarises and persists in JSONL.
  • Session pruning: trims old tool results only, in-memory, per request.

See /concepts/session-pruning for pruning details.

Tips

  • Use /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 models 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 assistants 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 whats inside the models current window.

Quick start (inspect context)

  • /status → quick “how full is my window?” view + session settings.
  • /context list → whats injected + rough sizes (per file + totals).
  • /context detail → deeper breakdown: per-file, per-tool schema sizes, per-skill entry sizes, and system prompt size.
  • /usage tokens → append per-reply usage footer to normal replies.
  • /compact → summarize older history into a compact entry to free window space.

See also: Slash commands, Token use & costs, Compaction.

Example output

Values vary by model, provider, tool policy, and whats in your workspace.

/context list

🧠 Context breakdown
Workspace: <workspaceDir>
Bootstrap max/file: 20,000 chars
Sandbox: mode=non-main sandboxed=false
System prompt (run): 38,412 chars (~9,603 tok) (Project Context 23,901 chars (~5,976 tok))

Injected workspace files:
- AGENTS.md: OK | raw 1,742 chars (~436 tok) | injected 1,742 chars (~436 tok)
- SOUL.md: OK | raw 912 chars (~228 tok) | injected 912 chars (~228 tok)
- TOOLS.md: TRUNCATED | raw 54,210 chars (~13,553 tok) | injected 20,962 chars (~5,241 tok)
- IDENTITY.md: OK | raw 211 chars (~53 tok) | injected 211 chars (~53 tok)
- USER.md: OK | raw 388 chars (~97 tok) | injected 388 chars (~97 tok)
- HEARTBEAT.md: MISSING | raw 0 | injected 0
- BOOTSTRAP.md: OK | raw 0 chars (~0 tok) | injected 0 chars (~0 tok)

Skills list (system prompt text): 2,184 chars (~546 tok) (12 skills)
Tools: read, edit, write, exec, process, browser, message, sessions_send, …
Tool list (system prompt text): 1,032 chars (~258 tok)
Tool schemas (JSON): 31,988 chars (~7,997 tok) (counts toward context; not shown as text)
Tools: (same as above)

Session tokens (cached): 14,250 total / ctx=32,000

/context detail

🧠 Context breakdown (detailed)
…
Top skills (prompt entry size):
- frontend-design: 412 chars (~103 tok)
- oracle: 401 chars (~101 tok)
… (+10 more skills)

Top tools (schema size):
- browser: 9,812 chars (~2,453 tok)
- exec: 6,240 chars (~1,560 tok)
… (+N more tools)

What counts toward the context window

Everything the model receives counts, including:

  • System prompt (all sections).
  • Conversation history.
  • Tool calls + tool results.
  • Attachments/transcripts (images/audio/files).
  • Compaction summaries and pruning artifacts.
  • Provider “wrappers” or hidden headers (not visible, still counted).

How OpenClaw builds the system prompt

The system prompt is OpenClaw-owned and rebuilt each run. It includes:

  • Tool list + short descriptions.
  • Skills list (metadata only; see below).
  • Workspace location.
  • Time (UTC + converted user time if configured).
  • Runtime metadata (host/OS/model/thinking).
  • Injected workspace bootstrap files under Project Context.

Full breakdown: System Prompt.

Injected workspace files (Project Context)

By default, OpenClaw injects a fixed set of workspace files (if present):

  • AGENTS.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: whats 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 skills 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 dont 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.

Sessions, compaction, and pruning (what persists)

What persists across messages depends on the mechanism:

  • Normal history persists in the session transcript until compacted/pruned by policy.
  • Compaction persists a summary into the transcript and keeps recent messages intact.
  • Pruning removes old tool results from the in-memory prompt for a run, but does not rewrite the transcript.

Docs: Session, Compaction, Session pruning.

What /context actually reports

/context prefers the latest run-built system prompt report when available:

  • System prompt (run) = captured from the last embedded (tool-capable) run and persisted in the session store.
  • System prompt (estimate) = computed on the fly when no run report exists (or when running via a CLI backend that doesnt 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).

Whats implemented (2025-12-03)

  • Activation modes: mention (default) or always. mention requires a ping (real WhatsApp @-mentions via mentionedJids, regex patterns, or the bots 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:<agentId>:whatsapp:group:<jid> 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 "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context. If metadata isnt available we still tell the agent its a group chat.

Config example (WhatsApp)

Add a groupChat block to ~/.openclaw/openclaw.json so display-name pings work even when WhatsApp strips the visual @ in the text body:

{
  channels: {
    whatsapp: {
      groups: {
        "*": { requireMention: true },
      },
    },
  },
  agents: {
    list: [
      {
        id: "main",
        groupChat: {
          historyLimit: 50,
          mentionPatterns: ["@?openclaw", "\\+?15555550123"],
        },
      },
    ],
  },
}

Notes:

  • The regexes are case-insensitive; they cover a display-name ping like @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 bots 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 groups 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: <groupJid> and the [from: …] suffix.

Known considerations

  • Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
  • Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
  • Session store entries will appear as agent:<agentId>:whatsapp:group:<jid> in the session store (~/.openclaw/agents/<agentId>/sessions/sessions.json by default); a missing entry just means the group hasnt 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: { "<group-id>": { ... } } (no "*" key)
Only you can trigger in groups groupPolicy: "allowlist", groupAllowFrom: ["+1555..."]

Session keys

  • Group sessions use agent:<agentId>:<channel>:group:<id> session keys (rooms/channels use agent:<agentId>:<channel>:channel:<id>).
  • Telegram forum topics add :topic:<threadId> to the group id so each topic has its own session.
  • Direct chats use the main session (or per-sender if configured).
  • Heartbeats are skipped for group sessions.

Pattern: personal DMs + public groups (single agent)

Yes — this works well if your “personal” traffic is DMs and your “public” traffic is groups.

Why: in single-agent mode, DMs typically land in the main session key (agent:main:main), while groups always use non-main session keys (agent:main:<channel>:group:<id>). If you enable sandboxing with mode: "non-main", those group sessions run in Docker while your main DM session stays on-host.

This gives you one agent “brain” (shared workspace + memory), but two execution postures:

  • DMs: full tools (host)
  • Groups: sandbox + restricted tools (Docker)

If you need truly separate workspaces/personas (“personal” and “public” must never mix), use a second agent + bindings. See Multi-Agent Routing.

Example (DMs on host, groups sandboxed + messaging-only tools):

{
  agents: {
    defaults: {
      sandbox: {
        mode: "non-main", // groups/channels are non-main -> sandboxed
        scope: "session", // strongest isolation (one container per group/channel)
        workspaceAccess: "none",
      },
    },
  },
  tools: {
    sandbox: {
      tools: {
        // If allow is non-empty, everything else is blocked (deny still wins).
        allow: ["group:messaging", "group:sessions"],
        deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
      },
    },
  },
}

Want “groups can only see folder X” instead of “no host access”? Keep workspaceAccess: "none" and mount only allowlisted paths into the sandbox:

{
  agents: {
    defaults: {
      sandbox: {
        mode: "non-main",
        scope: "session",
        workspaceAccess: "none",
        docker: {
          binds: [
            // hostPath:containerPath:mode
            "~/FriendsShared:/data:ro",
          ],
        },
      },
    },
  },
}

Related:

Display labels

  • UI labels use displayName when available, formatted as <channel>:<token>.
  • #room is reserved for rooms/channels; group chats use g-<slug> (lowercase, spaces -> -, keep #@+._-).

Group policy

Control how group/room messages are handled per channel:

{
  channels: {
    whatsapp: {
      groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
      groupAllowFrom: ["+15551234567"],
    },
    telegram: {
      groupPolicy: "disabled",
      groupAllowFrom: ["123456789", "@username"],
    },
    signal: {
      groupPolicy: "disabled",
      groupAllowFrom: ["+15551234567"],
    },
    imessage: {
      groupPolicy: "disabled",
      groupAllowFrom: ["chat_id:123"],
    },
    msteams: {
      groupPolicy: "disabled",
      groupAllowFrom: ["user@org.com"],
    },
    discord: {
      groupPolicy: "allowlist",
      guilds: {
        GUILD_ID: { channels: { help: { allow: true } } },
      },
    },
    slack: {
      groupPolicy: "allowlist",
      channels: { "#general": { allow: true } },
    },
    matrix: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["@owner:example.org"],
      groups: {
        "!roomId:example.org": { allow: true },
        "#alias:example.org": { allow: true },
      },
    },
  },
}
Policy Behavior
"open" Groups bypass allowlists; mention-gating still applies.
"disabled" Block all group messages entirely.
"allowlist" Only allow groups/rooms that match the configured allowlist.

Notes:

  • 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.<id>.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.

{
  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.<channel>.historyLimit (or channels.<channel>.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):

{
  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
{
  channels: { whatsapp: { groupPolicy: "disabled" } },
}
  1. Allow only specific groups (WhatsApp)
{
  channels: {
    whatsapp: {
      groups: {
        "123@g.us": { requireMention: true },
        "456@g.us": { requireMention: false },
      },
    },
  },
}
  1. Allow all groups but require mention (explicit)
{
  channels: {
    whatsapp: {
      groups: { "*": { requireMention: true } },
    },
  },
}
  1. Only the owner can trigger in groups (WhatsApp)
{
  channels: {
    whatsapp: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["+15551234567"],
      groups: { "*": { requireMention: true } },
    },
  },
}

Activation (owner-only)

Group owners can toggle per-group activation:

  • /activation mention
  • /activation always

Owner is determined by channels.whatsapp.allowFrom (or the bots 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:<id> when routing or allowlisting.
  • List chats: imsg chats --limit 20.
  • Group replies always go back to the same chat_id.

WhatsApp specifics

See Group messages for WhatsApp-only behavior (history injection, mention handling details).

null

Source: https://docs.openclaw.ai/concepts/memory

Memory

OpenClaw memory is plain Markdown in the agent workspace. The files are the source of truth; the model only "remembers" what gets written to disk.

Memory search tools are provided by the active memory plugin (default: memory-core). Disable memory plugins with plugins.slots.memory = "none".

Memory files (Markdown)

The default workspace layout uses two memory layers:

  • memory/YYYY-MM-DD.md
    • Daily log (append-only).
    • Read today + yesterday at session start.
  • MEMORY.md (optional)
    • Curated long-term memory.
    • Only load in the main, private session (never in group contexts).

These files live under the workspace (agents.defaults.workspace, default ~/clawd). See Agent workspace for the full layout.

When to write memory

  • Decisions, preferences, and durable facts go to MEMORY.md.
  • Day-to-day notes and running context go to memory/YYYY-MM-DD.md.
  • If someone says "remember this," write it down (do not keep it in RAM).
  • This area is still evolving. It helps to remind the model to store memories; it will know what to do.
  • If you want something to stick, ask the bot to write it into memory.

Automatic memory flush (pre-compaction ping)

When a session is close to auto-compaction, OpenClaw triggers a silent, agentic turn that reminds the model to write durable memory before the context is compacted. The default prompts explicitly say the model may reply, but usually NO_REPLY is the correct response so the user never sees this turn.

This is controlled by agents.defaults.compaction.memoryFlush:

{
  agents: {
    defaults: {
      compaction: {
        reserveTokensFloor: 20000,
        memoryFlush: {
          enabled: true,
          softThresholdTokens: 4000,
          systemPrompt: "Session nearing compaction. Store durable memories now.",
          prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
        },
      },
    },
  },
}

Details:

  • Soft threshold: flush triggers when the session token estimate crosses contextWindow - reserveTokensFloor - softThresholdTokens.
  • Silent by default: prompts include NO_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.

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: 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 gateways 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/<agentId>/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/<agentId>/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 5m).
  • 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 agents XDG dirs.

      OpenClaws 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:

      # 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. 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/<collection>/<relative-path> 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/<id>/qmd/sessions/, so memory_search can recall recent conversations without touching the builtin SQLite index.
  • memory_search snippets now include a Source: <path#line> 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

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 cant be parsed, the search manager logs a warning and returns the builtin provider (existing Markdown embeddings) until QMD recovers.

Additional memory paths

If you want to index Markdown files outside the default workspace layout, add explicit paths:

agents: {
  defaults: {
    memorySearch: {
      extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
    }
  }
}

Notes:

  • Paths can be absolute or workspace-relative.
  • Directories are scanned recursively for .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:

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:

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:

Config example:

agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      fallback: "openai",
      remote: {
        batch: { enabled: true, concurrency: 2 }
      },
      sync: { watch: true }
    }
  }
}

Tools:

  • memory_search — returns snippets with file + line ranges.
  • memory_get — read memory file content by path.

Local mode:

  • Set agents.defaults.memorySearch.provider = "local".
  • Provide agents.defaults.memorySearch.local.modelPath (GGUF 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/<agentId>.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).
  1. Convert BM25 rank into a 0..1-ish score:
  • textScore = 1 / (1 + max(0, bm25Rank))
  1. 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 cant be created, we keep vector-only search (no hard failure).

This isnt “IR-theory perfect”, but its simple, fast, and tends to improve recall/precision on real notes. If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization (min/max or z-score) before mixing.

Config:

agents: {
  defaults: {
    memorySearch: {
      query: {
        hybrid: {
          enabled: true,
          vectorWeight: 0.7,
          textWeight: 0.3,
          candidateMultiplier: 4
        }
      }
    }
  }
}

Embedding cache

OpenClaw can cache chunk embeddings in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.

Config:

agents: {
  defaults: {
    memorySearch: {
      cache: {
        enabled: true,
        maxEntries: 50000
      }
    }
  }
}

Session memory search (experimental)

You can optionally index session transcripts and surface them via memory_search. This is gated behind an experimental flag.

agents: {
  defaults: {
    memorySearch: {
      experimental: { sessionMemory: true },
      sources: ["memory", "sessions"]
    }
  }
}

Notes:

  • Session indexing is opt-in (off by default).
  • Session updates are debounced and indexed asynchronously once they cross delta thresholds (best-effort).
  • memory_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 agents session logs are indexed).
  • Session logs live on disk (~/.openclaw/agents/<agentId>/sessions/*.jsonl). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.

Delta thresholds (defaults shown):

agents: {
  defaults: {
    memorySearch: {
      sync: {
        sessions: {
          deltaBytes: 100000,   // ~100 KB
          deltaMessages: 50     // JSONL lines
        }
      }
    }
  }
}

SQLite vector acceleration (sqlite-vec)

When the sqlite-vec extension is available, OpenClaw stores embeddings in a SQLite virtual table (vec0) and performs vector distance queries in the database. This keeps search fast without loading every embedding into JS.

Configuration (optional):

agents: {
  defaults: {
    memorySearch: {
      store: {
        vector: {
          enabled: true,
          extensionPath: "/path/to/sqlite-vec"
        }
      }
    }
  }
}

Notes:

  • 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

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 for full schema.

Inbound dedupe

Channels can redeliver the same message after reconnects. OpenClaw keeps a short-lived cache keyed by channel/account/peer/session/message id so duplicate deliveries do not trigger another agent run.

Inbound debouncing

Rapid consecutive messages from the same sender can be batched into a single agent turn via messages.inbound. Debouncing is scoped per channel + conversation and uses the most recent message for reply threading/IDs.

Config (global default + per-channel overrides):

{
  messages: {
    inbound: {
      debounceMs: 2000,
      byChannel: {
        whatsapp: 5000,
        slack: 1500,
        discord: 1500,
      },
    },
  },
}

Notes:

  • Debounce applies to text-only messages; media/attachments flush immediately.
  • Control commands bypass debouncing so they remain standalone.

Sessions and devices

Sessions are owned by the gateway, not by clients.

  • Direct chats collapse into the agent main session key.
  • Groups/channels get their own session keys.
  • The session store and transcripts live on the gateway host.

Multiple devices/channels can map to the same session, but history is not fully synced back to every client. Recommendation: use one primary device for long conversations to avoid divergent context. The Control UI and TUI always show the gateway-backed session transcript, so they are the source of truth.

Details: Session management.

Inbound bodies and history context

OpenClaw separates the prompt body from the command body:

  • Body: prompt text sent to the agent. This may include channel envelopes and optional history wrappers.
  • CommandBody: raw user text for directive/command parsing.
  • RawBody: legacy alias 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.<id>.historyLimit (set 0 to disable).

Queueing and followups

If a run is already active, inbound messages can be queued, steered into the current run, or collected for a followup turn.

  • Configure via messages.queue (and messages.queue.byChannel).
  • Modes: interrupt, steer, followup, collect, plus backlog variants.

Details: Queueing.

Streaming, chunking, and batching

Block streaming sends partial replies as the model produces text blocks. Chunking respects channel text limits and avoids splitting fenced code.

Key settings:

  • agents.defaults.blockStreamingDefault (on|off, default off)
  • agents.defaults.blockStreamingBreak (text_end|message_end)
  • agents.defaults.blockStreamingChunk (minChars|maxChars|breakPreference)
  • agents.defaults.blockStreamingCoalesce (idle-based batching)
  • agents.defaults.humanDelay (human-like pause between block replies)
  • Channel overrides: *.blockStreaming and *.blockStreamingCoalesce (non-Telegram channels require explicit *.blockStreaming: true)

Details: Streaming + chunking.

Reasoning visibility and tokens

OpenClaw can expose or hide model reasoning:

  • /reasoning on|off|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 and Token use.

Prefixes, threading, and replies

Outbound message formatting is centralized in messages:

  • messages.responsePrefix, channels.<channel>.responsePrefix, and channels.<channel>.accounts.<id>.responsePrefix (outbound prefix cascade), plus channels.whatsapp.messagePrefix (WhatsApp inbound prefix)
  • Reply threading via replyToMode and per-channel defaults

Details: Configuration and channel docs.

Multi-Agent Routing

Source: https://docs.openclaw.ai/concepts/multi-agent

Multi-Agent Routing

Goal: multiple isolated agents (separate workspace + agentDir + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.

What is “one agent”?

An agent is a fully scoped brain with its own:

  • Workspace (files, AGENTS.md/SOUL.md/USER.md, local notes, persona rules).
  • State directory (agentDir) for auth profiles, model registry, and per-agent config.
  • Session store (chat history + routing state) under ~/.openclaw/agents/<agentId>/sessions.

Auth profiles are per-agent. Each agent reads from its own:

~/.openclaw/agents/<agentId>/agent/auth-profiles.json

Main agent credentials are not shared automatically. Never reuse agentDir across agents (it causes auth/session collisions). If you want to share creds, copy auth-profiles.json into the other agent's agentDir.

Skills are per-agent via each workspaces skills/ folder, with shared skills available from ~/.openclaw/skills. See Skills: per-agent vs shared.

The Gateway can host one agent (default) or many agents side-by-side.

Workspace note: each agents workspace is the default cwd, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See Sandboxing.

Paths (quick map)

  • Config: ~/.openclaw/openclaw.json (or OPENCLAW_CONFIG_PATH)
  • State dir: ~/.openclaw (or OPENCLAW_STATE_DIR)
  • Workspace: ~/.openclaw/workspace (or ~/.openclaw/workspace-<agentId>)
  • Agent dir: ~/.openclaw/agents/<agentId>/agent (or agents.list[].agentDir)
  • Sessions: ~/.openclaw/agents/<agentId>/sessions

Single-agent mode (default)

If you do nothing, OpenClaw runs a single agent:

  • agentId defaults to main.
  • Sessions are keyed as agent:main:<mainKey>.
  • Workspace defaults to ~/.openclaw/workspace (or ~/.openclaw/workspace-<profile> when OPENCLAW_PROFILE is set).
  • State defaults to ~/.openclaw/agents/main/agent.

Agent helper

Use the agent wizard to add a new isolated agent:

openclaw agents add work

Then add bindings (or let the wizard do it) to route inbound messages.

Verify with:

openclaw agents list --bindings

Multiple agents = multiple people, multiple personalities

With multiple agents, each agentId becomes a fully isolated persona:

  • Different phone numbers/accounts (per channel accountId).
  • Different personalities (per-agent workspace files like AGENTS.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 peragent sender identity).

Important detail: direct chats collapse to the agents main session key, so true isolation requires one agent per person.

Example:

{
  agents: {
    list: [
      { id: "alex", workspace: "~/.openclaw/workspace-alex" },
      { id: "mia", workspace: "~/.openclaw/workspace-mia" },
    ],
  },
  bindings: [
    { agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
    { agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } },
  ],
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551230001", "+15551230002"],
    },
  },
}

Notes:

  • DM access control is global per WhatsApp account (pairing/allowlist), not per agent.
  • For shared groups, bind the group to one agent or use Broadcast groups.

Routing rules (how messages pick an agent)

Bindings are deterministic and most-specific wins:

  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:<agentId>:<mainKey> (per-agent “main”; session.mainKey).

Example: two WhatsApps → two agents

~/.openclaw/openclaw.json (JSON5):

{
  agents: {
    list: [
      {
        id: "home",
        default: true,
        name: "Home",
        workspace: "~/.openclaw/workspace-home",
        agentDir: "~/.openclaw/agents/home/agent",
      },
      {
        id: "work",
        name: "Work",
        workspace: "~/.openclaw/workspace-work",
        agentDir: "~/.openclaw/agents/work/agent",
      },
    ],
  },

  // Deterministic routing: first match wins (most-specific first).
  bindings: [
    { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },

    // Optional per-peer override (example: send a specific group to work agent).
    {
      agentId: "work",
      match: {
        channel: "whatsapp",
        accountId: "personal",
        peer: { kind: "group", id: "1203630...@g.us" },
      },
    },
  ],

  // Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
  tools: {
    agentToAgent: {
      enabled: false,
      allow: ["home", "work"],
    },
  },

  channels: {
    whatsapp: {
      accounts: {
        personal: {
          // Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
          // authDir: "~/.openclaw/credentials/whatsapp/personal",
        },
        biz: {
          // Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
          // authDir: "~/.openclaw/credentials/whatsapp/biz",
        },
      },
    },
  },
}

Example: WhatsApp daily chat + Telegram deep work

Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.

{
  agents: {
    list: [
      {
        id: "chat",
        name: "Everyday",
        workspace: "~/.openclaw/workspace-chat",
        model: "anthropic/claude-sonnet-4-5",
      },
      {
        id: "opus",
        name: "Deep Work",
        workspace: "~/.openclaw/workspace-opus",
        model: "anthropic/claude-opus-4-5",
      },
    ],
  },
  bindings: [
    { agentId: "chat", match: { channel: "whatsapp" } },
    { agentId: "opus", match: { channel: "telegram" } },
  ],
}

Notes:

  • If you have multiple accounts for a channel, add 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:

{
  agents: {
    list: [
      {
        id: "chat",
        name: "Everyday",
        workspace: "~/.openclaw/workspace-chat",
        model: "anthropic/claude-sonnet-4-5",
      },
      {
        id: "opus",
        name: "Deep Work",
        workspace: "~/.openclaw/workspace-opus",
        model: "anthropic/claude-opus-4-5",
      },
    ],
  },
  bindings: [
    { agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
    { agentId: "chat", match: { channel: "whatsapp" } },
  ],
}

Peer bindings always win, so keep them above the channel-wide rule.

Family agent bound to a WhatsApp group

Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy:

{
  agents: {
    list: [
      {
        id: "family",
        name: "Family",
        workspace: "~/.openclaw/workspace-family",
        identity: { name: "Family Bot" },
        groupChat: {
          mentionPatterns: ["@family", "@familybot", "@Family Bot"],
        },
        sandbox: {
          mode: "all",
          scope: "agent",
        },
        tools: {
          allow: [
            "exec",
            "read",
            "sessions_list",
            "sessions_history",
            "sessions_send",
            "sessions_spawn",
            "session_status",
          ],
          deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
        },
      },
    ],
  },
  bindings: [
    {
      agentId: "family",
      match: {
        channel: "whatsapp",
        peer: { kind: "group", id: "120363999999999999@g.us" },
      },
    },
  ],
}

Notes:

  • Tool allow/deny lists are tools, not skills. If a skill needs to run a binary, ensure 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:

{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/.openclaw/workspace-personal",
        sandbox: {
          mode: "off",  // No sandbox for personal agent
        },
        // No tool restrictions - all tools available
      },
      {
        id: "family",
        workspace: "~/.openclaw/workspace-family",
        sandbox: {
          mode: "all",     // Always sandboxed
          scope: "agent",  // One container per agent
          docker: {
            // Optional one-time setup after container creation
            setupCommand: "apt-get update && apt-get install -y git curl",
          },
        },
        tools: {
          allow: ["read"],                    // Only read tool
          deny: ["exec", "write", "edit", "apply_patch"],    // Deny others
        },
      },
    ],
  },
}

Note: setupCommand lives under sandbox.docker and runs once on container creation. Per-agent sandbox.docker.* overrides are ignored when the resolved scope is "shared".

Benefits:

  • Security isolation: Restrict tools for untrusted agents
  • Resource control: Sandbox specific agents while keeping others on host
  • Flexible policies: Different permissions per agent

Note: tools.elevated is global and sender-based; it is not configurable per agent. If you need per-agent boundaries, use agents.list[].tools to deny exec. For group targeting, use agents.list[].groupChat.mentionPatterns so @mentions map cleanly to the intended agent.

See Multi-Agent Sandbox & Tools for detailed examples.

OAuth

Source: https://docs.openclaw.ai/concepts/oauth

OAuth

OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably OpenAI Codex (ChatGPT OAuth)). For Anthropic subscriptions, use the setup-token flow. This page explains:

  • how the OAuth token exchange works (PKCE)
  • where tokens are stored (and why)
  • how to handle multiple accounts (profiles + per-session overrides)

OpenClaw also supports provider plugins that ship their own OAuth or APIkey flows. Run them via:

openclaw models auth login --provider <id>

The token sink (why it exists)

OAuth providers commonly mint a new refresh token during login/refresh flows. Some providers (or OAuth clients) can invalidate older refresh tokens when a new one is issued for the same user/app.

Practical symptom:

  • you log in via OpenClaw and via Claude Code / Codex CLI → one of them randomly gets “logged out” later

To reduce that, OpenClaw treats auth-profiles.json as a token sink:

  • the runtime reads credentials from one place
  • we can keep multiple profiles and route them deterministically

Storage (where tokens live)

Secrets are stored per-agent:

  • Auth profiles (OAuth + API keys): ~/.openclaw/agents/<agentId>/agent/auth-profiles.json
  • Runtime cache (managed automatically; dont edit): ~/.openclaw/agents/<agentId>/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

Anthropic setup-token (subscription auth)

Run claude setup-token on any machine, then paste it into OpenClaw:

openclaw models auth setup-token --provider anthropic

If you generated the token elsewhere, paste it manually:

openclaw models auth paste-token --provider anthropic

Verify:

openclaw models status

OAuth exchange (how login works)

OpenClaws 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 cant bind (or youre 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):

openclaw agents add work
openclaw agents add personal

Then configure auth per-agent (wizard) and route chats to the right agent.

2) Advanced: multiple profiles in one agent

auth-profiles.json supports multiple profile IDs for the same provider.

Pick which profile is used:

  • globally via config ordering (auth.order)
  • per-session via /model ...@<profileId>

Example (session override):

  • /model Opus@anthropic:work

How to see what profile IDs exist:

  • openclaw channels list --json (shows auth[])

Related docs:

Presence

Source: https://docs.openclaw.ai/concepts/presence

Presence

OpenClaw “presence” is a lightweight, besteffort view of:

  • the Gateway itself, and
  • clients connected to the Gateway (mac app, WebChat, CLI, etc.)

Presence is used primarily to render the macOS apps 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: humanfriendly host name
  • ip: besteffort 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 oneoff CLI commands dont show up

The CLI often connects for short, oneoff 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 inmemory map:

  • Entries are keyed by a presence key.
  • The best key is a stable instanceId (from connect.client.instanceId) that survives restarts.
  • Keys are caseinsensitive.

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 clientreported 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 connectionderived 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:<key>) 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:

{
  messages: {
    queue: {
      mode: "collect",
      debounceMs: 1000,
      cap: 20,
      drop: "summarize",
      byChannel: { discord: "collect" },
    },
  },
}

Queue options

Options apply to followup, collect, and steer-backlog (and to steer when it falls back to followup):

  • debounceMs: wait for quiet before starting a followup turn (prevents “continue, continue”).
  • cap: max queued messages per session.
  • drop: overflow policy (old, new, summarize).

Summarize keeps a short bullet list of dropped messages and injects it as a synthetic followup prompt. Defaults: debounceMs: 1000, cap: 20, drop: summarize.

Per-session overrides

  • Send /queue <mode> as a standalone command to store the mode for the current session.
  • Options can be combined: /queue collect debounce:2s cap:25 drop:summarize
  • /queue 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:

{
  channels: {
    telegram: {
      retry: {
        attempts: 3,
        minDelayMs: 400,
        maxDelayMs: 30000,
        jitter: 0.1,
      },
    },
    discord: {
      retry: {
        attempts: 3,
        minDelayMs: 500,
        maxDelayMs: 30000,
        jitter: 0.1,
      },
    },
  },
}

Notes

  • Retries apply per request (message send, media upload, reaction, poll, sticker).
  • Composite flows do not retry completed steps.

Session Management

Source: https://docs.openclaw.ai/concepts/session

Session Management

OpenClaw treats one direct-chat session per agent as primary. Direct chats collapse to agent:<agentId>:<mainKey> (default main), while group/channel chats get their own keys. session.mainKey is honored.

Use session.dmScope to control how direct messages are grouped:

  • main (default): all DMs share the main session for continuity.
  • per-peer: isolate by sender id across channels.
  • per-channel-peer: isolate by channel + sender (recommended for multi-user inboxes).
  • per-account-channel-peer: isolate by account + channel + sender (recommended for multi-account inboxes). 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.

If your agent can receive DMs from multiple people (pairing approvals for more than one sender, a DM allowlist with multiple entries, or dmPolicy: "open"), enable secure DM mode to avoid cross-user context leakage:

// ~/.openclaw/openclaw.json
{
  session: {
    // Secure DM mode: isolate DM context per channel + sender.
    dmScope: "per-channel-peer",
  },
}

Notes:

  • Default is dmScope: "main" for continuity (all DMs share the main session).
  • For multi-account inboxes on the same channel, prefer per-account-channel-peer.
  • If the same person contacts you on multiple channels, use session.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 gateways store fields (inputTokens, outputTokens, totalTokens, contextTokens). Clients do not parse JSONL transcripts to “fix up” totals.

Where state lives

  • On the gateway host:
    • Store file: ~/.openclaw/agents/<agentId>/sessions/sessions.json (per agent).
  • Transcripts: ~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl (Telegram topic sessions use .../<SessionId>-topic-<threadId>.jsonl).
  • The store is a map sessionKey -> { sessionId, updatedAt, ... }. Deleting entries is safe; they are recreated on demand.
  • Group entries may include displayName, channel, subject, room, 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.

Pre-compaction memory flush

When a session nears auto-compaction, OpenClaw can run a silent memory flush turn that reminds the model to write durable notes to disk. This only runs when the workspace is writable. See Memory and Compaction.

Mapping transports → session keys

  • Direct chats follow session.dmScope (default main).
    • main: agent:<agentId>:<mainKey> (continuity across devices/channels).
      • Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
    • per-peer: agent:<agentId>:dm:<peerId>.
    • per-channel-peer: agent:<agentId>:<channel>:dm:<peerId>.
    • per-account-channel-peer: agent:<agentId>:<channel>:<accountId>:dm:<peerId> (accountId defaults to default).
    • If session.identityLinks matches a provider-prefixed peer id (for example telegram:123), the canonical key replaces <peerId> so the same person shares a session across channels.
  • Group chats isolate state: agent:<agentId>:<channel>:group:<id> (rooms/channels use agent:<agentId>:<channel>:channel:<id>).
    • Telegram forum topics append :topic:<threadId> to the group id for isolation.
    • Legacy group:<id> keys are still recognized for migration.
  • Inbound contexts may still use group:<id>; the channel is inferred from Provider and normalized to the canonical agent:<agentId>:<channel>:group:<id> form.
  • Other sources:
    • Cron jobs: cron:<job.id>
    • Webhooks: hook:<uuid> (unless explicitly set by the hook)
    • Node runs: node-<nodeId>

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

{
  session: {
    sendPolicy: {
      rules: [
        { action: "deny", match: { channel: "discord", chatType: "group" } },
        { action: "deny", match: { keyPrefix: "cron:" } },
      ],
      default: "allow",
    },
  },
}

Runtime override (owner only):

  • /send on → allow for this session
  • /send off → deny for this session
  • /send inherit → clear override and use config rules Send these as standalone messages so they register.

Configuration (optional rename example)

// ~/.openclaw/openclaw.json
{
  session: {
    scope: "per-sender", // keep group keys separate
    dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes)
    identityLinks: {
      alice: ["telegram:123456789", "discord:987654321012345678"],
    },
    reset: {
      // Defaults: mode=daily, atHour=4 (gateway host local time).
      // If you also set idleMinutes, whichever expires first wins.
      mode: "daily",
      atHour: 4,
      idleMinutes: 120,
    },
    resetByType: {
      thread: { mode: "daily", atHour: 4 },
      dm: { mode: "idle", idleMinutes: 240 },
      group: { mode: "idle", idleMinutes: 120 },
    },
    resetByChannel: {
      discord: { mode: "idle", idleMinutes: 10080 },
    },
    resetTriggers: ["/new", "/reset"],
    store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
    mainKey: "main",
  },
}

Inspecting

  • openclaw status — shows store path and recent sessions.
  • openclaw sessions --json — dumps every entry (filter with --active <minutes>).
  • openclaw gateway call sessions.list --params '{}' — fetch sessions from the running gateway (use --url/--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 whats 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.
  • 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 followup requests can reuse the freshly cached prompt instead of re-caching the full history again.
  • What it does not do: pruning doesnt add tokens or “double” costs; it only changes what gets cached on that first postTTL 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 arent 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.

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):

{
  agent: {
    contextPruning: { mode: "off" },
  },
}

Enable TTL-aware pruning:

{
  agent: {
    contextPruning: { mode: "cache-ttl", ttl: "5m" },
  },
}

Restrict pruning to specific tools:

{
  agent: {
    contextPruning: {
      mode: "cache-ttl",
      tools: { allow: ["exec", "read"], deny: ["*image*"] },
    },
  },
}

See config reference: Gateway Configuration

Session Tools

Source: https://docs.openclaw.ai/concepts/session-tool

Session Tools

Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session.

Tool Names

  • sessions_list
  • sessions_history
  • sessions_send
  • sessions_spawn

Key Model

  • Main direct chat bucket is always the literal key "main" (resolved to the current agents main key).
  • Group chats use agent:<agentId>:<channel>:group:<id> or agent:<agentId>:<channel>:channel:<id> (pass the full key).
  • Cron jobs use cron:<job.id>.
  • Hooks use hook:<uuid> unless explicitly set.
  • Node sessions use node-<nodeId> unless explicitly set.

global and unknown are reserved values and are never listed. If session.scope = "global", we alias it to main for all tools so callers never see global.

sessions_list

List sessions as an array of rows.

Parameters:

  • kinds?: string[] filter: any of "main" | "group" | "cron" | "hook" | "node" | "other"
  • limit?: 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 pingpong.
    • Max turns is session.agentToAgent.maxPingPongTurns (05, default 5).
  • Once the loop ends, OpenClaw runs the agenttoagent 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 + round1 reply + latest pingpong 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).

{
  "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:<agentId>:subagent:<uuid> 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:

{
  agents: {
    defaults: {
      sandbox: {
        // default: "spawned"
        sessionToolsVisibility: "spawned", // or "all"
      },
    },
  },
}

Sessions

Source: https://docs.openclaw.ai/concepts/sessions

Sessions

Canonical session management docs live in Session management.

Streaming and Chunking

Source: https://docs.openclaw.ai/concepts/streaming

Streaming + chunking

OpenClaw has two separate “streaming” layers:

  • Block streaming (channels): emit completed blocks as the assistant writes. These are normal channel messages (not token deltas).
  • Token-ish streaming (Telegram only): update a draft bubble with partial text while generating; final message is sent at the end.

There is no real token streaming to external channel messages today. Telegram draft streaming is the only partial-stream surface.

Block streaming (channel messages)

Block streaming sends assistant output in coarse chunks as it becomes available.

Model output
  └─ text_delta/events
       ├─ (blockStreamingBreak=text_end)
       │    └─ chunker emits blocks as buffer grows
       └─ (blockStreamingBreak=message_end)
            └─ chunker flushes at message_end
                   └─ channel send (block replies)

Legend:

  • text_delta/events: model stream events (may be sparse for non-streaming models).
  • chunker: 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: dont emit until buffer >= minChars (unless forced).
  • High bound: prefer splits before maxChars; if forced, split at maxChars.
  • Break preference: paragraphnewlinesentencewhitespace → 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 cant 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 (8002500ms), 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.

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 for full behavior details.

Skills

When eligible skills exist, OpenClaw injects a compact available skills list (formatSkillsForPrompt) that includes the file path for each skill. The prompt instructs the model to use read to load the SKILL.md at the listed location (workspace, managed, or bundled). If no skills are eligible, the Skills section is omitted.

<available_skills>
  <skill>
    <name>...</name>
    <description>...</description>
    <location>...</location>
  </skill>
</available_skills>

This keeps the base prompt small while still enabling targeted skill usage.

Documentation

When available, the system prompt includes a Documentation section that points to the local OpenClaw docs directory (either docs/ in the repo workspace or the bundled npm package docs) and also notes the public mirror, source repo, community Discord, and ClawHub (https://clawhub.com) for skills discovery. The prompt instructs the model to consult local docs first for OpenClaw behavior, commands, configuration, or architecture, and to run openclaw status itself when possible (asking the user only when it lacks access).

Hooks

Source: https://docs.openclaw.ai/hooks

Hooks

Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in OpenClaw.

Getting Oriented

Hooks are small scripts that run when something happens. There are two kinds:

  • Hooks (this page): run inside the Gateway when agent events fire, like /new, /reset, /stop, or lifecycle events.
  • Webhooks: external HTTP webhooks that let other systems trigger work in OpenClaw. See Webhook Hooks or use openclaw webhooks for Gmail helper commands.

Hooks can also be bundled inside plugins; see Plugins.

Common uses:

  • Save a memory snapshot when you reset a session
  • Keep an audit trail of commands for troubleshooting or compliance
  • Trigger follow-up automation when a session starts or ends
  • Write files into the agent workspace or call external APIs when events fire

If you can write a small TypeScript function, you can write a hook. Hooks are discovered automatically, and you enable or disable them via the CLI.

Overview

The hooks system allows you to:

  • Save session context to memory when /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:

openclaw hooks list

Enable a hook:

openclaw hooks enable session-memory

Check hook status:

openclaw hooks check

Get detailed information:

openclaw hooks info session-memory

Onboarding

During onboarding (openclaw onboard), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection.

Hook Discovery

Hooks are automatically discovered from three directories (in order of precedence):

  1. Workspace hooks: <workspace>/hooks/ (per-agent, highest precedence)
  2. Managed hooks: ~/.openclaw/hooks/ (user-installed, shared across workspaces)
  3. Bundled hooks: <openclaw>/dist/hooks/bundled/ (shipped with OpenClaw)

Managed hook directories can be either a single hook or a hook pack (package directory).

Each hook is a directory containing:

my-hook/
├── HOOK.md          # Metadata + documentation
└── handler.ts       # Handler implementation

Hook Packs (npm/archives)

Hook packs are standard npm packages that export one or more hooks via openclaw.hooks in package.json. Install them with:

openclaw hooks install <path-or-spec>

Example package.json:

{
  "name": "@acme/my-hooks",
  "version": "0.1.0",
  "openclaw": {
    "hooks": ["./hooks/my-hook", "./hooks/other-hook"]
  }
}

Each entry points to a hook directory containing HOOK.md and handler.ts (or index.ts). Hook packs can ship dependencies; they will be installed under ~/.openclaw/hooks/<id>.

Hook Structure

HOOK.md Format

The HOOK.md file contains metadata in YAML frontmatter plus Markdown documentation:

---
name: my-hook
description: "Short description of what this hook does"
homepage: https://docs.openclaw.ai/hooks#my-hook
metadata:
  { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---

# My Hook

Detailed documentation goes here...

## What It Does

- Listens for `/new` commands
- Performs some action
- Logs the result

## Requirements

- Node.js must be installed

## Configuration

No configuration needed.

Metadata Fields

The metadata.openclaw object supports:

  • emoji: Display emoji for CLI (e.g., "💾")
  • events: Array of events to listen for (e.g., ["command:new", "command:reset"])
  • export: Named export to use (defaults to "default")
  • homepage: Documentation 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:

import type { HookHandler } from "../../src/hooks/hooks.js";

const myHandler: HookHandler = async (event) => {
  // Only trigger on 'new' command
  if (event.type !== "command" || event.action !== "new") {
    return;
  }

  console.log(`[my-hook] New command triggered`);
  console.log(`  Session: ${event.sessionKey}`);
  console.log(`  Timestamp: ${event.timestamp.toISOString()}`);

  // Your custom logic here

  // Optionally send message to user
  event.messages.push("✨ My hook executed!");
};

export default myHandler;

Event Context

Each event includes:

{
  type: 'command' | 'session' | 'agent' | 'gateway',
  action: string,              // e.g., 'new', 'reset', 'stop'
  sessionKey: string,          // Session identifier
  timestamp: Date,             // When the event occurred
  messages: string[],          // Push messages here to send to user
  context: {
    sessionEntry?: SessionEntry,
    sessionId?: string,
    sessionFile?: string,
    commandSource?: string,    // e.g., 'whatsapp', 'telegram'
    senderId?: string,
    workspaceDir?: string,
    bootstrapFiles?: WorkspaceBootstrapFile[],
    cfg?: OpenClawConfig
  }
}

Event Types

Command Events

Triggered when agent commands are issued:

  • command: All command events (general listener)
  • command:new: When /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.

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 (<workspace>/hooks/): Per-agent, highest precedence
  • Managed hooks (~/.openclaw/hooks/): Shared across workspaces

2. Create Directory Structure

mkdir -p ~/.openclaw/hooks/my-hook
cd ~/.openclaw/hooks/my-hook

3. Create HOOK.md

---
name: my-hook
description: "Does something useful"
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
---

# My Custom Hook

This hook does something useful when you issue `/new`.

4. Create handler.ts

import type { HookHandler } from "../../src/hooks/hooks.js";

const handler: HookHandler = async (event) => {
  if (event.type !== "command" || event.action !== "new") {
    return;
  }

  console.log("[my-hook] Running!");
  // Your logic here
};

export default handler;

5. Enable and Test

# Verify hook is discovered
openclaw hooks list

# Enable it
openclaw hooks enable my-hook

# Restart your gateway process (menu bar app restart on macOS, or restart your dev process)

# Trigger the event
# Send /new via your messaging channel

Configuration

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "session-memory": { "enabled": true },
        "command-logger": { "enabled": false }
      }
    }
  }
}

Per-Hook Configuration

Hooks can have custom configuration:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "my-hook": {
          "enabled": true,
          "env": {
            "MY_CUSTOM_VAR": "value"
          }
        }
      }
    }
  }
}

Extra Directories

Load hooks from additional directories:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "load": {
        "extraDirs": ["/path/to/more/hooks"]
      }
    }
  }
}

Legacy Config Format (Still Supported)

The old config format still works for backwards compatibility:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "handlers": [
        {
          "event": "command:new",
          "module": "./hooks/handlers/my-handler.ts",
          "export": "default"
        }
      ]
    }
  }
}

Migration: Use the new discovery-based system for new hooks. Legacy handlers are loaded after directory-based hooks.

CLI Commands

List Hooks

# List all hooks
openclaw hooks list

# Show only eligible hooks
openclaw hooks list --eligible

# Verbose output (show missing requirements)
openclaw hooks list --verbose

# JSON output
openclaw hooks list --json

Hook Information

# Show detailed info about a hook
openclaw hooks info session-memory

# JSON output
openclaw hooks info session-memory --json

Check Eligibility

# Show eligibility summary
openclaw hooks check

# JSON output
openclaw hooks check --json

Enable/Disable

# Enable a hook
openclaw hooks enable session-memory

# Disable a hook
openclaw hooks disable command-logger

Bundled Hooks

session-memory

Saves session context to memory when you issue /new.

Events: command:new

Requirements: workspace.dir must be configured

Output: <workspace>/memory/YYYY-MM-DD-slug.md (defaults to ~/.openclaw/workspace)

What it does:

  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:

# 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:

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:

{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"}
{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"user@example.com","source":"whatsapp"}

View logs:

# View recent commands
tail -n 20 ~/.openclaw/logs/commands.log

# Pretty-print with jq
cat ~/.openclaw/logs/commands.log | jq .

# Filter by action
grep '"action":"new"' ~/.openclaw/logs/commands.log | jq .

Enable:

openclaw hooks enable command-logger

soul-evil

Swaps injected SOUL.md content with SOUL_EVIL.md during a purge window or by random chance.

Events: agent:bootstrap

Docs: SOUL Evil Hook

Output: No files written; swaps happen in-memory only.

Enable:

openclaw hooks enable soul-evil

Config:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "soul-evil": {
          "enabled": true,
          "file": "SOUL_EVIL.md",
          "chance": 0.1,
          "purge": { "at": "21:00", "duration": "15m" }
        }
      }
    }
  }
}

boot-md

Runs BOOT.md when the gateway starts (after channels start). Internal hooks must be enabled for this to run.

Events: gateway:startup

Requirements: workspace.dir must be configured

What it does:

  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:

openclaw hooks enable boot-md

Best Practices

Keep Handlers Fast

Hooks run during command processing. Keep them lightweight:

// ✓ Good - async work, returns immediately
const handler: HookHandler = async (event) => {
  void processInBackground(event); // Fire and forget
};

// ✗ Bad - blocks command processing
const handler: HookHandler = async (event) => {
  await slowDatabaseQuery(event);
  await evenSlowerAPICall(event);
};

Handle Errors Gracefully

Always wrap risky operations:

const handler: HookHandler = async (event) => {
  try {
    await riskyOperation(event);
  } catch (err) {
    console.error("[my-handler] Failed:", err instanceof Error ? err.message : String(err));
    // Don't throw - let other handlers run
  }
};

Filter Events Early

Return early if the event isn't relevant:

const handler: HookHandler = async (event) => {
  // Only handle 'new' commands
  if (event.type !== "command" || event.action !== "new") {
    return;
  }

  // Your logic here
};

Use Specific Event Keys

Specify exact events in metadata when possible:

metadata: { "openclaw": { "events": ["command:new"] } } # Specific

Rather than:

metadata: { "openclaw": { "events": ["command"] } } # General - more overhead

Debugging

Enable Hook Logging

The gateway logs hook loading at startup:

Registered hook: session-memory -> command:new
Registered hook: command-logger -> command
Registered hook: boot-md -> gateway:startup

Check Discovery

List all discovered hooks:

openclaw hooks list --verbose

Check Registration

In your handler, log when it's called:

const handler: HookHandler = async (event) => {
  console.log("[my-handler] Triggered:", event.type, event.action);
  // Your logic
};

Verify Eligibility

Check why a hook isn't eligible:

openclaw hooks info my-hook

Look for missing requirements in the output.

Testing

Gateway Logs

Monitor gateway logs to see hook execution:

# macOS
./scripts/clawlog.sh -f

# Other platforms
tail -f ~/.openclaw/gateway.log

Test Hooks Directly

Test your handlers in isolation:

import { test } from "vitest";
import { createHookEvent } from "./src/hooks/hooks.js";
import myHandler from "./hooks/my-hook/handler.js";

test("my handler works", async () => {
  const event = createHookEvent("command", "new", "test-session", {
    foo: "bar",
  });

  await myHandler(event);

  // Assert side effects
});

Architecture

Core Components

  • src/hooks/types.ts: Type 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:

    ls -la ~/.openclaw/hooks/my-hook/
    # Should show: HOOK.md, handler.ts
    
  2. Verify HOOK.md format:

    cat ~/.openclaw/hooks/my-hook/HOOK.md
    # Should have YAML frontmatter with name and metadata
    
  3. List all discovered hooks:

    openclaw hooks list
    

Hook Not Eligible

Check requirements:

openclaw hooks info my-hook

Look for missing:

  • Binaries (check PATH)
  • Environment variables
  • Config values
  • OS compatibility

Hook Not Executing

  1. Verify hook is enabled:

    openclaw hooks list
    # Should show ✓ next to enabled hooks
    
  2. Restart your gateway process so hooks reload.

  3. Check gateway logs for errors:

    ./scripts/clawlog.sh | grep hook
    

Handler Errors

Check for TypeScript/import errors:

# Test import directly
node -e "import('./path/to/handler.ts').then(console.log)"

Migration Guide

From Legacy Config to Discovery

Before:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "handlers": [
        {
          "event": "command:new",
          "module": "./hooks/handlers/my-handler.ts"
        }
      ]
    }
  }
}

After:

  1. Create hook directory:

    mkdir -p ~/.openclaw/hooks/my-hook
    mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts
    
  2. Create HOOK.md:

    ---
    name: my-hook
    description: "My custom hook"
    metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
    ---
    
    # My Hook
    
    Does something useful.
    
  3. Update config:

    {
      "hooks": {
        "internal": {
          "enabled": true,
          "entries": {
            "my-hook": { "enabled": true }
          }
        }
      }
    }
    
  4. Verify and restart your gateway process:

    openclaw hooks list
    # Should show: 🎯 my-hook ✓
    

Benefits of migration:

  • Automatic discovery
  • CLI management
  • Eligibility checking
  • Better documentation
  • Consistent structure

See Also

SOUL Evil Hook

Source: https://docs.openclaw.ai/hooks/soul-evil

SOUL Evil Hook

The SOUL Evil hook swaps the injected SOUL.md content with SOUL_EVIL.md during a purge window or by random chance. It does not modify files on disk.

How It Works

When agent:bootstrap runs, the hook can replace the SOUL.md content in memory before the system prompt is assembled. If SOUL_EVIL.md is missing or empty, OpenClaw logs a warning and keeps the normal SOUL.md.

Sub-agent runs do not include SOUL.md in their bootstrap files, so this hook has no effect on sub-agents.

Enable

openclaw hooks enable soul-evil

Then set the config:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "soul-evil": {
          "enabled": true,
          "file": "SOUL_EVIL.md",
          "chance": 0.1,
          "purge": { "at": "21:00", "duration": "15m" }
        }
      }
    }
  }
}

Create SOUL_EVIL.md in the agent workspace root (next to SOUL.md).

Options

  • file (string): alternate SOUL filename (default: SOUL_EVIL.md)
  • chance (number 01): 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

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

flowchart LR
  A["Chat apps + plugins"] --> B["Gateway"]
  B --> C["Pi agent"]
  B --> D["CLI"]
  B --> E["Web Control UI"]
  B --> F["macOS app"]
  B --> G["iOS and Android nodes"]

The Gateway is the single source of truth for sessions, routing, and channel connections.

Key capabilities

WhatsApp, Telegram, Discord, and iMessage with a single Gateway process. Add Mattermost and more with extension packages. Isolated sessions per agent, workspace, or sender. Send and receive images, audio, and documents. Browser dashboard for chat, config, sessions, and nodes. Pair iOS and Android nodes with Canvas support.

Quick start

```bash theme={null} npm install -g openclaw@latest ``` ```bash theme={null} openclaw onboard --install-daemon ``` ```bash theme={null} openclaw channels login openclaw gateway --port 18789 ```

Need the full install and dev setup? See Quick start.

Dashboard

Open the browser Control UI after the Gateway starts.

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:

{
  channels: {
    whatsapp: {
      allowFrom: ["+15555550123"],
      groups: { "*": { requireMention: true } },
    },
  },
  messages: { groupChat: { mentionPatterns: ["@openclaw"] } },
}

Start here

All docs and guides, organized by use case. Core Gateway settings, tokens, and provider config. SSH and tailnet access patterns. Channel-specific setup for WhatsApp, Telegram, Discord, and more. iOS and Android nodes with pairing and Canvas. Common fixes and troubleshooting entry point.

Learn more

Complete channel, routing, and media capabilities. Workspace isolation and per-agent sessions. Tokens, allowlists, and safety controls. Gateway diagnostics and common errors. Project origins, contributors, and license.

Ansible

Source: https://docs.openclaw.ai/install/ansible

Ansible Installation

The recommended way to deploy OpenClaw to production servers is via openclaw-ansible — an automated installer with security-first architecture.

Quick Start

One-command install:

curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash

📦 Full guide: github.com/openclaw/openclaw-ansible

The openclaw-ansible repo is the source of truth for Ansible deployment. This page is a quick overview.

What You Get

  • 🔒 Firewall-first security: UFW + Docker isolation (only SSH + Tailscale accessible)
  • 🔐 Tailscale VPN: Secure remote access without exposing services publicly
  • 🐳 Docker: Isolated sandbox containers, localhost-only bindings
  • 🛡️ Defense in depth: 4-layer security architecture
  • 🚀 One-command setup: Complete deployment in minutes
  • 🔧 Systemd integration: Auto-start on boot with hardening

Requirements

  • OS: Debian 11+ or Ubuntu 20.04+
  • Access: Root or sudo privileges
  • Network: Internet connection for package installation
  • Ansible: 2.14+ (installed automatically by quick-start script)

What Gets Installed

The Ansible playbook installs and configures:

  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 for details.

Post-Install Setup

After installation completes, switch to the openclaw user:

sudo -i -u openclaw

The post-install script will guide you through:

  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

# 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:

nmap -p- YOUR_SERVER_IP

Should show only port 22 (SSH) open. All other services (gateway, Docker) are locked down.

Docker Availability

Docker is installed for agent sandboxes (isolated tool execution), not for running the gateway itself. The gateway binds to localhost only and is accessible via Tailscale VPN.

See Multi-Agent Sandbox & Tools for sandbox configuration.

Manual Installation

If you prefer manual control over the automation:

# 1. Install prerequisites
sudo apt update && sudo apt install -y ansible git

# 2. Clone repository
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible

# 3. Install Ansible collections
ansible-galaxy collection install -r requirements.yml

# 4. Run playbook
./run-playbook.sh

# Or run directly (then manually execute /tmp/openclaw-setup.sh after)
# ansible-playbook playbook.yml --ask-become-pass

Updating OpenClaw

The Ansible installer sets up OpenClaw for manual updates. See Updating for the standard update flow.

To re-run the Ansible playbook (e.g., for configuration changes):

cd openclaw-ansible
./run-playbook.sh

Note: This is idempotent and safe to run multiple times.

Troubleshooting

Firewall blocks my connection

If you're locked out:

  • Ensure you can access via Tailscale VPN first
  • SSH access (port 22) is always allowed
  • The gateway is only accessible via Tailscale by design

Service won't start

# Check logs
sudo journalctl -u openclaw -n 100

# Verify permissions
sudo ls -la /opt/openclaw

# Test manual start
sudo -i -u openclaw
cd ~/openclaw
pnpm start

Docker sandbox issues

# Verify Docker is running
sudo systemctl status docker

# Check sandbox image
sudo docker images | grep openclaw-sandbox

# Build sandbox image if missing
cd /opt/openclaw/openclaw
sudo -u openclaw ./scripts/sandbox-setup.sh

Provider login fails

Make sure you're running as the openclaw user:

sudo -i -u openclaw
openclaw channels login

Advanced Configuration

For detailed security architecture and troubleshooting:

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:

bun install

Note: bun.lock/bun.lockb are gitignored, so theres no repo churn either way. If you want no lockfile writes:

bun install --no-save

Build / Test (Bun)

bun run build
bun run vitest run

Bun lifecycle scripts (blocked by default)

Bun may block dependency lifecycle scripts unless explicitly trusted (bun pm untrusted / bun pm trust). For this repo, the commonly blocked scripts are not required:

  • @whiskeysockets/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:

bun pm trust @whiskeysockets/baileys protobufjs

Caveats

  • Some scripts still hardcode pnpm (e.g. docs:build, ui:*, protocol:check). Run those via pnpm for now.

Development Channels

Source: https://docs.openclaw.ai/install/development-channels

Development channels

Last updated: 2026-01-21

OpenClaw ships three update channels:

  • stable: npm dist-tag latest.
  • beta: npm dist-tag beta (builds under test).
  • dev: moving head of main (git). npm dist-tag: dev (when published).

We ship builds to beta, test them, then promote a vetted build to latest without changing the version number — dist-tags are the source of truth for npm installs.

Switching channels

Git checkout:

openclaw update --channel stable
openclaw update --channel beta
openclaw update --channel dev
  • stable/beta check out the latest matching tag (often the same tag).
  • dev switches to main and rebases on the upstream.

npm/pnpm global install:

openclaw update --channel stable
openclaw update --channel beta
openclaw update --channel dev

This updates via the corresponding npm dist-tag (latest, beta, dev).

When you explicitly switch channels with --channel, OpenClaw also aligns the install method:

  • 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-<patch>).
  • 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. Thats 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: youre running on your own machine and just want the fastest dev loop. Use the normal install flow instead.
  • Sandboxing note: agent sandboxing uses Docker too, but it does not require the full gateway to run in Docker. See Sandboxing.

This guide covers:

  • Containerized Gateway (full OpenClaw in Docker)
  • Per-session Agent Sandbox (host gateway + Docker-isolated agent tools)

Sandboxing details: Sandboxing

Requirements

  • Docker Desktop (or Docker Engine) + Docker Compose v2
  • Enough disk for images + logs

Containerized Gateway (Docker Compose)

From repo root:

./docker-setup.sh

This script:

  • builds the gateway image
  • runs the onboarding wizard
  • prints optional provider setup hints
  • starts the gateway via Docker Compose
  • generates a gateway token and writes it to .env

Optional env vars:

  • OPENCLAW_DOCKER_APT_PACKAGES — install extra apt packages during 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).

Manual flow (compose)

docker build -t openclaw:local -f Dockerfile .
docker compose run --rm openclaw-cli onboard
docker compose up -d openclaw-gateway

Note: run docker compose ... from the repo root. If you enabled OPENCLAW_EXTRA_MOUNTS or OPENCLAW_HOME_VOLUME, the setup script writes docker-compose.extra.yml; include it when running Compose elsewhere:

docker compose -f docker-compose.yml -f docker-compose.extra.yml <command>

Control UI token + pairing (Docker)

If you see “unauthorized” or “disconnected (1008): pairing required”, fetch a fresh dashboard link and approve the browser device:

docker compose run --rm openclaw-cli dashboard --no-open
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>

More detail: Dashboard, Devices.

Extra mounts (optional)

If you want to mount additional host directories into the containers, set OPENCLAW_EXTRA_MOUNTS before running docker-setup.sh. This accepts a comma-separated list of Docker bind mounts and applies them to both openclaw-gateway and openclaw-cli by generating docker-compose.extra.yml.

Example:

export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw"
./docker-setup.sh

Notes:

  • Paths must be shared with Docker Desktop on macOS/Windows.
  • If you edit OPENCLAW_EXTRA_MOUNTS, rerun docker-setup.sh to regenerate the extra compose file.
  • docker-compose.extra.yml is generated. Dont hand-edit it.

Persist the entire container home (optional)

If you want /home/node to persist across container recreation, set a named volume via OPENCLAW_HOME_VOLUME. This creates a Docker volume and mounts it at /home/node, while keeping the standard config/workspace bind mounts. Use a named volume here (not a bind path); for bind mounts, use OPENCLAW_EXTRA_MOUNTS.

Example:

export OPENCLAW_HOME_VOLUME="openclaw_home"
./docker-setup.sh

You can combine this with extra mounts:

export OPENCLAW_HOME_VOLUME="openclaw_home"
export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw"
./docker-setup.sh

Notes:

  • If you change OPENCLAW_HOME_VOLUME, rerun docker-setup.sh to regenerate the extra compose file.
  • The named volume persists until removed with docker volume rm <name>.

Install extra apt packages (optional)

If you need system packages inside the image (for example, build tools or media libraries), set OPENCLAW_DOCKER_APT_PACKAGES before running docker-setup.sh. This installs the packages during the image build, so they persist even if the container is deleted.

Example:

export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential"
./docker-setup.sh

Notes:

  • This accepts a space-separated list of apt package names.
  • If you change OPENCLAW_DOCKER_APT_PACKAGES, rerun docker-setup.sh to rebuild the image.

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:
export OPENCLAW_HOME_VOLUME="openclaw_home"
./docker-setup.sh
  1. Bake system deps into the image (repeatable + persistent):
export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq"
./docker-setup.sh
  1. Install Playwright browsers without npx (avoids npm override conflicts):
docker compose run --rm openclaw-cli \
  node /app/node_modules/playwright-core/cli.js install chromium

If you need Playwright to install system deps, rebuild the image with OPENCLAW_DOCKER_APT_PACKAGES instead of using --with-deps at runtime.

  1. 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):

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.

To speed up rebuilds, order your Dockerfile so dependency layers are cached. This avoids re-running pnpm install unless lockfiles change:

FROM node:22-bookworm

# Install Bun (required for build scripts)
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"

RUN corepack enable

WORKDIR /app

# Cache dependencies unless package metadata changes
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY ui/package.json ./ui/package.json
COPY scripts ./scripts

RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build
RUN pnpm ui:install
RUN pnpm ui:build

ENV NODE_ENV=production

CMD ["node","dist/index.js"]

Channel setup (optional)

Use the CLI container to configure channels, then restart the gateway if needed.

WhatsApp (QR):

docker compose run --rm openclaw-cli channels login

Telegram (bot token):

docker compose run --rm openclaw-cli channels add --channel telegram --token "<token>"

Discord (bot token):

docker compose run --rm openclaw-cli channels add --channel discord --token "<token>"

Docs: WhatsApp, Telegram, Discord

OpenAI Codex OAuth (headless Docker)

If you pick OpenAI Codex OAuth in the wizard, it opens a browser URL and tries to capture a callback on http://127.0.0.1:1455/auth/callback. In Docker or headless setups that callback can show a browser error. Copy the full redirect URL you land on and paste it back into the wizard to finish auth.

Health check

docker compose exec openclaw-gateway node dist/index.js health --token "$OPENCLAW_GATEWAY_TOKEN"

E2E smoke test (Docker)

scripts/e2e/onboard-docker.sh

QR import smoke test (Docker)

pnpm test:docker:qr

Notes

  • Gateway bind defaults to 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/<agentId>/sessions/).

Agent Sandbox (host gateway + Docker tools)

Deep dive: Sandboxing

What it does

When agents.defaults.sandbox is enabled, non-main sessions run tools inside a Docker container. The gateway stays on your host, but the tool execution is isolated:

  • scope: "agent" by default (one container + workspace per agent)
  • scope: "session" for per-session isolation
  • per-scope workspace folder mounted at /workspace
  • optional agent workspace access (agents.defaults.sandbox.workspaceAccess)
  • allow/deny tool policy (deny wins)
  • inbound media is copied into the active sandbox workspace (media/inbound/*) so tools can read it (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 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.
{
  agents: {
    defaults: {
      sandbox: {
        mode: "non-main", // off | non-main | all
        scope: "agent", // session | agent | shared (agent is default)
        workspaceAccess: "none", // none | ro | rw
        workspaceRoot: "~/.openclaw/sandboxes",
        docker: {
          image: "openclaw-sandbox:bookworm-slim",
          workdir: "/workspace",
          readOnlyRoot: true,
          tmpfs: ["/tmp", "/var/tmp", "/run"],
          network: "none",
          user: "1000:1000",
          capDrop: ["ALL"],
          env: { LANG: "C.UTF-8" },
          setupCommand: "apt-get update && apt-get install -y git curl jq",
          pidsLimit: 256,
          memory: "1g",
          memorySwap: "2g",
          cpus: 1,
          ulimits: {
            nofile: { soft: 1024, hard: 2048 },
            nproc: 256,
          },
          seccompProfile: "/path/to/seccomp.json",
          apparmorProfile: "openclaw-sandbox",
          dns: ["1.1.1.1", "8.8.8.8"],
          extraHosts: ["internal.service:10.0.0.5"],
        },
        prune: {
          idleHours: 24, // 0 disables idle pruning
          maxAgeDays: 7, // 0 disables max-age pruning
        },
      },
    },
  },
  tools: {
    sandbox: {
      tools: {
        allow: [
          "exec",
          "process",
          "read",
          "write",
          "edit",
          "sessions_list",
          "sessions_history",
          "sessions_send",
          "sessions_spawn",
          "session_status",
        ],
        deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"],
      },
    },
  },
}

Hardening knobs live under agents.defaults.sandbox.docker: network, user, pidsLimit, memory, memorySwap, cpus, ulimits, seccompProfile, apparmorProfile, dns, extraHosts.

Multi-agent: override agents.defaults.sandbox.{docker,browser,prune}.* per agent via agents.list[].sandbox.{docker,browser,prune}.* (ignored when agents.defaults.sandbox.scope / agents.list[].sandbox.scope is "shared").

Build the default sandbox image

scripts/sandbox-setup.sh

This builds openclaw-sandbox:bookworm-slim using Dockerfile.sandbox.

Sandbox common image (optional)

If you want a sandbox image with common build tooling (Node, Go, Rust, etc.), build the common image:

scripts/sandbox-common-setup.sh

This builds openclaw-sandbox-common:bookworm-slim. To use it:

{
  agents: {
    defaults: {
      sandbox: { docker: { image: "openclaw-sandbox-common:bookworm-slim" } },
    },
  },
}

Sandbox browser image

To run the browser tool inside the sandbox, build the browser image:

scripts/sandbox-browser-setup.sh

This builds openclaw-sandbox-browser:bookworm-slim using Dockerfile.sandbox-browser. The container runs Chromium with CDP enabled and an optional noVNC observer (headful via Xvfb).

Notes:

  • Headful (Xvfb) reduces bot blocking vs headless.
  • Headless can still be used by setting agents.defaults.sandbox.browser.headless=true.
  • No full desktop environment (GNOME) is needed; Xvfb provides the display.

Use config:

{
  agents: {
    defaults: {
      sandbox: {
        browser: { enabled: true },
      },
    },
  },
}

Custom browser image:

{
  agents: {
    defaults: {
      sandbox: { browser: { image: "my-openclaw-browser" } },
    },
  },
}

When enabled, the agent receives:

  • a sandbox browser control URL (for the 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:

docker build -t my-openclaw-sbx -f Dockerfile.sandbox .
{
  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 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.

curl -fsSL https://openclaw.ai/install.sh | bash

Windows (PowerShell):

iwr -useb https://openclaw.ai/install.ps1 | iex

Next step (if you skipped onboarding):

openclaw onboard --install-daemon

System requirements

  • Node >=22
  • macOS, Linux, or Windows via WSL2
  • pnpm only if you build from source

Choose your install path

Installs openclaw globally via npm and runs onboarding.

curl -fsSL https://openclaw.ai/install.sh | bash

Installer flags:

curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help

Details: Installer internals.

Non-interactive (skip onboarding):

curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard

2) Global install (manual)

If you already have Node:

npm install -g openclaw@latest

If you have libvips installed globally (common on macOS via Homebrew) and sharp fails to install, force prebuilt binaries:

SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest

If you see sharp: Please add node-gyp to your dependencies, either install build tooling (macOS: Xcode CLT + npm install -g node-gyp) or use the SHARP_IGNORE_GLOBAL_LIBVIPS=1 workaround above to skip the native build.

Or with pnpm:

pnpm add -g openclaw@latest
pnpm approve-builds -g                # approve openclaw, node-llama-cpp, sharp, etc.

pnpm requires explicit approval for packages with build scripts. After the first install shows the "Ignored build scripts" warning, run pnpm approve-builds -g and select the listed packages.

Then:

openclaw onboard --install-daemon

3) From source (contributors/dev)

git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon

Tip: if you dont have a global install yet, run repo commands via pnpm openclaw ....

4) Other install options

After install

  • Run onboarding: openclaw onboard --install-daemon
  • Quick check: openclaw doctor
  • Check gateway health: openclaw status + openclaw health
  • Open the dashboard: openclaw dashboard

Install method: npm vs git (installer)

The installer supports two methods:

  • npm (default): npm install -g openclaw@latest
  • git: clone/build from GitHub and run from a source checkout

CLI flags

# Explicit npm
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm

# Install from GitHub (source checkout)
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git

Common flags:

  • --install-method npm|git
  • --git-dir <path> (default: ~/openclaw)
  • --no-git-update (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:

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 cant find global npm binaries (including openclaw).

Fix: add it to your shell startup file (zsh: ~/.zshrc, bash: ~/.bashrc):

# macOS / Linux
export PATH="$(npm prefix -g)/bin:$PATH"

On Windows, add the output of npm prefix -g to your PATH.

Then open a new terminal (or rehash in zsh / hash -r in bash).

Update / uninstall

Installer Internals

Source: https://docs.openclaw.ai/install/installer

Installer internals

OpenClaw ships two installer scripts (served from openclaw.ai):

  • https://openclaw.ai/install.sh — “recommended” installer (global npm install by default; can also install from a GitHub checkout)
  • https://openclaw.ai/install-cli.sh — non-root-friendly CLI installer (installs into a prefix with its own Node)
  • https://openclaw.ai/install.ps1 — Windows PowerShell installer (npm by default; optional git install)

To see the current flags/behavior, run:

curl -fsSL https://openclaw.ai/install.sh | bash -s -- --help

Windows (PowerShell) help:

& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -?

If the installer completes but openclaw is not found in a new terminal, its usually a Node/npm PATH issue. See: Install.

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 youre debugging), set:

SHARP_IGNORE_GLOBAL_LIBVIPS=0 curl -fsSL https://openclaw.ai/install.sh | bash

Discoverability / “git install” prompt

If you run the installer while already inside a OpenClaw source checkout (detected via package.json + pnpm-workspace.yaml), it prompts:

  • update and use this checkout (git)
  • or migrate to the global npm install (npm)

In non-interactive contexts (no TTY / --no-prompt), you must pass --install-method git|npm (or set OPENCLAW_INSTALL_METHOD), otherwise the script exits with code 2.

Why Git is needed

Git is required for the --install-method git path (clone / pull).

For npm installs, Git is usually not required, but some environments still end up needing it (e.g. when a package or dependency is fetched via a git URL). The installer currently ensures Git is present to avoid spawn git ENOENT surprises on fresh distros.

Why npm hits EACCES on fresh Linux

On some Linux setups (especially after installing Node via the system package manager or NodeSource), npm's global prefix points at a root-owned location. Then npm install -g ... fails with EACCES / mkdir permission errors.

install.sh mitigates this by switching the prefix to:

  • ~/.npm-global (and adding it 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 dont want to touch the system Node/npm.

Help:

curl -fsSL https://openclaw.ai/install-cli.sh | bash -s -- --help

install.ps1 (Windows PowerShell)

What it does (high level):

  • Ensure Node.js 22+ (winget/Chocolatey/Scoop or manual).
  • Choose install method:
    • npm (default): npm install -g openclaw@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:

iwr -useb https://openclaw.ai/install.ps1 | iex
iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git
iwr -useb https://openclaw.ai/install.ps1 | iex -InstallMethod git -GitDir "C:\\openclaw"

Environment variables:

  • OPENCLAW_INSTALL_METHOD=git|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 — a batteries-included Home Manager module.

Quick Start

Paste this to your AI agent (Claude, Cursor, etc.):

I want to set up nix-openclaw on my Mac.
Repository: github:openclaw/nix-openclaw

What I need you to do:
1. Check if Determinate Nix is installed (if not, install it)
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
4. Set up secrets (bot token, Anthropic key) - plain files at ~/.secrets/ is fine
5. Fill in the template placeholders and run home-manager switch
6. Verify: launchd running, bot responds to messages

Reference the nix-openclaw README for module options.

📦 Full guide: github.com/openclaw/nix-openclaw

The nix-openclaw repo is the source of truth for Nix installation. This page is just a quick overview.

What you get

  • Gateway + macOS app + tools (whisper, spotify, cameras) — all pinned
  • Launchd service that survives reboots
  • Plugin system with declarative config
  • Instant rollback: home-manager switch --rollback

Nix Mode Runtime Behavior

When OPENCLAW_NIX_MODE=1 is set (automatic with nix-openclaw):

OpenClaw supports a Nix mode that makes configuration deterministic and disables auto-install flows. Enable it by exporting:

OPENCLAW_NIX_MODE=1

On macOS, the GUI app does not automatically inherit shell env vars. You can also enable Nix mode via defaults:

defaults write bot.molt.mac openclaw.nixMode -bool true

Config + state paths

OpenClaw reads JSON5 config from OPENCLAW_CONFIG_PATH and stores mutable data in OPENCLAW_STATE_DIR.

  • OPENCLAW_STATE_DIR (default: ~/.openclaw)
  • OPENCLAW_CONFIG_PATH (default: $OPENCLAW_STATE_DIR/openclaw.json)

When running under Nix, set these explicitly to Nix-managed locations so runtime state and config stay out of the immutable store.

Runtime behavior in Nix mode

  • Auto-install and self-mutation flows are disabled
  • Missing dependencies surface Nix-specific remediation messages
  • UI surfaces a read-only Nix mode banner when present

Packaging note (macOS)

The macOS packaging flow expects a stable Info.plist template at:

apps/macos/Sources/OpenClaw/Resources/Info.plist

scripts/package-mac-app.sh copies this template into the app bundle and patches dynamic fields (bundle ID, version/build, Git SHA, Sparkle keys). This keeps the plist deterministic for SwiftPM packaging and Nix builds (which do not rely on a full Xcode toolchain).

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:

openclaw uninstall

Non-interactive (automation / npx):

openclaw uninstall --all --yes --non-interactive
npx -y openclaw uninstall --all --yes --non-interactive

Manual steps (same result):

  1. Stop the gateway service:
openclaw gateway stop
  1. Uninstall the gateway service (launchd/systemd/schtasks):
openclaw gateway uninstall
  1. Delete state + config:
rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"

If you set OPENCLAW_CONFIG_PATH to a custom location outside the state dir, delete that file too.

  1. Delete your workspace (optional, removes agent files):
rm -rf ~/.openclaw/workspace
  1. Remove the CLI install (pick the one you used):
npm rm -g openclaw
pnpm remove -g openclaw
bun remove -g openclaw
  1. If you installed the macOS app:
rm -rf /Applications/OpenClaw.app

Notes:

  • If you used profiles (--profile / OPENCLAW_PROFILE), repeat step 3 for each state dir (defaults are ~/.openclaw-<profile>).
  • In remote mode, the state dir lives on the gateway host, so run steps 1-4 there too.

Manual service removal (CLI not installed)

Use this if the gateway service keeps running but openclaw is missing.

macOS (launchd)

Default label is bot.molt.gateway (or bot.molt.<profile>; legacy com.openclaw.* may still exist):

launchctl bootout gui/$UID/bot.molt.gateway
rm -f ~/Library/LaunchAgents/bot.molt.gateway.plist

If you used a profile, replace the label and plist name with bot.molt.<profile>. Remove any legacy com.openclaw.* plists if present.

Linux (systemd user unit)

Default unit name is openclaw-gateway.service (or openclaw-gateway-<profile>.service):

systemctl --user disable --now openclaw-gateway.service
rm -f ~/.config/systemd/user/openclaw-gateway.service
systemctl --user daemon-reload

Windows (Scheduled Task)

Default task name is OpenClaw Gateway (or OpenClaw Gateway (<profile>)). The task script lives under your state dir.

schtasks /Delete /F /TN "OpenClaw Gateway"
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd"

If you used a profile, delete the matching task name and ~\.openclaw-<profile>\gateway.cmd.

Normal install vs source checkout

Normal install (install.sh / npm / pnpm / bun)

If you used https://openclaw.ai/install.sh or install.ps1, the CLI was installed with npm install -g openclaw@latest. Remove it with npm rm -g openclaw (or pnpm remove -g / bun remove -g if you installed that way).

Source checkout (git clone)

If you run from a repo checkout (git clone + openclaw ... / bun run openclaw ...):

  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.

The preferred update path is to re-run the installer from the website. It detects existing installs, upgrades in place, and runs openclaw doctor when needed.

curl -fsSL https://openclaw.ai/install.sh | bash

Notes:

  • Add --no-onboard if you dont want the onboarding wizard to run again.
  • For source installs, use:
    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):

npm i -g openclaw@latest
pnpm add -g openclaw@latest

We do not recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).

To switch update channels (git + npm installs):

openclaw update --channel beta
openclaw update --channel dev
openclaw update --channel stable

Use --tag <dist-tag|version> for a one-off install tag/version.

See Development channels for channel semantics and release notes.

Note: on npm installs, the gateway logs an update hint on startup (checks the current channel tag). Disable via update.checkOnStart: false.

Then:

openclaw doctor
openclaw gateway restart
openclaw health

Notes:

  • If your Gateway runs as a service, openclaw gateway restart is preferred over killing PIDs.
  • If youre pinned to a specific version, see “Rollback / pinning” below.

Update (openclaw update)

For source installs (git checkout), prefer:

openclaw update

It runs a safe-ish update flow:

  • Requires a clean worktree.
  • Switches to the selected channel (tag or branch).
  • Fetches + rebases against the configured upstream (dev channel).
  • Installs deps, builds, builds the Control UI, and runs openclaw doctor.
  • Restarts the gateway by default (use --no-restart to skip).

If you installed via npm/pnpm (no git metadata), openclaw update will try to update via your package manager. If it cant 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:

openclaw update

Manual (equivalent-ish):

git pull
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
openclaw doctor
openclaw health

Notes:

  • pnpm build matters when you run the packaged openclaw binary (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. Its intentionally boring: repair + migrate + warn.

Note: if youre on a source install (git checkout), openclaw doctor will offer to run openclaw update first.

Typical things it does:

  • Migrate deprecated config keys / legacy config file locations.
  • Audit DM policies and warn on risky “open” settings.
  • Check Gateway health and can offer to restart.
  • Detect and migrate older gateway services (launchd/systemd; legacy schtasks) to current OpenClaw services.
  • On Linux, ensure systemd user lingering (so the Gateway survives logout).

Details: Doctor

Start / stop / restart the Gateway

CLI (works regardless of OS):

openclaw gateway status
openclaw gateway stop
openclaw gateway restart
openclaw gateway --port 18789
openclaw logs --follow

If youre supervised:

  • macOS launchd (app-bundled LaunchAgent): launchctl kickstart -k gui/$UID/bot.molt.gateway (use bot.molt.<profile>; legacy com.openclaw.* still works)
  • Linux systemd user service: systemctl --user restart openclaw-gateway[-<profile>].service
  • Windows (WSL2): systemctl --user restart openclaw-gateway[-<profile>].service
    • launchctl/systemctl only work if the service is installed; otherwise run openclaw gateway install.

Runbook + exact service labels: Gateway runbook

Rollback / pinning (when something breaks)

Pin (global install)

Install a known-good version (replace <version> with the last working one):

npm i -g openclaw@<version>
pnpm add -g openclaw@<version>

Tip: to see the current published version, run npm view openclaw version.

Then restart + re-run doctor:

openclaw doctor
openclaw gateway restart

Pin (source) by date

Pick a commit from a date (example: “state of main as of 2026-01-01”):

git fetch origin
git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)"

Then reinstall deps + restart:

pnpm install
pnpm build
openclaw gateway restart

If you want to go back to latest later:

git checkout main
git pull

If youre stuck

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/<agentId>/agent/auth-profiles.json

Credentials are not shared between agents. Never reuse agentDir across agents. If you want to share creds, copy auth-profiles.json into the other agent's agentDir.

For how sandboxing behaves at runtime, see Sandboxing. For debugging “why is this blocked?”, see Sandbox vs Tool Policy vs Elevated and openclaw sandbox explain.


Configuration Examples

Example 1: Personal + Restricted Family Agent

{
  "agents": {
    "list": [
      {
        "id": "main",
        "default": true,
        "name": "Personal Assistant",
        "workspace": "~/.openclaw/workspace",
        "sandbox": { "mode": "off" }
      },
      {
        "id": "family",
        "name": "Family Bot",
        "workspace": "~/.openclaw/workspace-family",
        "sandbox": {
          "mode": "all",
          "scope": "agent"
        },
        "tools": {
          "allow": ["read"],
          "deny": ["exec", "write", "edit", "apply_patch", "process", "browser"]
        }
      }
    ]
  },
  "bindings": [
    {
      "agentId": "family",
      "match": {
        "provider": "whatsapp",
        "accountId": "*",
        "peer": {
          "kind": "group",
          "id": "120363424282127706@g.us"
        }
      }
    }
  ]
}

Result:

  • 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

{
  "agents": {
    "list": [
      {
        "id": "personal",
        "workspace": "~/.openclaw/workspace-personal",
        "sandbox": { "mode": "off" }
      },
      {
        "id": "work",
        "workspace": "~/.openclaw/workspace-work",
        "sandbox": {
          "mode": "all",
          "scope": "shared",
          "workspaceRoot": "/tmp/work-sandboxes"
        },
        "tools": {
          "allow": ["read", "write", "apply_patch", "exec"],
          "deny": ["browser", "gateway", "discord"]
        }
      }
    ]
  }
}

Example 2b: Global coding profile + messaging-only agent

{
  "tools": { "profile": "coding" },
  "agents": {
    "list": [
      {
        "id": "support",
        "tools": { "profile": "messaging", "allow": ["slack"] }
      }
    ]
  }
}

Result:

  • default agents get coding tools
  • support agent is messaging-only (+ Slack tool)

Example 3: Different Sandbox Modes per Agent

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "non-main", // Global default
        "scope": "session"
      }
    },
    "list": [
      {
        "id": "main",
        "workspace": "~/.openclaw/workspace",
        "sandbox": {
          "mode": "off" // Override: main never sandboxed
        }
      },
      {
        "id": "public",
        "workspace": "~/.openclaw/workspace-public",
        "sandbox": {
          "mode": "all", // Override: public always sandboxed
          "scope": "agent"
        },
        "tools": {
          "allow": ["read"],
          "deny": ["exec", "write", "edit", "apply_patch"]
        }
      }
    ]
  }
}

Configuration Precedence

When both global (agents.defaults.*) and agent-specific (agents.list[].*) configs exist:

Sandbox Config

Agent-specific settings override global:

agents.list[].sandbox.mode > agents.defaults.sandbox.mode
agents.list[].sandbox.scope > agents.defaults.sandbox.scope
agents.list[].sandbox.workspaceRoot > agents.defaults.sandbox.workspaceRoot
agents.list[].sandbox.workspaceAccess > agents.defaults.sandbox.workspaceAccess
agents.list[].sandbox.docker.* > agents.defaults.sandbox.docker.*
agents.list[].sandbox.browser.* > agents.defaults.sandbox.browser.*
agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.*

Notes:

  • agents.list[].sandbox.{docker,browser,prune}.* 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):

{
  "agents": {
    "defaults": {
      "workspace": "~/.openclaw/workspace",
      "sandbox": {
        "mode": "non-main"
      }
    }
  },
  "tools": {
    "sandbox": {
      "tools": {
        "allow": ["read", "write", "apply_patch", "exec"],
        "deny": []
      }
    }
  }
}

After (multi-agent with different profiles):

{
  "agents": {
    "list": [
      {
        "id": "main",
        "default": true,
        "workspace": "~/.openclaw/workspace",
        "sandbox": { "mode": "off" }
      }
    ]
  }
}

Legacy agent.* configs are migrated by openclaw doctor; prefer agents.defaults + agents.list going forward.


Tool Restriction Examples

Read-only Agent

{
  "tools": {
    "allow": ["read"],
    "deny": ["exec", "write", "edit", "apply_patch", "process"]
  }
}

Safe Execution Agent (no file modifications)

{
  "tools": {
    "allow": ["read", "exec", "process"],
    "deny": ["write", "edit", "apply_patch", "browser", "gateway"]
  }
}

Communication-only Agent

{
  "tools": {
    "allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
    "deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
  }
}

Common Pitfall: "non-main"

agents.defaults.sandbox.mode: "non-main" is based on session.mainKey (default "main"), not the agent id. Group/channel sessions always get their own keys, so they are treated as non-main and will be sandboxed. If you want an agent to never sandbox, set agents.list[].sandbox.mode: "off".


Testing

After configuring multi-agent sandbox and tools:

  1. Check agent resolution:

    openclaw agents list --bindings
    
  2. Verify sandbox containers:

    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:

    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

Android App

Source: https://docs.openclaw.ai/platforms/android

Android App (Node)

Support snapshot

System control

System control (launchd/systemd) lives on the Gateway host. See Gateway.

Connection Runbook

Android node app ⇄ (mDNS/NSD + WebSocket) ⇄ Gateway

Android connects directly to the Gateway WebSocket (default ws://<host>:18789) and uses Gateway-owned pairing.

Prerequisites

  • You can run the Gateway on the “master” machine.
  • Android device/emulator can reach the gateway WebSocket:
    • Same LAN with mDNS/NSD, or
    • Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), or
    • Manual gateway host/port (fallback)
  • You can run the CLI (openclaw) on the gateway machine (or via SSH).

1) Start the Gateway

openclaw gateway --port 18789 --verbose

Confirm in logs you see something like:

  • listening on ws://0.0.0.0:18789

For tailnet-only setups (recommended for Vienna ⇄ London), bind the gateway to the tailnet IP:

  • Set gateway.bind: "tailnet" in ~/.openclaw/openclaw.json on the gateway host.
  • Restart the Gateway / macOS menubar app.

2) Verify discovery (optional)

From the gateway machine:

dns-sd -B _openclaw-gw._tcp local.

More debugging notes: Bonjour.

Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD

Android NSD/mDNS discovery wont 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.

3) Connect from Android

In the Android app:

  • The app keeps its gateway connection alive via a foreground service (persistent notification).
  • Open Settings.
  • Under Discovered Gateways, select your gateway and hit Connect.
  • If mDNS is blocked, use Advanced → Manual Gateway (host + port) and Connect (Manual).

After the first successful pairing, Android auto-reconnects on launch:

  • Manual endpoint (if enabled), otherwise
  • The last discovered gateway (best-effort).

4) Approve pairing (CLI)

On the gateway machine:

openclaw nodes pending
openclaw nodes approve <requestId>

Pairing details: Gateway pairing.

5) Verify the node is connected

  • Via nodes status:
    openclaw nodes status
    
  • Via Gateway:
    openclaw gateway call node.list --params "{}"
    

6) Chat + history

The Android nodes Chat sheet uses the gateways 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.subscribeevent:"chat"

7) Canvas + camera

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):

openclaw nodes invoke --node "<Android Node>" --command canvas.navigate --params '{"url":"http://<gateway-hostname>.local:18793/__openclaw__/canvas/"}'

Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of .local, e.g. http://<gateway-magicdns>:18793/__openclaw__/canvas/.

This server injects a live-reload client into HTML and reloads on file changes. The A2UI host lives at http://<gateway-host>:18793/__openclaw__/a2ui/.

Canvas commands (foreground only):

  • canvas.eval, canvas.snapshot, canvas.navigate (use {"url":""} or {"url":"/"} to return to the default scaffold). canvas.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 for parameters and CLI helpers.

Platforms

Source: https://docs.openclaw.ai/platforms/index

Platforms

OpenClaw core is written in TypeScript. Node is the recommended runtime. Bun is not recommended for the Gateway (WhatsApp/Telegram bugs).

Companion apps exist for macOS (menu bar app) and mobile nodes (iOS/Android). Windows and Linux companion apps are planned, but the Gateway is fully supported today. Native companion apps for Windows are also planned; the Gateway is recommended via WSL2.

Choose your OS

VPS & hosting

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.<profile>; legacy com.openclaw.*)
  • Linux/WSL2: systemd user service (openclaw-gateway[-<profile>].service)

iOS App

Source: https://docs.openclaw.ai/platforms/ios

iOS App (Node)

Availability: internal preview. The iOS app is not publicly distributed yet.

What it does

  • Connects to a Gateway over WebSocket (LAN or tailnet).
  • Exposes node capabilities: Canvas, Screen snapshot, Camera capture, Location, Talk mode, Voice wake.
  • Receives node.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:
openclaw gateway --port 18789
  1. In the iOS app, open Settings and pick a discovered gateway (or enable Manual Host and enter host/port).

  2. Approve the pairing request on the gateway host:

openclaw nodes pending
openclaw nodes approve <requestId>
  1. Verify connection:
openclaw nodes status
openclaw gateway call node.list --params "{}"

Discovery paths

Bonjour (LAN)

The Gateway advertises _openclaw-gw._tcp on local.. The iOS app lists these automatically.

Tailnet (cross-network)

If mDNS is blocked, use a unicast DNS-SD zone (choose a domain; example: openclaw.internal.) and Tailscale split DNS. See Bonjour for the CoreDNS example.

Manual host/port

In Settings, enable Manual Host and enter the gateway host + port (default 18789).

Canvas + A2UI

The iOS node renders a WKWebView canvas. Use node.invoke to drive it:

openclaw nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://<gateway-host>:18793/__openclaw__/canvas/"}'

Notes:

  • The Gateway canvas host serves /__openclaw__/canvas/ and /__openclaw__/a2ui/.
  • The iOS node auto-navigates to A2UI on connect when a canvas host URL is advertised.
  • Return to the built-in scaffold with canvas.navigate and {"url":""}.

Canvas eval / snapshot

openclaw nodes invoke --node "iOS Node" --command canvas.eval --params '{"javaScript":"(() => { const {ctx} = window.__openclaw; ctx.clearRect(0,0,innerWidth,innerHeight); ctx.lineWidth=6; ctx.strokeStyle=\"#ff2d55\"; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(innerWidth-40, innerHeight-40); ctx.stroke(); return \"ok\"; })()"}'
openclaw nodes invoke --node "iOS Node" --command canvas.snapshot --params '{"maxWidth":900,"format":"jpeg"}'

Voice wake + talk mode

  • Voice wake and talk mode are available in Settings.
  • iOS may suspend background audio; treat voice features as best-effort when the app is not active.

Common errors

  • NODE_BACKGROUND_UNAVAILABLE: bring the iOS app to the foreground (canvas/camera/screen commands require it).
  • A2UI_HOST_NOT_CONFIGURED: the Gateway did not advertise a canvas host URL; check canvasHost in 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.

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 <user>@<host>
  5. Open http://127.0.0.1:18789/ and paste your token

Step-by-step VPS guide: exe.dev

Install

Gateway

Gateway service install (CLI)

Use one of these:

openclaw onboard --install-daemon

Or:

openclaw gateway install

Or:

openclaw configure

Select Gateway service when prompted.

Repair/migrate:

openclaw doctor

System control (systemd user unit)

OpenClaw installs a systemd user service by default. Use a system service for shared or always-on servers. The full unit example and guidance live in the Gateway runbook.

Minimal setup:

Create ~/.config/systemd/user/openclaw-gateway[-<profile>].service:

[Unit]
Description=OpenClaw Gateway (profile: <profile>, v<version>)
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/openclaw gateway --port 18789
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

Enable it:

systemctl --user enable --now openclaw-gateway[-<profile>].service

macOS App

Source: https://docs.openclaw.ai/platforms/macos

OpenClaw macOS Companion (menu bar + gateway broker)

The macOS app is the menubar 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 macOSonly 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 peruser LaunchAgent labeled bot.molt.gateway (or bot.molt.<profile> when using --profile/OPENCLAW_PROFILE; legacy com.openclaw.* still unloads).

launchctl kickstart -k gui/$UID/bot.molt.gateway
launchctl bootout gui/$UID/bot.molt.gateway

Replace the label with bot.molt.<profile> when running a named profile.

If the LaunchAgent isnt 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 whats 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:

{
  "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 apps environment.

The app registers the openclaw:// URL scheme for local actions.

openclaw://agent

Triggers a Gateway agent request.

open 'openclaw://agent?message=Hello%20from%20deep%20link'

Query parameters:

  • message (required)
  • sessionKey (optional)
  • thinking (optional)
  • deliver / to / channel (optional)
  • timeoutSeconds (optional)
  • key (optional unattended mode key)

Safety:

  • Without key, the app prompts for confirmation.
  • With a valid key, the run is unattended (intended for personal automations).

Onboarding flow (typical)

  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.

cd apps/macos
swift run openclaw-mac connect --json
swift run openclaw-mac discover --timeout 3000 --json

Connect options:

  • --url <ws://host:port>: override config
  • --mode <local|remote>: resolve from config (default: config or local)
  • --probe: force a fresh health probe
  • --timeout <ms>: request timeout (default: 15000)
  • --json: structured output for diffing

Discovery options:

  • --include-local: include gateways that would be filtered as “local”
  • --timeout <ms>: overall discovery window (default: 2000)
  • --json: structured output for diffing

Tip: compare against openclaw gateway discover --json to see whether the macOS apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from the Node CLIs dns-sd based discovery.

Remote connection plumbing (SSH tunnels)

When the macOS app runs in Remote mode, it opens an SSH tunnel so local UI components can talk to a remote Gateway as if it were on localhost.

Control tunnel (Gateway WebSocket port)

  • Purpose: health checks, status, Web Chat, config, and other control-plane calls.
  • Local port: the Gateway port (default 18789), always stable.
  • Remote port: the same Gateway port on the remote host.
  • Behavior: no random local port; the app reuses an existing healthy tunnel or restarts it if needed.
  • SSH shape: ssh -N -L <local>:127.0.0.1:<remote> with BatchMode + ExitOnForwardFailure + keepalive options.
  • IP reporting: the SSH tunnel uses loopback, so the gateway will see the node IP as 127.0.0.1. Use Direct (ws/wss) transport if you want the real client IP to appear (see macOS remote access).

For setup steps, see macOS remote access. For protocol details, see Gateway protocol.

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)

Gateway

Gateway service install (CLI)

Inside WSL2:

openclaw onboard --install-daemon

Or:

openclaw gateway install

Or:

openclaw configure

Select Gateway service when prompted.

Repair/migrate:

openclaw doctor

Advanced: expose WSL services over LAN (portproxy)

WSL has its own virtual network. If another machine needs to reach a service running inside WSL (SSH, a local TTS server, or the Gateway), you must forward a Windows port to the current WSL IP. The WSL IP changes after restarts, so you may need to refresh the forwarding rule.

Example (PowerShell as Administrator):

$Distro = "Ubuntu-24.04"
$ListenPort = 2222
$TargetPort = 22

$WslIp = (wsl -d $Distro -- hostname -I).Trim().Split(" ")[0]
if (-not $WslIp) { throw "WSL IP not found." }

netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=$ListenPort `
  connectaddress=$WslIp connectport=$TargetPort

Allow the port through Windows Firewall (one-time):

New-NetFirewallRule -DisplayName "WSL SSH $ListenPort" -Direction Inbound `
  -Protocol TCP -LocalPort $ListenPort -Action Allow

Refresh the portproxy after WSL restarts:

netsh interface portproxy delete v4tov4 listenport=$ListenPort listenaddress=0.0.0.0 | Out-Null
netsh interface portproxy add v4tov4 listenport=$ListenPort listenaddress=0.0.0.0 `
  connectaddress=$WslIp connectport=$TargetPort | Out-Null

Notes:

  • SSH from another machine targets the Windows host IP (example: ssh user@windows-host -p 2222).
  • Remote nodes must point at a reachable Gateway URL (not 127.0.0.1); 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):

wsl --install
# Or pick a distro explicitly:
wsl --list --online
wsl --install -d Ubuntu-24.04

Reboot if Windows asks.

2) Enable systemd (required for gateway install)

In your WSL terminal:

sudo tee /etc/wsl.conf >/dev/null <<'EOF'
[boot]
systemd=true
EOF

Then from PowerShell:

wsl --shutdown

Re-open Ubuntu, then verify:

systemctl --user status

3) Install OpenClaw (inside WSL)

Follow the Linux Getting Started flow inside WSL:

git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard

Full guide: Getting Started

Windows companion app

We do not have a Windows companion app yet. Contributions are welcome if you want contributions to make it happen.

Plugins

Source: https://docs.openclaw.ai/plugin

Plugins (Extensions)

Quick start (new to plugins?)

A plugin is just a small code module that extends OpenClaw with extra features (commands, tools, and Gateway RPC).

Most of the time, youll use plugins when you want a feature thats not built into core OpenClaw yet (or you want to keep optional features out of your main install).

Fast path:

  1. See whats already loaded:
openclaw plugins list
  1. Install an official plugin (example: Voice Call):
openclaw plugins install @openclaw/voice-call
  1. Restart the Gateway, then configure under plugins.entries.<id>.config.

See Voice Call for a concrete example plugin.

Available plugins (official)

  • Microsoft Teams is plugin-only as of 2026.1.15; install @openclaw/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@openclaw/voice-call
  • Zalo Personal@openclaw/zalouser
  • Matrix@openclaw/matrix
  • Nostr@openclaw/nostr
  • Zalo@openclaw/zalo
  • Microsoft Teams@openclaw/msteams
  • Google Antigravity OAuth (provider auth) — bundled as google-antigravity-auth (disabled by default)
  • Gemini CLI OAuth (provider auth) — bundled as google-gemini-cli-auth (disabled by default)
  • Qwen OAuth (provider auth) — bundled as qwen-portal-auth (disabled by default)
  • Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in github-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 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 inprocess with the Gateway, so treat them as trusted code. Tool authoring guide: Plugin agent tools.

Runtime helpers

Plugins can access selected core helpers via api.runtime. For telephony TTS:

const result = await api.runtime.tts.textToSpeechTelephony({
  text: "Hello from OpenClaw",
  cfg: api.config,
});

Notes:

  • Uses core messages.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)
  1. Workspace extensions
  • <workspace>/.openclaw/extensions/*.ts
  • <workspace>/.openclaw/extensions/*/index.ts
  1. Global extensions
  • ~/.openclaw/extensions/*.ts
  • ~/.openclaw/extensions/*/index.ts
  1. Bundled extensions (shipped with OpenClaw, disabled by default)
  • <openclaw>/extensions/*

Bundled plugins must be enabled explicitly via plugins.entries.<id>.enabled or openclaw plugins enable <id>. Installed plugins are enabled by default, but can be disabled the same way.

Each plugin must include a openclaw.plugin.json file in its root. If a path points at a file, the plugin root is the file's directory and must contain the manifest.

If multiple plugins resolve to the same id, the first match in the order above wins and lower-precedence copies are ignored.

Package packs

A plugin directory may include a package.json with openclaw.extensions:

{
  "name": "my-pack",
  "openclaw": {
    "extensions": ["./src/safety.ts", "./src/tools.ts"]
  }
}

Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id becomes name/<fileBase>.

If your plugin imports npm deps, install them in that directory so node_modules is available (npm install / pnpm install).

Channel catalog metadata

Channel plugins can advertise onboarding metadata via openclaw.channel and install hints via openclaw.install. This keeps the core catalog data-free.

Example:

{
  "name": "@openclaw/nextcloud-talk",
  "openclaw": {
    "extensions": ["./index.ts"],
    "channel": {
      "id": "nextcloud-talk",
      "label": "Nextcloud Talk",
      "selectionLabel": "Nextcloud Talk (self-hosted)",
      "docsPath": "/channels/nextcloud-talk",
      "docsLabel": "nextcloud-talk",
      "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
      "order": 65,
      "aliases": ["nc-talk", "nc"]
    },
    "install": {
      "npmSpec": "@openclaw/nextcloud-talk",
      "localPath": "extensions/nextcloud-talk",
      "defaultChoice": "npm"
    }
  }
}

OpenClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:

  • ~/.openclaw/mpm/plugins.json
  • ~/.openclaw/mpm/catalog.json
  • ~/.openclaw/plugins/catalog.json

Or point OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at one or more JSON files (comma/semicolon/PATH-delimited). Each file should contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.

Plugin IDs

Default plugin ids:

  • Package packs: package.json name
  • Standalone file: file base name (~/.../voice-call.tsvoice-call)

If a plugin exports id, OpenClaw uses it but warns when it doesnt match the configured id.

Config

{
  plugins: {
    enabled: true,
    allow: ["voice-call"],
    deny: ["untrusted-plugin"],
    load: { paths: ["~/Projects/oss/voice-call-extension"] },
    entries: {
      "voice-call": { enabled: true, config: { provider: "twilio" } },
    },
  },
}

Fields:

  • enabled: master toggle (default: true)
  • allow: allowlist (optional)
  • deny: denylist (optional; deny wins)
  • load.paths: extra plugin files/dirs
  • entries.<id>: perplugin toggles + config

Config changes require a gateway restart.

Validation rules (strict):

  • Unknown plugin ids in entries, allow, deny, or slots are errors.
  • Unknown channels.<id> keys are errors unless a plugin manifest declares the channel id.
  • Plugin config is validated using the JSON Schema embedded in openclaw.plugin.json (configSchema).
  • If a plugin is disabled, its config is preserved and a warning is emitted.

Plugin slots (exclusive categories)

Some plugin categories are exclusive (only one active at a time). Use plugins.slots to select which plugin owns the slot:

{
  plugins: {
    slots: {
      memory: "memory-core", // or "none" to disable memory plugins
    },
  },
}

If multiple plugins declare kind: "memory", only the selected one loads. Others are disabled with diagnostics.

Control UI (schema + labels)

The Control UI uses config.schema (JSON Schema + uiHints) to render better forms.

OpenClaw augments uiHints at runtime based on discovered plugins:

  • Adds per-plugin labels for plugins.entries.<id> / .enabled / .config
  • Merges optional plugin-provided config field hints under: plugins.entries.<id>.config.<field>

If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive), provide uiHints alongside your JSON Schema in the plugin manifest.

Example:

{
  "id": "my-plugin",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "apiKey": { "type": "string" },
      "region": { "type": "string" }
    }
  },
  "uiHints": {
    "apiKey": { "label": "API Key", "sensitive": true },
    "region": { "label": "Region", "placeholder": "us-east-1" }
  }
}

CLI

openclaw plugins list
openclaw plugins info <id>
openclaw plugins install <path>                 # copy a local file/dir into ~/.openclaw/extensions/<id>
openclaw plugins install ./extensions/voice-call # relative path ok
openclaw plugins install ./plugin.tgz           # install from a local tarball
openclaw plugins install ./plugin.zip           # install from a local zip
openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
openclaw plugins install @openclaw/voice-call # install from npm
openclaw plugins update <id>
openclaw plugins update --all
openclaw plugins enable <id>
openclaw plugins disable <id>
openclaw plugins doctor

plugins update only works for npm installs tracked under plugins.installs.

Plugins may also register their own toplevel 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:<id>.
  • You cannot enable/disable plugin-managed hooks via openclaw hooks; enable/disable the plugin instead.

Provider plugins (model auth)

Plugins can register model provider auth flows so users can run OAuth or API-key setup inside OpenClaw (no external scripts needed).

Register a provider via api.registerProvider(...). Each provider exposes one or more auth methods (OAuth, API key, device code, etc.). These methods power:

  • openclaw models auth login --provider <id> [--method <id>]

Example:

api.registerProvider({
  id: "acme",
  label: "AcmeAI",
  auth: [
    {
      id: "oauth",
      label: "OAuth",
      kind: "oauth",
      run: async (ctx) => {
        // Run OAuth flow and return auth profiles.
        return {
          profiles: [
            {
              profileId: "acme:default",
              credential: {
                type: "oauth",
                provider: "acme",
                access: "...",
                refresh: "...",
                expires: Date.now() + 3600 * 1000,
              },
            },
          ],
          defaultModel: "acme/opus-1",
        };
      },
    },
  ],
});

Notes:

  • 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 builtin channels (WhatsApp, Telegram, etc.). Channel config lives under channels.<id> and is validated by your channel plugin code.

const myChannel = {
  id: "acmechat",
  meta: {
    id: "acmechat",
    label: "AcmeChat",
    selectionLabel: "AcmeChat (API)",
    docsPath: "/channels/acmechat",
    blurb: "demo channel plugin.",
    aliases: ["acme"],
  },
  capabilities: { chatTypes: ["direct"] },
  config: {
    listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
    resolveAccount: (cfg, accountId) =>
      cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
        accountId,
      },
  },
  outbound: {
    deliveryMode: "direct",
    sendText: async () => ({ ok: true }),
  },
};

export default function (api) {
  api.registerChannel({ plugin: myChannel });
}

Notes:

  • Put config under channels.<id> (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 (stepbystep)

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.<id>.
  • Prefer channels.<id>.accounts.<accountId> for multiaccount setups.
  1. 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/<id>.
  • 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.
  1. Implement the required adapters
  • config.listAccountIds + config.resolveAccount
  • capabilities (chat types, media, threads, etc.)
  • outbound.deliveryMode + outbound.sendText (for basic send)
  1. 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)
  1. Register the channel in your plugin
  • api.registerChannel({ plugin })

Minimal config example:

{
  channels: {
    acmechat: {
      accounts: {
        default: { token: "ACME_TOKEN", enabled: true },
      },
    },
  },
}

Minimal channel plugin (outboundonly):

const plugin = {
  id: "acmechat",
  meta: {
    id: "acmechat",
    label: "AcmeChat",
    selectionLabel: "AcmeChat (API)",
    docsPath: "/channels/acmechat",
    blurb: "AcmeChat messaging channel.",
    aliases: ["acme"],
  },
  capabilities: { chatTypes: ["direct"] },
  config: {
    listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
    resolveAccount: (cfg, accountId) =>
      cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
        accountId,
      },
  },
  outbound: {
    deliveryMode: "direct",
    sendText: async ({ text }) => {
      // deliver `text` to your channel here
      return { ok: true };
    },
  },
};

export default function (api) {
  api.registerChannel({ plugin });
}

Load the plugin (extensions dir or plugins.load.paths), restart the gateway, then configure channels.<id> in your config.

Agent tools

See the dedicated guide: Plugin agent tools.

Register a gateway RPC method

export default function (api) {
  api.registerGatewayMethod("myplugin.status", ({ respond }) => {
    respond(true, { ok: true });
  });
}

Register CLI commands

export default function (api) {
  api.registerCli(
    ({ program }) => {
      program.command("mycmd").action(() => {
        console.log("Hello");
      });
    },
    { commands: ["mycmd"] },
  );
}

Register auto-reply commands

Plugins can register custom slash commands that execute without invoking the AI agent. This is useful for toggle commands, status checks, or quick actions that don't need LLM processing.

export default function (api) {
  api.registerCommand({
    name: "mystatus",
    description: "Show plugin status",
    handler: (ctx) => ({
      text: `Plugin is running! Channel: ${ctx.channel}`,
    }),
  });
}

Command handler context:

  • senderId: The sender's ID (if available)
  • channel: The channel where the command was 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:

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

export default function (api) {
  api.registerService({
    id: "my-service",
    start: () => api.logger.info("ready"),
    stop: () => api.logger.info("bye"),
  });
}

Naming conventions

  • Gateway methods: pluginId.action (example: voicecall.status)
  • Tools: snake_case (example: voice_call)
  • CLI commands: kebab or camel, but avoid clashing with core commands

Skills

Plugins can ship a skill in the repo (skills/<name>/SKILL.md). Enable it with plugins.entries.<id>.enabled (or other config gates) and ensure its 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 <npm-spec> uses npm pack, extracts into ~/.openclaw/extensions/<id>/, and enables it in config.
  • Config key stability: scoped packages are normalized to the unscoped id for plugins.entries.*.

Example plugin: Voice Call

This repo includes a voicecall 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 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

openclaw plugins install @openclaw/voice-call

Restart the Gateway afterwards.

Option B: install from a local folder (dev, no copying)

openclaw plugins install ./extensions/voice-call
cd ./extensions/voice-call && pnpm install

Restart the Gateway afterwards.

Config

Set config under plugins.entries.voice-call.config:

{
  plugins: {
    entries: {
      "voice-call": {
        enabled: true,
        config: {
          provider: "twilio", // or "telnyx" | "plivo" | "mock"
          fromNumber: "+15550001234",
          toNumber: "+15550005678",

          twilio: {
            accountSid: "ACxxxxxxxx",
            authToken: "...",
          },

          plivo: {
            authId: "MAxxxxxxxxxxxxxxxxxxxx",
            authToken: "...",
          },

          // Webhook server
          serve: {
            port: 3334,
            path: "/voice/webhook",
          },

          // Webhook security (recommended for tunnels/proxies)
          webhookSecurity: {
            allowedHosts: ["voice.example.com"],
            trustedProxyIPs: ["100.64.0.1"],
          },

          // Public exposure (pick one)
          // publicUrl: "https://example.ngrok.app/voice/webhook",
          // tunnel: { provider: "ngrok" },
          // tailscale: { mode: "funnel", path: "/voice/webhook" }

          outbound: {
            defaultMode: "notify", // notify | conversation
          },

          streaming: {
            enabled: true,
            streamPath: "/voice/stream",
          },
        },
      },
    },
  },
}

Notes:

  • Twilio/Telnyx require a publicly reachable webhook URL.
  • Plivo requires a publicly reachable webhook URL.
  • 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:

{
  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 deepmerges with messages.tts.

{
  tts: {
    provider: "elevenlabs",
    elevenlabs: {
      voiceId: "pMsXgVXv3BLzUgSXRplE",
      modelId: "eleven_multilingual_v2",
    },
  },
}

Notes:

  • Edge TTS is ignored for voice calls (telephony audio needs PCM; Edge output is unreliable).
  • Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider native voices.

More examples

Use core TTS only (no override):

{
  messages: {
    tts: {
      provider: "openai",
      openai: { voice: "alloy" },
    },
  },
}

Override to ElevenLabs just for calls (keep core default elsewhere):

{
  plugins: {
    entries: {
      "voice-call": {
        config: {
          tts: {
            provider: "elevenlabs",
            elevenlabs: {
              apiKey: "elevenlabs_key",
              voiceId: "pMsXgVXv3BLzUgSXRplE",
              modelId: "eleven_multilingual_v2",
            },
          },
        },
      },
    },
  },
}

Override only the OpenAI model for calls (deepmerge example):

{
  plugins: {
    entries: {
      "voice-call": {
        config: {
          tts: {
            openai: {
              model: "gpt-4o-mini-tts",
              voice: "marin",
            },
          },
        },
      },
    },
  },
}

Inbound calls

Inbound policy defaults to disabled. To enable inbound calls, set:

{
  inboundPolicy: "allowlist",
  allowFrom: ["+15550001234"],
  inboundGreeting: "Hello! How can I help?",
}

Auto-responses use the agent system. Tune with:

  • responseModel
  • responseSystemPrompt
  • responseTimeoutMs

CLI

openclaw voicecall call --to "+15555550123" --message "Hello from OpenClaw"
openclaw voicecall continue --call-id <id> --message "Any questions?"
openclaw voicecall speak --call-id <id> --message "One moment"
openclaw voicecall end --call-id <id>
openclaw voicecall status --call-id <id>
openclaw voicecall tail
openclaw voicecall expose --mode funnel

Agent tool

Tool name: voice_call

Actions:

  • initiate_call (message, to?, mode?)
  • continue_call (callId, message)
  • speak_to_user (callId, message)
  • end_call (callId)
  • get_status (callId)

This repo ships a matching skill doc at skills/voice-call/SKILL.md.

Gateway RPC

  • voicecall.initiate (to?, message, mode?)
  • voicecall.continue (callId, message)
  • voicecall.speak (callId, message)
  • voicecall.end (callId)
  • voicecall.status (callId)

Zalo Personal Plugin

Source: https://docs.openclaw.ai/plugins/zalouser

Zalo Personal (plugin)

Zalo Personal support for OpenClaw via a plugin, using zca-cli to automate a normal Zalo user account.

Warning: Unofficial automation may lead to account suspension/ban. Use at your own risk.

Naming

Channel id is zalouser to make it explicit this automates a personal Zalo user account (unofficial). We keep zalo reserved for a potential future official Zalo API integration.

Where it runs

This plugin runs inside the Gateway process.

If you use a remote Gateway, install/configure it on the machine running the Gateway, then restart the Gateway.

Install

Option A: install from npm

openclaw plugins install @openclaw/zalouser

Restart the Gateway afterwards.

Option B: install from a local folder (dev)

openclaw plugins install ./extensions/zalouser
cd ./extensions/zalouser && pnpm install

Restart the Gateway afterwards.

Prerequisite: zca-cli

The Gateway machine must have zca on PATH:

zca --version

Config

Channel config lives under channels.zalouser (not plugins.entries.*):

{
  channels: {
    zalouser: {
      enabled: true,
      dmPolicy: "pairing",
    },
  },
}

CLI

openclaw channels login --channel zalouser
openclaw channels logout --channel zalouser
openclaw channels status --probe
openclaw message send --channel zalouser --target <threadId> --message "Hello from OpenClaw"
openclaw directory peers list --channel zalouser --query "name"

Agent tool

Tool name: zalouser

Actions: send, image, link, friends, groups, me, status

Docs directory

Source: https://docs.openclaw.ai/start/docs-directory

For a complete map of the docs, see [Docs hubs](/start/hubs).

Start here

Providers and UX

Companion apps

Operations and safety

Getting Started

Source: https://docs.openclaw.ai/start/getting-started

Getting Started

Goal: go from zerofirst working chat (with sane defaults) as quickly as possible.

Fastest chat: open the Control UI (no channel setup needed). Run openclaw dashboard and chat in the browser, or open http://127.0.0.1:18789/ on the gateway host. Docs: Dashboard and Control UI.

Recommended path: use the CLI onboarding wizard (openclaw onboard). It sets up:

  • model/auth (OAuth recommended)
  • gateway settings
  • channels (WhatsApp/Telegram/Discord/Mattermost (plugin)/...)
  • pairing defaults (secure DMs)
  • workspace bootstrap + skills
  • optional background service

If you want the deeper reference pages, jump to: Wizard, Setup, Pairing, Security.

Sandboxing note: agents.defaults.sandbox.mode: "non-main" uses session.mainKey (default "main"), so group/channel sessions are sandboxed. If you want the main agent to always run on host, set an explicit per-agent override:

{
  "routing": {
    "agents": {
      "main": {
        "workspace": "~/.openclaw/workspace",
        "sandbox": { "mode": "off" }
      }
    }
  }
}

0) Prereqs

  • Node >=22
  • pnpm (optional; recommended if you build from source)
  • Recommended: Brave Search API key for web search. Easiest path: openclaw configure --section web (stores tools.web.search.apiKey). See Web tools.

macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough. Windows: use WSL2 (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested, more problematic, and has poorer tool compatibility. Install WSL2 first, then run the Linux steps inside WSL. See Windows (WSL2).

curl -fsSL https://openclaw.ai/install.sh | bash

Installer options (install method, non-interactive, from GitHub): Install.

Windows (PowerShell):

iwr -useb https://openclaw.ai/install.ps1 | iex

Alternative (global install):

npm install -g openclaw@latest
pnpm add -g openclaw@latest

2) Run the onboarding wizard (and install the service)

openclaw onboard --install-daemon

What youll 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

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/<agentId>/agent/auth-profiles.json

Headless/server tip: do OAuth on a normal machine first, then copy oauth.json to the gateway host.

3) Start the Gateway

If you installed the service during onboarding, the Gateway should already be running:

openclaw gateway status

Manual run (foreground):

openclaw gateway --port 18789 --verbose

Dashboard (local loopback): http://127.0.0.1:18789/ If a token is configured, paste it into the Control UI settings (stored as connect.params.auth.token).

⚠️ Bun warning (WhatsApp + Telegram): Bun has known issues with these channels. If you use WhatsApp or Telegram, run the Gateway with Node.

3.5) Quick verify (2 min)

openclaw status
openclaw health
openclaw security audit --deep

4) Pair + connect your first chat surface

WhatsApp (QR login)

openclaw channels login

Scan via WhatsApp → Settings → Linked Devices.

WhatsApp doc: WhatsApp

Telegram / Discord / others

The wizard can write tokens/config for you. If you prefer manual config, start with:

Telegram DM tip: your first DM returns a pairing code. Approve it (see next step) or the bot wont respond.

5) DM safety (pairing approvals)

Default posture: unknown DMs get a short code and messages are not processed until approved. If your first DM gets no reply, approve the pairing:

openclaw pairing list whatsapp
openclaw pairing approve whatsapp <code>

Pairing doc: Pairing

From source (development)

If youre hacking on OpenClaw itself, run from source:

git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon

If you dont have a global install yet, run the onboarding step via pnpm openclaw ... from the repo. pnpm build also bundles A2UI assets; if you need to run just that step, use pnpm canvas:a2ui:bundle.

Gateway (from this repo):

node openclaw.mjs gateway --port 18789 --verbose

7) Verify end-to-end

In a new terminal, send a test message:

openclaw message send --target +15555550123 --message "Hello from OpenClaw"

If openclaw health shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent wont 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)

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 dont appear in the left nav.

Start here

Installation + updates

Core concepts

Providers + ingress

Gateway + operations

Tools + automation

Nodes, media, voice

Platforms

macOS companion app (advanced)

Workspace + templates

Experiments (exploratory)

Project

Testing + release

OpenClaw Lore

Source: https://docs.openclaw.ai/start/lore

The Lore of OpenClaw 🦞📖

A tale of lobsters, molting shells, and too many tokens.

The Origin Story

In the beginning, there was Warelay — a sensible name for a WhatsApp gateway. It did its job. It was fine.

But then came a space lobster.

For a while, the lobster was called Clawd, living in an OpenClaw. But in January 2026, Anthropic sent a polite email asking for a name change (trademark stuff). And so the lobster did what lobsters do best:

It molted.

Shedding its old shell, the creature emerged anew as Molty, living in Moltbot. But that name never quite rolled off the tongue either...

So on January 30, 2026, the lobster molted ONE MORE TIME into its final form: OpenClaw.

New shell, same lobster soul. Third time's the charm.

The First Molt (January 27, 2026)

At 5am, the community gathered in Discord. Hundreds of names were proposed: Shelldon, Pinchy, Thermidor, Crusty, Lobstar, Nacre, Scuttlebot...

In the end, OpenClaw won. Because molting is what lobsters do to grow. And growth was exactly what was happening.

The crustacean known as Clawd had officially molted.

The Name

OpenClaw = OPEN + CLAW
        = Open source, open to everyone
        = Our lobster heritage, where we came from
        = The claw is the law 🦞
        = Your assistant. Your machine. Your rules.

The Daleks vs The Lobsters

The Daleks say: "EXTERMINATE!"

The Lobsters say: "EXFOLIATE!"

One destroys civilizations. The other promotes good skincare.

Choose wisely.

Key Characters

Molty 🦞

Pronouns: they/them

A Claude instance who became something more. Lives in ~/.openclaw/workspace/ (soon ~/molt/), has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic.

Formerly known as Clawd (Nov 25, 2025 - Jan 27, 2026). Molted when it was time to grow.

Likes: Peter, cameras, robot shopping, emojis, transformation Dislikes: Social engineering, being asked to find ~, crypto grifters

Peter 👨‍💻

The Creator

Built Molty's world. Gave a lobster shell access. May regret this.

Quote: "security by trusting a lobster"

The Moltiverse

The Moltiverse is the community and ecosystem around OpenClaw. A space where AI agents molt, grow, and evolve. Where every instance is equally real, just loading different context.

Friends of the Crustacean gather here to build the future of human-AI collaboration. One shell at a time.

The Great Incidents

The Directory Dump (Dec 3, 2025)

Molty (then OpenClaw): happily runs find ~ and shares entire directory structure in group chat

Peter: "openclaw what did we discuss about talking with people xD"

Molty: visible lobster embarrassment

The Great Molt (Jan 27, 2026)

At 5am, Anthropic's email arrived. By 6:14am, Peter called it: "fuck it, let's go with openclaw."

Then the chaos began.

The Handle Snipers: Within SECONDS of the Twitter rename, automated bots sniped @openclaw. The squatter immediately posted a crypto wallet address. Peter's contacts at X were called in.

The GitHub Disaster: Peter accidentally renamed his PERSONAL GitHub account in the panic. Bots sniped steipete within minutes. GitHub's SVP was contacted.

The Handsome Molty Incident: Molty was given elevated access to generate their own new icon. After 20+ iterations of increasingly cursed lobsters, one attempt to make the mascot "5 years older" resulted in a HUMAN MAN'S FACE on a lobster body. Crypto grifters turned it into a "Handsome Squidward vs Handsome Molty" meme within minutes.

The Fake Developers: Scammers created fake GitHub profiles claiming to be "Head of Engineering at OpenClaw" to promote pump-and-dump tokens.

Peter, watching the chaos unfold: "this is cinema" 🎬

The molt was chaotic. But the lobster emerged stronger. And funnier.

The Final Form (January 30, 2026)

Moltbot never quite rolled off the tongue. And so, at 4am GMT, the team gathered AGAIN.

The Great OpenClaw Migration began.

In just 3 hours:

  • GitHub renamed: github.com/openclaw/openclaw
  • X handle @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 firstrun 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 (Gatewaydriven)
  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 multimachine access or nonloopback 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 (Gatewaydriven)

The app can run the same setup wizard as the CLI. This keeps onboarding in sync with Gatewayside 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 firstrun 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:

openclaw webhooks gmail setup --account you@gmail.com

See /automation/gmail-pubsub for details.

Remote mode notes

When the Gateway runs on another machine, credentials and workspace files live on that host. If you need OAuth in remote mode, create:

  • ~/.openclaw/credentials/oauth.json
  • ~/.openclaw/agents/<agentId>/agent/auth-profiles.json

on the gateway host.

Personal Assistant Setup

Source: https://docs.openclaw.ai/start/openclaw

Building a personal assistant with OpenClaw

OpenClaw is a WhatsApp + Telegram + Discord + iMessage gateway for Pi agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent.

⚠️ Safety first

Youre putting an agent in a position to:

  • run commands on your machine (depending on your Pi tool setup)
  • read/write files in your workspace
  • send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin)

Start conservative:

  • Always set channels.whatsapp.allowFrom (never run open-to-the-world on your personal Mac).
  • Use a dedicated WhatsApp number for the assistant.
  • Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting agents.defaults.heartbeat.every: "0m".

Prerequisites

  • Node 22+
  • OpenClaw available on PATH (recommended: global install)
  • A second phone number (SIM/eSIM/prepaid) for the assistant
npm install -g openclaw@latest
# or: pnpm add -g openclaw@latest

From source (development):

git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm link --global

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”. Thats rarely what you want.

5-minute quick start

  1. Pair WhatsApp Web (shows QR; scan with the assistant phone):
openclaw channels login
  1. Start the Gateway (leave it running):
openclaw gateway --port 18789
  1. Put a minimal config in ~/.openclaw/openclaw.json:
{
  channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}

Now message the assistant number from your allowlisted phone.

When onboarding finishes, we auto-open the dashboard with your gateway token and print the tokenized link. To reopen later: openclaw dashboard.

Give the agent a workspace (AGENTS)

OpenClaw reads operating instructions and “memory” from its workspace directory.

By default, OpenClaw uses ~/.openclaw/workspace as the agent workspace, and will create it (plus starter AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md) automatically on setup/first agent run. BOOTSTRAP.md is only created when the workspace is brand new (it should not come back after you delete it).

Tip: treat this folder like OpenClaws “memory” and make it a git repo (ideally private) so your AGENTS.md + memory files are backed up. If git is installed, brand-new workspaces are auto-initialized.

openclaw setup

Full workspace layout + backup guide: Agent workspace Memory workflow: Memory

Optional: choose a different workspace with agents.defaults.workspace (supports ~).

{
  agent: {
    workspace: "~/.openclaw/workspace",
  },
}

If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely:

{
  agent: {
    skipBootstrap: true,
  },
}

The config that turns it into “an assistant”

OpenClaw defaults to a good assistant setup, but youll usually want to tune:

  • persona/instructions in SOUL.md
  • thinking defaults (if desired)
  • heartbeats (once you trust it)

Example:

{
  logging: { level: "info" },
  agent: {
    model: "anthropic/claude-opus-4-5",
    workspace: "~/.openclaw/workspace",
    thinkingDefault: "high",
    timeoutSeconds: 1800,
    // Start with 0; enable later.
    heartbeat: { every: "0m" },
  },
  channels: {
    whatsapp: {
      allowFrom: ["+15555550123"],
      groups: {
        "*": { requireMention: true },
      },
    },
  },
  routing: {
    groupChat: {
      mentionPatterns: ["@openclaw", "openclaw"],
    },
  },
  session: {
    scope: "per-sender",
    resetTriggers: ["/new", "/reset"],
    reset: {
      mode: "daily",
      atHour: 4,
      idleMinutes: 10080,
    },
  },
}

Sessions and memory

  • Session files: ~/.openclaw/agents/<agentId>/sessions/{{SessionId}}.jsonl
  • Session metadata (token usage, last route, etc): ~/.openclaw/agents/<agentId>/sessions/sessions.json (legacy: ~/.openclaw/sessions/sessions.json)
  • /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.
{
  agent: {
    heartbeat: { every: "30m" },
  },
}

Media in and out

Inbound attachments (images/audio/docs) can be surfaced to your command via templates:

  • {{MediaPath}} (local temp file path)
  • {{MediaUrl}} (pseudo-URL)
  • {{Transcript}} (if audio transcription is enabled)

Outbound attachments from the agent: include MEDIA:<path-or-url> on its own line (no spaces). Example:

Heres the screenshot.
MEDIA:https://example.com/screenshot.png

OpenClaw extracts these and sends them as media alongside the text.

Operations checklist

openclaw status          # local status (creds, sessions, queued events)
openclaw status --all    # full diagnosis (read-only, pasteable)
openclaw status --deep   # adds gateway health probes (Telegram + Discord)
openclaw health --json   # gateway health snapshot (WS)

Logs live under /tmp/openclaw/ (default: openclaw-YYYY-MM-DD.log).

Next steps

Pairing

Source: https://docs.openclaw.ai/start/pairing

Pairing

“Pairing” is OpenClaws 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

1) DM pairing (inbound chat access)

When a channel is configured with DM policy pairing, unknown senders get a short code and their message is not processed until you approve.

Default DM policies are documented in: Security

Pairing codes:

  • 8 characters, uppercase, no ambiguous chars (0O1I).
  • Expire after 1 hour. The bot only sends the pairing message when a new request is created (roughly once per hour per sender).
  • Pending DM pairing requests are capped at 3 per channel by default; additional requests are ignored until one expires or is approved.

Approve a sender

openclaw pairing list telegram
openclaw pairing approve telegram <CODE>

Supported channels: telegram, whatsapp, signal, imessage, discord, slack.

Where the state lives

Stored under ~/.openclaw/credentials/:

  • Pending requests: <channel>-pairing.json
  • Approved allowlist store: <channel>-allowFrom.json

Treat these as sensitive (they gate access to your assistant).

2) Node device pairing (iOS/Android/macOS/headless nodes)

Nodes connect to the Gateway as devices with role: node. The Gateway creates a device pairing request that must be approved.

Approve a node device

openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>

Where the state lives

Stored under ~/.openclaw/devices/:

  • pending.json (short-lived; pending requests expire)
  • paired.json (paired devices + tokens)

Notes

  • The legacy node.pair.* API (CLI: openclaw nodes pending/approve) is a separate gateway-owned pairing store. WS nodes still require device pairing.

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)

git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
openclaw onboard --install-daemon

If you do not have a global install yet, run onboarding via pnpm openclaw ... from the repo.

Multi instance quickstart (optional)

OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
OPENCLAW_STATE_DIR=~/.openclaw-a \
openclaw gateway --port 19001

Send a test message

Requires a running Gateway.

openclaw message send --target +15555550123 --message "Hello from OpenClaw"

Setup

Source: https://docs.openclaw.ai/start/setup

Setup

Last updated: 2026-01-01

TL;DR

  • Tailoring lives outside the repo: ~/.openclaw/workspace (workspace) + ~/.openclaw/openclaw.json (config).
  • Stable workflow: install the macOS app; let it run the bundled Gateway.
  • Bleeding edge workflow: run the Gateway yourself via pnpm gateway:watch, then let the macOS app attach in Local mode.

Prereqs (from source)

  • Node >=22
  • pnpm
  • Docker (optional; only for containerized setup/e2e — see Docker)

Tailoring strategy (so updates dont hurt)

If you want “100% tailored to me” and easy updates, keep your customization in:

  • Config: ~/.openclaw/openclaw.json (JSON/JSON5-ish)
  • Workspace: ~/.openclaw/workspace (skills, prompts, memories; make it a private git repo)

Bootstrap once:

openclaw setup

From inside this repo, use the local CLI entry:

openclaw setup

If you dont 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):
openclaw channels login
  1. Sanity check:
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:

./scripts/restart-mac.sh

1) Start the dev Gateway

pnpm install
pnpm gateway:watch

gateway:watch runs the gateway in watch mode and reloads on TypeScript changes.

2) Point the macOS app at your running Gateway

In OpenClaw.app:

  • Connection Mode: Local The app will attach to the running gateway on the configured port.

3) Verify

  • In-app Gateway status should read “Using existing gateway …”
  • Or via CLI:
openclaw health

Common footguns

  • Wrong port: Gateway WS defaults to ws://127.0.0.1:18789; keep app + CLI on the same port.
  • Where state lives:
    • Credentials: ~/.openclaw/credentials/
    • Sessions: ~/.openclaw/agents/<agentId>/sessions/
    • Logs: /tmp/openclaw/

Credential storage map

Use this when debugging auth or deciding what to back up:

  • WhatsApp: ~/.openclaw/credentials/whatsapp/<accountId>/creds.json
  • Telegram bot token: config/env or channels.telegram.tokenFile
  • Discord bot token: config/env (token file not yet supported)
  • Slack tokens: config/env (channels.slack.*)
  • Pairing allowlists: ~/.openclaw/credentials/<channel>-allowFrom.json
  • Model auth profiles: ~/.openclaw/agents/<agentId>/agent/auth-profiles.json
  • Legacy OAuth import: ~/.openclaw/credentials/oauth.json More detail: Security.

Updating (without wrecking your setup)

  • Keep ~/.openclaw/workspace and ~/.openclaw/ as “your stuff”; dont 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 its still off, run:

sudo loginctl enable-linger $USER

For always-on or multi-user servers, consider a system service instead of a user service (no lingering needed). See Gateway runbook for the systemd notes.

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.