On this page
On this page
Gateway
Gateway integrations for external apps
External apps talk to OpenClaw through the Gateway protocol: WebSocket transport plus RPC methods. Use it when a script, dashboard, CI job, IDE extension, or another process wants to start agent runs, stream events, wait for results, cancel work, or inspect Gateway resources.
What is available today
| Surface | Status | Use it for |
|---|---|---|
| Gateway client guide | Stable packages | npm packages, auth, reconnect, history, events, approvals, and version policy. |
| Embedding guide | Release train | Child-process environment, readiness, lifecycle, recovery, RPC ownership, and packaging. |
| Gateway protocol | Ready | WebSocket transport, connect handshake, auth scopes, protocol versioning, and events. |
| Gateway protocol RPC methods | Ready | Current Gateway methods for agents, sessions, tasks, models, tools, artifacts, and approvals. |
openclaw agent |
Ready | One-shot script integration when shelling out to the CLI is enough. |
openclaw message |
Ready | Sending messages or channel actions from scripts. |
Recommended path
- Run or discover a Gateway.
- Connect over the Gateway protocol.
- Call documented RPC methods from Gateway protocol RPC methods.
- Pin the OpenClaw version you test against.
- Recheck the RPC reference when upgrading OpenClaw.
For agent runs, start with the agent RPC and pair it with agent.wait for a
terminal result. For durable conversation state, use the sessions.* methods.
For UI integrations, subscribe to Gateway events and render only the event
families your app understands.
agent.wait can return status: "pending" while a turn is queued. A timeout
response without terminal metadata means the wait expired; continue waiting or
consume lifecycle events. Terminal status: "error" can represent cancellation:
stopReason: "superseded" means a newer session writer replaced the run. Preserve
that reason when presenting the result.
Cooperative host suspension
Hosting controllers that freeze or snapshot a running process can use the host-neutral suspension handshake:
- Stop admitting external ingress controlled by the host.
- Call
gateway.suspend.preparewith a stable, uniquerequestId. - If the response is
busy, keep the process running and retry later. To hold admission closed while already-admitted work finishes, request the optional drain mode and pollgateway.suspend.statusinstead. - If the response is
ready, save the returnedsuspensionId, then freeze or snapshot the process beforeexpiresAtMs. - After thaw, or if suspension is abandoned, call
gateway.suspend.resumewith thatsuspensionIdover the existing or a newly authenticated WebSocket. The CLI equivalents areopenclaw gateway suspendandopenclaw gateway resume <suspensionId>.
A draining or prepared Gateway accepts authenticated operator WebSocket
connections, allowing a controller to reconnect and check, renew, or release
its own lease. New node and worker connections remain fenced. A prepared
Gateway fences every method except gateway.suspend.* and one exact
predecessor-bound restart. That exception requires a non-safe
gateway.restart.request whose target matches the live Gateway lock; safe and
untargeted restart requests remain fenced. That restart RPC exception is not
available while the Gateway is still draining. Controllers may reconnect after thaw and
call resume. The
Admin HTTP RPC plugin remains available for hosts
that cannot speak WebSocket at all. If every control path is lost, the
lease expiry reopens admission automatically: 45 seconds for a drain, or two
minutes for an idle freeze/snapshot lease.
Closing the Gateway cancels background work queued by operator reconnects without waiting for suspension expiry. Shutdown still waits for work already running to finish.
Lifecycle operator requests take priority over unrelated queued work. Their authentication, live authority checks, and same-connection mutation ordering still apply. Hello does not wait for session membership refreshes: people remain visible, while pending watched-session references are omitted until a subsequent presence publication.
The hello snapshot includes suspension: { phase }, and gateway.suspension
events publish admission changes immediately. The phase is accepting,
preparing, draining, or prepared; neither surface exposes suspension IDs.
The Control UI's bottom-left connection indicator shows Suspending… during
preparation or draining and Suspended while prepared, including in Settings.
It clears when suspension admission reopens, not when a request succeeds.
Offline and restart indicators take precedence; there is no separate resuming phase.
The RPC contract is:
gateway.suspend.prepare—operator.admin; params{ "requestId": "stable-host-operation-id", "terminalPolicy": "preserve", "drain": true }gateway.suspend.status—operator.read; params{ "suspensionId": "id-from-prepare", "includeLifecycle": true }gateway.suspend.resume—operator.admin; params{ "suspensionId": "id-from-prepare" }gateway.suspend.handoff—operator.admin; params{ "suspensionId": "id-from-prepare", "target": { "pid": 123, "processInstanceId": "id-from-system-info" } }
terminalPolicy and drain are optional. terminalPolicy accepts only
"preserve" or "terminate" and defaults to "preserve"; drain defaults
to false. The terminal policy applies to both immediate preparation and drain
mode:
"preserve": open terminal sessions block suspension. Use this for host freeze/snapshot operations that must preserve the running process."terminate": open process-local terminal sessions do not block suspension. Use this for release updates that will restart the Gateway. Preparation does not close terminals; the actual Gateway restart ends their PTYs and commands.
Pending final chat-state writes (terminal-persistence) and all other tracked
work still block preparation under either policy.
IDs are trimmed, must contain a non-whitespace character, and are limited to
128 characters. A busy prepare result has status: "busy", reason,
retryAfterMs, activeCount, and blockers. A ready result has this shape:
If drain: true finds active work, preparation acquires a bounded lease,
pauses new automatic cron scheduling, closes admission to unrelated new work,
and returns:
Already-admitted work and its owned completions continue naturally; unrelated
new runs, sessions, scheduled jobs, and independent work stay rejected. With
terminalPolicy: "preserve", an open terminal can keep the lease draining until
it closes, the controller resumes the Gateway, or the lease expires. With
terminalPolicy: "terminate", that same terminal remains open but does not
block readiness. Neither policy terminates or detaches terminals during
preparation, polling, renewal, resume, or lease expiry.
Drain leases expire 45 seconds after initial preparation: a 30-second drain
budget plus 15 seconds for handoff or cleanup. Repeating prepare, polling, and
reaching ready do not extend that deadline. A lost controller therefore cannot
leave users fenced for minutes. RPC connections may close and reconnect without
releasing the lease; CLI and HTTP controllers use separate connections for each
call. A successful handoff must commit shutdown before expiry. Expiry never
interrupts admitted work or discards pending writes; it resumes scheduling and
reopens admission, logging a warning. Controllers must defer if they cannot
complete handoff within the deadline. Entry and exit logs identify the owner
request, lease, caller, and reason; suspension IDs remain absent from public
presence events.
Poll gateway.suspend.status with the returned suspensionId, honoring
retryAfterMs. A held drain recommends five-second polling so completed work
does not leave new work waiting through the longer busy-preparation retry.
While blockers remain, status returns status: "draining"
together with expiresAtMs, retryAfterMs, activeCount, and blockers.
Each status call immediately returns a fresh active-work snapshot, including
pending protected lifecycle or final-chat writes. Polling never waits for those
writes, cancels work, releases custody, or renews the lease. The handoff owner
checks write custody again before authorizing a stop.
Once every blocker has finished, the same lease transitions to
{"status":"ready","expiresAtMs":...}.
Status preserves the original response shape by default for published validators.
Opt into lifecycle metadata with includeLifecycle: true to receive ownerId,
the original requestId; draining status also includes phase: "draining".
Use an updated response validator when requesting this metadata.
Status returns {"status":"running"} when no suspension is held; querying a
different active lease returns a conflict without exposing its identifiers.
Resume returns {"ok":true,"status":"running","resumed":true}; repeating it
after a successful resume returns resumed: false.
The dedicated openclaw gateway suspend command retains its existing
refuse-only behavior. Controllers can request drain mode through any Gateway
client or the generic CLI RPC command:
For a release update, use the same handshake with terminalPolicy: "terminate"
so an open terminal cannot hold the drain indefinitely:
Wait for the lease to become ready before performing the checked restart.
Terminal commands and scrollback are not recovered after restart; see
Restart recovery.
An external deployment controller that explicitly authorizes interrupting
remaining work can instead call gateway.suspend.handoff after its own graceful
drain budget. The target must match the pid and processInstanceId obtained from
system.info before suspension. This arms
restart cleanup for that exact lease and host iteration's next SIGTERM; it
does not send a signal or create a successor. The controller still owns the
native service restart. A successful response is
{ "status": "armed", "suspensionId": "...", "expiresAtMs": ... }.
The arm expires with the lease. Repeating prepare or handoff does not extend
armed authority. Resume, replacement, another accepted lifecycle action, or
host retirement invalidates it. Pending final-chat persistence refuses arming
and is checked again when SIGTERM consumes the arm. If that check refuses,
the Gateway logs the refusal and retains ordinary graceful-stop behavior.
An accepted handoff uses the existing restart recovery and abort cleanup,
then exits for the external controller. An ordinary stop without an arm keeps
waiting for active work. Controllers must defer on unsupported methods or
refused handoffs; a draining lease alone never authorizes interruption.
Once shutdown commits, including an installation-replaced restart during a
held suspension, the same owner can still poll status: "draining". With
includeLifecycle: true, it also receives phase: "interrupting". Ownership and
foreign-token conflicts remain stable across authenticated operator reconnects;
node and worker connections remain fenced. The shutdown record remains available
past the old lease expiry; this is shutdown progress, not a renewable lease or
permission to freeze the process. Resume is refused after shutdown commits.
The owner records phase: "exiting" before server teardown; RPC access ends
when that teardown closes request admission and transports. These facts remain
in memory until process exit or the next in-process lifecycle resets them.
The running Gateway serves this contract; staging a newer installation does not change an older resident's responses. Request lifecycle metadata only once a Gateway version supporting it is running. Drivers must still verify their exact predecessor and handle transport closure through their lifecycle owner.
A competing request ID returns retryable UNAVAILABLE with retryAfterMs.
Resume and lease expiry request cron scheduling to resume, then reopen admission.
Cron worker refresh failures are reported in logs; suspension does not add a
separate fail-closed scheduler recovery loop. A mismatched resume ID returns
INVALID_REQUEST. Prepare is subject to the
Gateway's control-plane write limit of 30 attempts per minute; honor the
returned retry delay. WebSocket clients are bucketed by device and IP. Admin HTTP
controllers are bucketed by resolved client IP, so controllers behind one
proxy can share a budget.
Without drain: true, preparation remains refuse-only: OpenClaw closes new
root/session/command admission, pauses automatic cron ticks, and inspects work
synchronously. If anything is active, it resumes the scheduler and reopens
admission before returning busy; it does not interrupt or drain that work.
With drain: true, the same suspension owner instead keeps admission closed
and cron scheduling paused until existing work settles. Already-owned cron
completion and reconciliation continue.
The lease budget starts before work inspection: 45 seconds with drain: true,
or two minutes for refuse-only freeze/snapshot preparation. Preparation that
exhausts its budget resumes scheduling and fails instead of returning an expired
lease. Clock rollback does not extend the budget. Repeat prepare before
expiresAtMs with the same requestId, terminal policy, and drain mode to
observe the same suspensionId; changing any of those values conflicts with the
existing lease. Only refuse-only leases renew, and only before a restart handoff
is armed. Use status for routine polling to avoid consuming the write budget. Explicit resume and lease expiry
restore scheduling before reopening admission. Leases remain in memory and
disappear if the Gateway process exits.
Restart emission that becomes due during a ready lease waits until the lease
resumes; an in-flight restart makes preparation return busy.
While draining or ready, /healthz remains live and /readyz returns 503.
Local or authenticated readiness responses include gateway-draining;
unauthenticated remote checks receive only { "ready": false }. The HTTP health
check, suspension methods on authenticated operator WebSocket connections, and
an already-enabled Admin HTTP RPC route remain available. Other unrelated RPCs
return retryable UNAVAILABLE. Built-in HTTP user-work routes and ordinary
plugin HTTP routes,
including OpenAI-compatible APIs, tool/session operations, node watches, and
configured hooks, return 503 with error.code: "gateway_unavailable". New
plugin-owned WebSocket upgrades also return 503; this covers upgrade
ownership, not work performed later over an established plugin socket.
This handshake does not persist incoming messages, stop third-party channel
transports, or control the hosting platform. The host must fence its ingress
before preparation and remains responsible for wake, snapshot/freeze, and
stop. activeCount is the aggregate tracked-work count, while blockers
contains non-zero native category counts and bounded summary messages. Categories
include background-exec, cron-run, agent-run, acp-run, and
media-generation, alongside request, queue, reply, session, and terminal work.
Categories can overlap, so the count is not a number of unique jobs. This is not
a general process-quiescence barrier. Blockers contain no command text, output,
operating system process IDs, or session or scope identifiers. Channel health, maintenance,
cache refresh, established
plugin WebSocket sessions, and unregistered plugin-owned background work can
remain active.
The hosting platform must freeze or snapshot the full process tree and its
filesystem consistently; unregistered work cannot be proven idle by this first
contract.
App code vs plugin code
Use Gateway RPC when code lives outside OpenClaw:
- Node scripts that start or observe agent runs
- CI jobs that call a Gateway
- dashboards and admin panels
- IDE extensions
- external bridges that do not need to become channel plugins
- integration tests with fake or real Gateway transports
Use the Plugin SDK when code runs inside OpenClaw:
- provider plugins
- channel plugins
- tool or lifecycle hooks
- agent harness plugins
- trusted runtime helpers
External apps should not import openclaw/plugin-sdk/*; those subpaths are for
plugins loaded by OpenClaw.