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

# OpenClaw

> Deploy OpenClaw on Control Plane using the Template Catalog. A single-replica, always-on AI assistant that connects your model to Telegram, Slack, Discord and WhatsApp, with tools and a browser Control UI. A dictionary secret and an existing single-location GVC must exist first.

## Overview

[OpenClaw](https://github.com/openclaw/openclaw) is a self-hosted, always-on AI assistant: one **Gateway** that connects the model of your choice to your chat apps (Telegram, Slack, Discord, WhatsApp) and gives it tools, including a shell, files, a headless browser, scheduled jobs, memory and MCP servers, with a built-in browser **Control UI**. This template runs the Gateway as a single stateful replica with all of its state on a snapshotted volume. You bring the model key: Anthropic, OpenAI, or any OpenAI-compatible endpoint.

The install is **private by default**. You reach the Control UI over `cpln port-forward` for setup, and turn on public access (or a custom domain) for daily use. The [Control Plane MCP server](/ai/mcp) is pre-registered, so the assistant can manage your Control Plane resources once someone signs in.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC that you already have, and that GVC should have a single location (see [Prerequisites](#prerequisites)).
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-openclaw` | OpenClaw Gateway (stateful, 1 replica, port `18789`): Control UI, WebSocket, health checks and the optional OpenAI-compatible HTTP API. Uses the `-browser` image (headless Chromium) when `browser.enabled` | — |
| Volume Set | `RELEASE_NAME-openclaw-vs` | 10 GiB `ext4` volume at `/data` holding all state in `/data/.openclaw`: config, SQLite, channel sessions, plugins, workspace and memory. Scheduled snapshots | Snapshots: `backup.enabled` |
| Secret | `RELEASE_NAME-openclaw-seed` | The first-boot `openclaw.json`, copied only when none exists. Contains no credentials | — |
| Identity | `RELEASE_NAME-openclaw-identity` | Identity bound to the workload | — |
| Policy | `RELEASE_NAME-openclaw-policy` | Grants the identity `reveal` on exactly your prerequisite secret and the seed secret | — |

The Gateway runs as one replica by design: OpenClaw supports a single Gateway per state directory. For a separate assistant per person or team, install another release.

## Prerequisites

<Steps>
  <Step title="Create the dictionary secret">
    One [dictionary secret](/guides/create-secret/dictionary) holds the Gateway token, your model key, and the bot tokens of any chat channels you enable. Secrets are org-level, so no GVC flag is involved.

    | Key | Required | Holds |
    | - | - | - |
    | `gateway-token` | Yes | Your Control UI login and full-admin API token. **At least 24 characters** |
    | `llm-api-key` | Yes | The key for `model.provider` (Anthropic, OpenAI, or your OpenAI-compatible endpoint) |
    | `telegram-bot-token` | With `channels.telegram.enabled` | Bot token from @BotFather |
    | `slack-bot-token`, `slack-app-token` | With `channels.slack.enabled` | Bot token (`xoxb-…`) and Socket Mode app token (`xapp-…`) |
    | `discord-bot-token` | With `channels.discord.enabled` | Bot token (enable the Message Content intent) |

    ```bash theme={null}
    cpln secret create-dictionary --name SECRET_NAME \
      --entry gateway-token="$(openssl rand -hex 32)" \
      --entry llm-api-key='YOUR-LLM-API-KEY'
    ```

    Set `secret.name` to this name. Channel keys can be added later; see [Connecting Chat Channels](#connecting-chat-channels).
  </Step>

  <Step title="Use a single-location GVC">
    A workload runs in every location of its GVC, so a multi-location GVC would run one assistant per location, each competing for the same bot tokens. Install into a GVC with one location.
  </Step>

  <Step title="Optional: a custom domain">
    Only when you want the Control UI on your own hostname. You create the domain yourself; the template never creates one. See [Custom Domain](#custom-domain).
  </Step>
</Steps>

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** A `secret.name` that points at a secret which does not exist installs "successfully" and then never starts. The container never runs, so `cpln logs` returns **zero lines**. The one place the reason appears is `status.versions[].message`:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-openclaw --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret SECRET_NAME no longer exists. Workload updates are paused until the secret is added
  or the reference to the secret removed.
  ```

  Use `get-deployments`; plain `cpln workload get` has no `versions` key. Once the secret exists the workload recovers on its own after a wait, or run `cpln workload force-redeployment RELEASE_NAME-openclaw --gvc GVC_NAME` to skip it. The same applies to a chat channel enabled before its token key is in the secret.
</Warning>

Backups need nothing further: they are platform-managed volume snapshots with no bucket or cloud account involved. See [Backing Up](#backing-up).

## Installation

Install from the marketplace registry with the one required value, the name of the secret you created:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/openclaw \
  --version 1.0.0 --gvc GVC_NAME \
  --set secret.name=SECRET_NAME
```

The default model is Anthropic's `claude-opus-5-5`; add `--set model.provider=...` and `--set model.name=...` for another (see [Model](#model)). Then follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

One subsection per values area. Each block shows the shipped defaults.

Values seed OpenClaw's config on the first boot. Afterwards, a `helm upgrade` re-applies `model`, `channels`, `httpApi` and `cplnMcp` only when those values change; settings you change in the Control UI persist otherwise. A value change restarts the Gateway, so the assistant is briefly offline. An upgrade only carries the values you pass it, so always repeat the ones you installed with:

```bash theme={null}
cpln helm upgrade RELEASE_NAME oci://ghcr.io/controlplane-com/templates/openclaw \
  --version 1.0.0 --gvc GVC_NAME \
  --set secret.name=SECRET_NAME \
  --set channels.telegram.enabled=true
```

### Secret

```yaml theme={null}
secret:
  name: my-openclaw-secret # the prerequisite dictionary secret — must exist before install
```

* `secret.name` — Name of the dictionary secret from [Prerequisites](#prerequisites). The keys it must contain depend on the channels you enable. A `gateway-token` shorter than 24 characters stops the Gateway at start (see [Troubleshooting](#troubleshooting)).

### Model

```yaml theme={null}
model:
  provider: anthropic # anthropic | openai | custom (any OpenAI-compatible endpoint, e.g. OpenRouter, LiteLLM)
  name: claude-opus-5-5 # model ID from your provider; no "anthropic/" or "openai/" prefix
  baseUrl: "" # OpenAI-compatible base URL (usually ending in /v1), required only when provider is custom
```

* `model.provider` — `anthropic`, `openai`, or `custom` for any OpenAI-compatible endpoint, such as OpenRouter or a [LiteLLM](/template-catalog/templates/litellm) gateway. The key is always `llm-api-key` in your secret.
* `model.name` — The bare model ID from your provider. The chart adds the provider prefix itself and refuses a name containing `/`.
* `model.baseUrl` — Required with `custom` and refused with the other providers. Usually ends in `/v1`.

After changing `model.provider` on a running install, rebuild the memory index; see [Important Notes](#important-notes).

### Channels

```yaml theme={null}
channels:
  telegram:
    enabled: false # needs secret key telegram-bot-token (from @BotFather)
  slack:
    enabled: false # Socket Mode; needs secret keys slack-bot-token and slack-app-token
  discord:
    enabled: false # needs secret key discord-bot-token (enable the Message Content intent)
  whatsapp:
    enabled: false # link your phone after install: Control UI → Settings → Channels → WhatsApp → Show QR
```

All four channels connect outbound, so they work on a private install with no public URL. Add a channel's token keys to your secret **before** enabling it. Bot creation, pairing and the WhatsApp link are in [Connecting Chat Channels](#connecting-chat-channels).

### Browser Tool and HTTP API

```yaml theme={null}
browser:
  enabled: true # headless Chromium for the agent's browser tool (uses the -browser image)
httpApi:
  enabled: false # /v1/chat/completions and /v1/responses; bearer = gateway token; in-GVC callers must also send X-Forwarded-For
```

* `browser.enabled` — Run the image variant with headless Chromium, so the agent can open and read web pages. With it off, `resources.maxMemory: 2Gi` is enough.
* `httpApi.enabled` — Serve an OpenAI-compatible API at `/v1/chat/completions` and `/v1/responses` on port `18789`, authenticated with the gateway token. See [Connecting](#connecting) for the headers each caller needs.

### Control Plane MCP

```yaml theme={null}
cplnMcp:
  enabled: true # pre-register the Control Plane MCP server; nobody is signed in at install
  signIn: shared # shared: you sign in once from a terminal and every chat uses your account | per-requester: each chat user signs in from a chat channel (needs publicAccess.enabled)
```

* `cplnMcp.enabled` — Register the [Control Plane MCP server](/ai/mcp) as `cpln`. Nobody is signed in at install, so it grants nothing until someone signs in.
* `cplnMcp.signIn` — `shared` (default): you sign in once from a terminal and every chat acts with your Control Plane account. `per-requester`: each chat user signs in with their own account from a chat channel. `per-requester` requires `publicAccess.enabled: true`; the chart refuses to render it otherwise.

The two modes sign in differently; see [Connecting Control Plane Tools (MCP)](#connecting-control-plane-tools-mcp).

### Resources and Storage

```yaml theme={null}
image:
  repository: ghcr.io/openclaw/openclaw
  tag: "2026.9.8" # exact release; "-browser" is appended automatically when browser.enabled
resources:
  minCpu: 500m # keep maxCpu <= 4x minCpu (stateful workload)
  minMemory: 1Gi
  maxCpu: 2000m
  maxMemory: 3Gi # 2Gi is enough with browser.enabled: false
volumeset:
  capacity: 10 # initial capacity in GiB (minimum 10)
  autoscaling:
    enabled: false
    maxCapacity: 100 # GiB
    minFreePercentage: 10
    scalingFactor: 1.2
backup:
  enabled: true # scheduled snapshots of the data volume (they contain channel sessions and tokens — treat them as secrets)
  schedule: "0 3 * * *" # cron, UTC; at most hourly (a single fixed minute)
  retention: 7d # how long each snapshot is kept
```

* `image.tag` — The exact OpenClaw release. Take a snapshot before changing it; see [Backing Up](#backing-up).
* `resources` — CPU and memory reserved (`min*`) and the limits (`max*`). On a stateful workload `maxCpu` may be at most 4 times `minCpu`; the chart refuses a wider ratio.
* `volumeset.capacity` — Initial volume size in GiB (minimum 10). With `volumeset.autoscaling.enabled` the volume grows by `scalingFactor` when free space drops below `minFreePercentage`, up to `maxCapacity`.
* `backup.*` — Scheduled volume snapshots. `schedule` is a 5-field cron in UTC with a single fixed minute, because the platform runs snapshots at most hourly; the chart refuses a schedule such as `*/10 * * * *`.

### Access

```yaml theme={null}
publicAccess:
  enabled: false # expose the Control UI on the public canonical HTTPS endpoint — read the README security note first
  origin: "" # custom domain, e.g. https://assistant.example.com (no path). Empty = canonical endpoint
internalAccess:
  type: same-gvc # none | same-gvc | same-org | workload-list
  workloads: [] # used with workload-list, e.g. //gvc/GVC/workload/NAME
```

* `publicAccess.enabled` — Serve the Control UI on the public canonical HTTPS endpoint. While it is off, requests from the internet are refused and the UI is reachable over `cpln port-forward` only.
* `publicAccess.origin` — The https origin of a custom domain (no path, no trailing slash). Requires `publicAccess.enabled: true`. See [Custom Domain](#custom-domain).
* `internalAccess.type` — Which workloads may call port `18789` from inside Control Plane:

| Type | Description |
| - | - |
| `none` | No internal access. |
| `same-gvc` | Allow access from all workloads in the same GVC (default). |
| `same-org` | Allow access from all workloads in the same org. |
| `workload-list` | Allow access only from workloads listed in `internalAccess.workloads`. |

<Warning>
  **With `publicAccess.enabled`, an agent with a shell sits on the internet behind one token.** Keep the gateway token long and secret, keep DM pairing on, and vet third-party skills before installing them.
</Warning>

An access change restarts the Gateway and takes from about 30 seconds to 10 minutes to take effect. Re-test before concluding that a value did not work.

## Connecting

| What | Address | Auth |
| - | - | - |
| Control UI (setup, recovery) | `http://localhost:18789` through `cpln port-forward RELEASE_NAME-openclaw 18789:18789 --gvc GVC_NAME` | `gateway-token`; the browser is approved automatically |
| Control UI (daily use) | `status.canonicalEndpoint` with `publicAccess.enabled: true` (read it with `cpln workload get RELEASE_NAME-openclaw --gvc GVC_NAME -o yaml`), or your custom domain | `gateway-token`, then a one-time browser approval (see [First Run](#first-run)) |
| HTTP API, in-GVC (`httpApi.enabled`) | `http://RELEASE_NAME-openclaw.GVC_NAME.cpln.local:18789/v1/chat/completions` | `Authorization: Bearer` with the gateway token, **plus** `X-Forwarded-For` with a non-loopback address such as `10.0.0.1`. Without it the call is refused with `403 proxy_attribution_required`; `127.0.0.1` is refused the same way |
| HTTP API, public (`httpApi.enabled`) | `/v1/chat/completions` on the public endpoint | `Authorization: Bearer` with the gateway token |
| OpenClaw CLI | `cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs COMMAND'` | — |
| Gateway token | `cpln secret reveal SECRET_NAME -o yaml`, key `gateway-token` | A bare `reveal` prints only a summary table |

Verify the Gateway is up from your own machine. In one terminal:

```bash theme={null}
cpln port-forward RELEASE_NAME-openclaw 18789:18789 --gvc GVC_NAME
```

In another, the endpoint the readiness check uses should answer `200`:

```bash theme={null}
curl -s http://localhost:18789/startupz
```

The port-forward is fine for setup, but browser tabs through the tunnel can freeze. Use the public endpoint or a custom domain day to day.

### First Run

On the port-forward, sign in to `http://localhost:18789` with the gateway token; that browser is approved automatically. On the public endpoint or a custom domain each new browser needs a one-time approval:

<Steps>
  <Step title="Read the gateway token">
    ```bash theme={null}
    cpln secret reveal SECRET_NAME -o yaml
    ```

    Copy the value of `gateway-token`.
  </Step>

  <Step title="Sign in">
    Open the Control UI and paste the token. The page then shows **Approve this browser** with a request ID.
  </Step>

  <Step title="Approve the browser">
    List pending requests if you need to check the ID, then approve it:

    ```bash theme={null}
    cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs devices list'
    cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs devices approve REQUEST_ID'
    ```

    The waiting page connects by itself within seconds.
  </Step>
</Steps>

Approvals live on the volume: they survive restarts, upgrades and a token rotation, but a reinstall asks again.

### Custom Domain

Create a Control Plane [domain](/guides/configure-domain) routed to port `18789` of `RELEASE_NAME-openclaw`, then set `publicAccess.origin` to its https origin, for example `https://assistant.example.com`. The canonical endpoint is then refused by the origin check. After an uninstall and reinstall, delete and recreate the domain: it does not rebind to the new workload by itself.

### Connecting Chat Channels

Telegram, Slack and Discord connect outbound, so they work on a private install too. For each one:

<Steps>
  <Step title="Create the bot and add its token to your secret">
    Create the bot (table below), then add its key or keys to your secret **before** enabling the channel. Export the secret, add the key under `data`, and re-apply it:

    ```bash theme={null}
    cpln secret reveal SECRET_NAME -o yaml > secret.yaml
    # add the channel key(s) under data: in secret.yaml
    cpln apply -f secret.yaml
    cpln secret reveal SECRET_NAME -o yaml
    ```

    A channel enabled without its key wedges the deployment silently (see [Prerequisites](#prerequisites)). Delete `secret.yaml` afterwards; it holds your keys in plain text.
  </Step>

  <Step title="Enable the channel">
    Set `channels.<name>.enabled: true` and run `cpln helm upgrade` with your other values. The Gateway restarts (about 2 minutes) and connects. Check with `channels status` (see [Restarting the Gateway or a Channel](#restarting-the-gateway-or-a-channel)); it should read `running, connected`. A token changed later needs `cpln workload force-redeployment`.
  </Step>

  <Step title="DM the bot and approve the pairing code">
    The bot replies with a pairing code. Approve it, using `slack` or `discord` instead of `telegram` as needed:

    ```bash theme={null}
    cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs pairing approve telegram CODE'
    ```

    DM again, and the reply comes from the model. The first approved sender becomes the assistant's owner. Every new contact gets a pairing code and is ignored until you approve it.
  </Step>
</Steps>

| Channel | Create the bot | Secret keys |
| - | - | - |
| Telegram | Message **@BotFather**, send `/newbot`, copy the token | `telegram-bot-token` |
| Slack | [api.slack.com/apps](https://api.slack.com/apps) → **Create New App → From a manifest**, paste the Socket Mode manifest from the [OpenClaw Slack setup](https://docs.openclaw.ai/channels/slack/setup). **Basic Information → App-Level Tokens** → generate one with `connections:write` (`xapp-…`). **Install App** → copy the Bot User OAuth Token (`xoxb-…`) | `slack-app-token`, `slack-bot-token` |
| Discord | [Developer Portal](https://discord.com/developers/applications) → **New Application → Bot**: turn on **Message Content Intent**, **Reset Token** and copy it. **OAuth2 → URL Generator**: scopes `bot` + `applications.commands`, permissions View Channels, Send Messages, Read Message History; open the URL to add the bot to your server. Turn on the server's **Privacy Settings → Direct Messages** so the bot can DM you its pairing code | `discord-bot-token` |

**Only DMs are answered at first.** Messages in a Slack channel or a Discord server are ignored until that channel or server is allowlisted. See the upstream [Slack access control](https://docs.openclaw.ai/channels/slack/access-control) and [Discord setup](https://docs.openclaw.ai/channels/discord/setup) docs.

**WhatsApp** needs no token. With `channels.whatsapp.enabled: true`, open the Control UI over the public endpoint → **Settings → Channels → WhatsApp → Show QR**, then on the phone go to *Settings → Linked devices → Link a device*. The image does not refresh, so click **Show QR** again after about 60 seconds. Do not abandon a half-done setup tab; it blocks a new one for about 5 minutes. On a private install, print the QR in a terminal instead:

```bash theme={null}
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs channels login --channel whatsapp'
```

Use a dedicated number: this is unofficial WhatsApp Web, and accounts can be banned.

### Connecting Control Plane Tools (MCP)

With `cplnMcp.enabled` the [Control Plane MCP server](/ai/mcp) is registered as `cpln`; nobody is signed in at install. **Pick the mode by where you want to sign in.** The two are not interchangeable:

| `cplnMcp.signIn` | Sign in from | Tools act as |
| - | - | - |
| `shared` (default) | A terminal, once | Your account, for every chat and the Control UI chat |
| `per-requester` | A link the bot sends in Telegram, Slack, Discord or WhatsApp | Each chat user's own account |

**Shared: sign in from a terminal.** Asking the bot to sign in from a chat does **not** work in this mode: it hands you a link whose callback goes to `127.0.0.1:8989` and fails with *connection refused*. Instead run these two commands in two terminals; the tunnel carries the browser's return to the Gateway:

```bash theme={null}
cpln port-forward RELEASE_NAME-openclaw 8989:8989 --gvc GVC_NAME
```

```bash theme={null}
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs mcp login cpln'
```

Open the printed URL and approve. The login command reports success, and you can stop the tunnel. Everyone who can message the assistant then acts with your Control Plane permissions. The Control UI's MCP page shows only this command; its **Sign in** button does not work through `cpln port-forward`.

**Per-requester: sign in from a chat.** This needs `publicAccess.enabled`, because the link returns to the Gateway's public address. Connect a chat channel first, then in a DM ask the assistant to use Control Plane (for example "list my GVCs"). It replies with a sign-in link; open it, approve with your own Control Plane account, and ask again. Each chat identity signs in once, so your Slack and Discord accounts each get their own link. Links are single-use and whoever opens one connects their account, so do not request them in group chats. The Control UI chat cannot sign in in this mode. To switch an existing install, set `cplnMcp.signIn` and run `cpln helm upgrade`, then ask for a new link; links issued in the old mode do not work.

Check either mode:

```bash theme={null}
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs mcp status --verbose'
```

Shared mode reads `oauth: authorized`; per-requester reads `connected principals: N`.

## Operations

### Backing Up

Backups are **scheduled volume snapshots** managed by the platform; no cloud account or bucket is required. By default the volume is snapshotted daily at 03:00 UTC and each snapshot is kept for 7 days (`backup.schedule`, `backup.retention`). Snapshots contain your WhatsApp session and MCP sign-in tokens, so treat them as secrets.

List the snapshots of the release's volume set:

```bash theme={null}
cpln volumeset snapshot get RELEASE_NAME-openclaw-vs --gvc GVC_NAME
```

**Take a manual snapshot before changing `image.tag`.** A new OpenClaw release runs one-way state migrations, so an older release cannot read the migrated state. Read your GVC's location from `spec.staticPlacement.locationLinks` with `cpln gvc get GVC_NAME -o yaml`, then:

```bash theme={null}
cpln volumeset snapshot create RELEASE_NAME-openclaw-vs --gvc GVC_NAME --snapshot-name SNAPSHOT_NAME --location LOCATION --volume-index 0
```

### Restoring a Backup

A snapshot restores **in place**, onto the release's own volume set; the platform then replaces the replica and the Gateway boots on the restored state. There is no path to restore into a different or reinstalled release.

<Warning>
  Restoring reverts the volume to the snapshot, so everything written after it is gone, including newer pairings, channel links and MCP sign-ins. The assistant is offline while the replica is replaced.
</Warning>

<Steps>
  <Step title="Find the snapshot">
    List snapshots and note the name and location of the one you want:

    ```bash theme={null}
    cpln volumeset snapshot get RELEASE_NAME-openclaw-vs --gvc GVC_NAME
    ```
  </Step>

  <Step title="Restore it">
    The release has one replica, so the volume index is `0`:

    ```bash theme={null}
    cpln volumeset snapshot restore RELEASE_NAME-openclaw-vs --gvc GVC_NAME --snapshot-name SNAPSHOT_NAME --location LOCATION --volume-index 0
    ```
  </Step>

  <Step title="Verify">
    Wait for the workload to report ready with `cpln workload get-deployments RELEASE_NAME-openclaw --gvc GVC_NAME -o yaml`, then sign in and confirm your sessions and settings. To roll back an image upgrade, restore the snapshot you took before it and upgrade again with the previous `image.tag`.
  </Step>
</Steps>

### Restarting the Gateway or a Channel

Restart the Gateway in place. It takes about 15 seconds after any running agent turn finishes, and keeps serving while it waits; channels and open Control UI tabs reconnect:

```bash theme={null}
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs gateway restart'
```

Restart one channel, then confirm with `channels status`. The start call returns `"started": true` even if the channel fails moments later, for example with a bad token, so the status is what to read:

```bash theme={null}
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs gateway call channels.start --params "{\"channel\":\"whatsapp\"}"'
cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs channels status'
```

Use `cpln workload force-redeployment RELEASE_NAME-openclaw --gvc GVC_NAME` only as a last resort (it takes the assistant offline for about 4 minutes), or after changing a key in your secret, because a new key reaches only a new replica. Pairings, the WhatsApp link and MCP sign-ins survive it.

### Scaling and Availability

The Gateway runs as exactly one replica, with no `replicas` value: OpenClaw supports a single Gateway per state directory, and each bot token can hold only one connection. A restart, a value change or a platform reschedule is therefore a short full outage, after which the Gateway comes back on the same volume with its state intact. Scale vertically with `resources`, keeping `maxCpu` at most 4 times `minCpu`. For more isolation, such as one assistant per team, install another release with its own secret and bot tokens.

### Troubleshooting

<AccordionGroup>
  <Accordion title="Workload never becomes ready and cpln logs returns nothing">
    **Symptom:** `cpln helm install` reported success, the workload has no ready replica, and `cpln logs` returns zero lines. `cpln workload get-deployments RELEASE_NAME-openclaw --gvc GVC_NAME -o yaml` shows under `status.versions[].message`:

    ```text theme={null}
    The secret SECRET_NAME no longer exists. Workload updates are paused until the secret is added
    or the reference to the secret removed.
    ```

    **Cause:** `secret.name` names a secret that does not exist, or a channel was enabled before its key was in the secret.

    **Fix:** Create the secret, or add the missing key, as in [Prerequisites](#prerequisites). The workload recovers on its own after a wait, or run `cpln workload force-redeployment RELEASE_NAME-openclaw --gvc GVC_NAME` to skip it.
  </Accordion>

  <Accordion title="Gateway exits at start with gateway-token must be at least 24 characters">
    **Symptom:** The container restarts repeatedly and the logs show:

    ```text theme={null}
    FATAL: key gateway-token in secret SECRET_NAME must be at least 24 characters (generate one with: openssl rand -hex 32)
    ```

    **Cause:** The `gateway-token` key is too short.

    **Fix:** Replace it with a longer value (export, edit and re-apply the secret as in [Connecting Chat Channels](#connecting-chat-channels)), then run `cpln workload force-redeployment RELEASE_NAME-openclaw --gvc GVC_NAME`. A running replica does not pick up a changed key by itself.
  </Accordion>

  <Accordion title="HTTP API returns 403 proxy_attribution_required">
    **Symptom:** A call from another workload to `/v1/chat/completions` fails with `403 proxy_attribution_required` even with the correct bearer token.

    **Cause:** In-GVC callers must send an `X-Forwarded-For` header with a non-loopback address. A missing header and `127.0.0.1` are refused alike.

    **Fix:** Add a header such as `X-Forwarded-For: 10.0.0.1` alongside `Authorization: Bearer`.
  </Accordion>

  <Accordion title="Control Plane sign-in link from chat fails with connection refused">
    **Symptom:** In `shared` mode you asked the bot to sign in to Control Plane; the link it sent ends at `127.0.0.1:8989` with *connection refused*.

    **Cause:** Shared sign-in completes only from a terminal.

    **Fix:** Sign in with the port-forward and `mcp login cpln` commands in [Connecting Control Plane Tools (MCP)](#connecting-control-plane-tools-mcp), or switch to `cplnMcp.signIn: per-requester` on a public install and ask for a new link.
  </Accordion>

  <Accordion title="You've reached your Codex subscription usage limit">
    **Symptom:** With `model.provider: openai`, replies fail with *You've reached your Codex subscription usage limit*.

    **Cause:** Most likely your OpenAI API account is out of credit or quota. OpenClaw runs `openai/*` models on its Codex runtime, which words billing errors this way.

    **Fix:** Check the credit and quota of the OpenAI account behind `llm-api-key`. To see the provider's real error (for example `credit_balance_exhausted`), pin the model to the `openclaw` runtime with `agents.defaults.models["openai/MODEL"].agentRuntime.id`.
  </Accordion>

  <Accordion title="Slack channel or Discord server messages are ignored">
    **Symptom:** The bot answers DMs but not messages in a Slack channel (the Gateway logs `channel-not-allowed`) or a Discord server (the Control UI shows Discord as *needs attention*). Or a Slack DM shows *Sending messages to this app has been turned off*.

    **Cause:** Only DMs are answered until a channel or server is allowlisted. The Slack DM message is an app setting.

    **Fix:** Allowlist the channel or server as described in the upstream [Slack](https://docs.openclaw.ai/channels/slack/access-control) and [Discord](https://docs.openclaw.ai/channels/discord/setup) docs. For the Slack DM message, open **App Home → Messages Tab**, allow messages, then reload Slack.
  </Accordion>

  <Accordion title="Memory search returns nothing after changing the model provider">
    **Symptom:** After switching `model.provider`, the assistant cannot recall anything it saved.

    **Cause:** The memory index was built for the previous provider.

    **Fix:** Rebuild it:

    ```bash theme={null}
    cpln workload exec RELEASE_NAME-openclaw --gvc GVC_NAME --container openclaw -- sh -c 'cd /app && node openclaw.mjs memory status --index --agent main'
    ```
  </Accordion>

  <Accordion title="Custom domain stopped working after a reinstall">
    **Symptom:** The domain reads ready, but the Control UI on it is unreachable after you uninstalled and reinstalled the release.

    **Cause:** A domain does not rebind to a new workload by itself.

    **Fix:** Delete the domain and create it again with the same route to port `18789` of `RELEASE_NAME-openclaw`.
  </Accordion>

  <Accordion title="Public endpoint still refused after enabling public access">
    **Symptom:** You upgraded with `publicAccess.enabled: true` and the canonical endpoint still refuses requests.

    **Cause:** An access change restarts the Gateway and takes from about 30 seconds to 10 minutes to take effect.

    **Fix:** Wait and re-test. If it persists, confirm the upgrade carried `publicAccess.enabled=true` with `cpln workload get RELEASE_NAME-openclaw --gvc GVC_NAME -o yaml`.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Approve only chat contacts you would trust as admins.** An approved contact can have the agent run shell commands in the container without asking, and those commands can read the gateway token and the workload's Control Plane token; per-requester MCP sign-in does not change that. To hold each command for your approval, run `node openclaw.mjs config set tools.exec.ask always` through `cpln workload exec` as in [Connecting](#connecting).
* **With `publicAccess.enabled`, an agent with a shell is on the internet behind one token.** Keep the gateway token long and secret, keep DM pairing on, and vet third-party skills.
* **Every new chat contact gets a pairing code** and is ignored until you approve it with `pairing approve CHANNEL CODE`.
* **Take a volume snapshot before changing `image.tag`.** Image upgrades run one-way state migrations, so a downgrade cannot read migrated state.
* **The assistant spends model tokens on its own.** OpenClaw runs a heartbeat turn every 30 minutes by default; change or turn it off in the Control UI.
* **After changing `model.provider`, rebuild the memory index** with `node openclaw.mjs memory status --index --agent main`; until then memory search returns nothing.
* **The Control UI microphone only dictates until you choose a voice provider.** For spoken conversation (Talk), run `node openclaw.mjs config set talk.realtime.provider openai`.
* **To run sessions on another machine** (a laptop, a VM), pair it as a [session host](https://docs.openclaw.ai/gateway/cloud-sessions) using the public endpoint. With `model.provider: openai` it also needs the [Codex placement](https://docs.openclaw.ai/plugins/codex-harness/placement) steps, or it stays greyed out in the picker.
* **A changed key in your secret needs a forced redeployment** to reach the Gateway, which takes the assistant offline for about 4 minutes.
* **A custom domain must be deleted and recreated after a reinstall**; it does not rebind to the new workload.
* **Uninstall deletes the volume and all state**, including the WhatsApp link, MCP sign-ins, sessions and workspace, with no tested way to restore it into a new install. Copy out anything you need first. Your own prerequisite secret is not deleted.

## External References

<CardGroup cols={2}>
  <Card title="OpenClaw on GitHub" icon="github" href="https://github.com/openclaw/openclaw">
    Upstream source repository
  </Card>

  <Card title="OpenClaw Documentation" icon="book" href="https://docs.openclaw.ai/">
    Official documentation
  </Card>

  <Card title="Control UI" icon="window" href="https://docs.openclaw.ai/web/control-ui">
    Using the browser Control UI
  </Card>

  <Card title="Channels" icon="comments" href="https://docs.openclaw.ai/channels">
    Chat channel setup and access control
  </Card>

  <Card title="OpenClaw Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/openclaw">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
