Build an OpenClaw Multi-Agent Platform Workflow with LangGraph
OpenClaw gives agents live browser, terminal, and desktop control; LangGraph gives them state. Here is a supervisor-plus-workers reference implementation with typed retries.
Deepak Bagada
CEO, SaaSNext
- Treat OpenClaw as a capability fabric and LangGraph as the orchestrator: the runtime executes, the graph decides.
- A typed task ledger keyed by stable ids prevents worker collisions and makes retries deterministic.
- Bound every retry class — transport backoff, graph re-queue, and side-effectful terminal commands need separate policies.
- OpenClaw is model-agnostic, so one graph can route supervisors and workers across Claude, GPT, Gemini, or local Ollama.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
OpenAI's early-2026 acquisition of OpenClaw — the open-source agent platform that began life as Clawdbot/Moltbot — quietly changed what "autonomous agent" means in production. OpenClaw is not another function-calling framework. It is a local-first daemon that runs on macOS, Linux, and Windows and gives agents real operational control: a live browser it can click through, a real terminal it can drive, a desktop it can see, and a registry of more than 20,000 community tools. Pair that runtime with LangGraph as the orchestration layer and you can build a deterministic, checkpointable multi-agent platform where a supervisor decomposes a mission and specialist workers execute it through a single shared machine-interface fabric.
This dispatch is a reference implementation of exactly that: a supervisor-plus-workers platform built on LangGraph 0.2+ and wired to a running OpenClaw daemon through a thin MCP-compatible tool bridge. Everything is model-agnostic on purpose — OpenClaw routes the same toolset through Claude, GPT, Gemini, or local Ollama checkpoints, so the graph below runs unchanged against whichever model your fleet prefers.
What OpenClaw Actually Changes
Most agent stacks built before OpenClaw were API-only: an LLM could invoke a function but could not see a screen, hold a browser session, or operate a native application. That limitation forced every "autonomous" workflow into the narrow world of REST endpoints. OpenClaw removes the boundary by exposing four capability classes through one daemon:
- Live browser control — real Chromium sessions with DOM access, clicks, scroll, and form submission, not headless stubs.
- Terminal control — sandboxed shell execution on the host machine with confirmation and policy hooks.
- Desktop/screen control — screen-aware automation of native macOS and Windows applications, with OCR and layout extraction.
- Model-agnostic tool calling — a single tool registry served to Claude, GPT, Gemini, or local Ollama models alike.
The 20,000+ tool count matters less than the architectural consequence: your orchestration layer no longer needs to know how a tool works, only when to call it. That is the correct division of labor, and it is exactly what LangGraph is built for. For the broader catalog of production agent patterns, watch the Daily AI World workflows library — these reference architectures are stabilizing fast in 2026.
Architecture: Supervisor, Workers, Shared Ledger
The design uses a supervisor pattern with a shared state ledger. A supervisor agent reads the mission, decomposes it into typed tasks, and assigns each to a specialist worker. Workers are LangGraph nodes that invoke OpenClaw tools through the bridge. A ledger key in graph state tracks attempts and outcomes so workers never collide and failed tasks can be re-queued.
┌──────────────────────────┐
│ PlatformState │
│ mission + tasks + ledger │
└────────────┬─────────────┘
│
┌───────────▼───────────┐
│ Supervisor │
│ decompose the mission │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Dispatch │
│ mark QUEUED→RUNNING │
└───────────┬───────────┘
┌──────────────────┼───────────────────┐
│ │ │
┌────────▼────────┐ ┌───────▼────────┐ ┌────────▼────────┐
│ Browser Worker │ │ Terminal Worker│ │ Screen Worker │
│ browser_act │ │ terminal_run │ │ screen_observe │
└────────┬────────┘ └───────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────┼───────────────────┘
│
┌───────────▼───────────┐
│ Retry or finish? │
└───────┬───────────┬───┘
retry │ │ compile
┌───────▼──┐ ┌─────▼──────┐
│ Dispatch │ │ Compile │
│ (re-queue)│ │ report+END │
└──────────┘ └────────────┘
Every edge in this graph is checkpointable. If the daemon restarts mid-mission, LangGraph's checkpointer resumes from the last completed node instead of replaying the whole mission.
Prerequisites and Project Layout
Install OpenClaw, start the daemon, and confirm the tool bridge responds before you write any graph code:
brew install openclaw # macOS; apt/yum on Linux
openclaw daemon start # local-first daemon
openclaw tools list | head -40 # confirm the bridge is live
openclaw auth bridge --issue # mint a bridge token for your app
Then scaffold the project:
openclaw-platform/
├── .env # model routing + daemon connection
├── schemas.py # Pydantic state and task models
├── tools.py # LangChain tool wrappers over the OpenClaw bridge
├── graph.py # supervisor + workers + conditional edges
└── main.py # entrypoint with checkpointer
Environment Configuration
.env keeps model routing and daemon credentials out of your code:
# .env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AIza...
OLLAMA_BASE_URL=http://localhost:11434
SUPERVISOR_MODEL=anthropic/claude-sonnet-4-5
WORKER_MODEL=openai/gpt-5.2
LOCAL_FALLBACK_MODEL=ollama/qwen3-coder
OPENCLAW_DAEMON_URL=http://127.0.0.1:18255
OPENCLAW_BRIDGE_TOKEN=oc_br_...
MAX_SCREEN_WAIT_SECONDS=120
MAX_BROWSER_STEPS=25
TOOL_TIMEOUT_SECONDS=90
Never commit this file; the bridge token is a live credential. Rotate it with openclaw auth bridge --revoke whenever you rotate the daemon keys.
State and Schema Definitions
schemas.py defines the graph state and the typed task ledger:
# schemas.py
from __future__ import annotations
from enum import Enum
from typing import Any, Literal, Optional
from langgraph.graph import MessagesState
from pydantic import BaseModel, Field
class TaskStatus(str, Enum):
QUEUED = "queued"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
class Task(BaseModel):
id: str = Field(..., description="Stable task id used by the ledger.")
owner: Literal["browser", "terminal", "screen"]
instruction: str
status: TaskStatus = TaskStatus.QUEUED
attempts: int = 0
result: Optional[str] = None
class PlatformState(MessagesState):
mission: str
tasks: list[Task] = Field(default_factory=list)
ledger: dict[str, dict[str, Any]] = Field(default_factory=dict)
final_report: Optional[str] = None
class ToolResult(BaseModel):
ok: bool
output: str = ""
error: str = ""
duration_ms: int = 0
Keeping tasks as Pydantic models rather than raw dicts means the graph can serialize them cleanly to the checkpointer and the ledger stays diffable.
OpenClaw Tool Wrappers
tools.py exposes three LangChain tools, each a thin client over the OpenClaw bridge. All three share one transport with a retry-aware POST helper:
# tools.py
from __future__ import annotations
import json
import os
import random
import time
import httpx
from langchain_core.tools import tool
BRIDGE = os.getenv("OPENCLAW_DAEMON_URL", "http://127.0.0.1:18255")
TOKEN = os.getenv("OPENCLAW_BRIDGE_TOKEN", "")
TIMEOUT = float(os.getenv("TOOL_TIMEOUT_SECONDS", "90"))
def _post(path: str, payload: dict) -> dict:
# Transport with exponential backoff for 429/5xx (see Retry Rules).
attempt = 0
while True:
resp = httpx.post(
f"{BRIDGE}/{path}",
json=payload,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=TIMEOUT,
)
if resp.status_code < 500 or attempt >= 4:
resp.raise_for_status()
return resp.json()
attempt += 1
time.sleep(min(2 ** attempt + random.uniform(0, 0.5), 30))
@tool
def browser_act(url: str, action: str) -> ToolResult:
# OpenClaw live-browser control: 'goto', 'click', 'type', 'scroll'.
try:
out = _post("tools/browser/act", {"url": url, "action": action})
return ToolResult(ok=True, output=json.dumps(out))
except Exception as exc:
return ToolResult(ok=False, error=str(exc))
@tool
def terminal_run(command: str, cwd: str = ".") -> ToolResult:
# Execute a shell command on the OpenClaw daemon host.
started = time.perf_counter()
try:
out = _post("tools/terminal/run", {"command": command, "cwd": cwd})
return ToolResult(
ok=True,
output=out.get("stdout", ""),
duration_ms=int((time.perf_counter() - started) * 1000),
)
except Exception as exc:
return ToolResult(ok=False, error=str(exc))
@tool
def screen_observe(region: str = "active") -> ToolResult:
# Capture and describe the active screen region (OCR + layout).
try:
out = _post("tools/screen/observe", {"region": region})
return ToolResult(ok=True, output=json.dumps(out))
except Exception as exc:
return ToolResult(ok=False, error=str(exc))
The _post helper encodes the transport-level retry policy once; every future tool wrapper inherits it for free.
The LangGraph Platform Graph
graph.py assembles supervisor, dispatch, workers, and conditional retry logic:
# graph.py
from __future__ import annotations
import json
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, StateGraph
from schemas import PlatformState, Task, TaskStatus, ToolResult
from tools import browser_act, screen_observe, terminal_run
MAX_ATTEMPTS = 3
class Supervisor:
def __init__(self) -> None:
self.llm = ChatOpenAI(model="gpt-5.2", temperature=0.1)
def __call__(self, state: PlatformState) -> dict:
prompt = (
"Decompose the mission below into concrete tasks. "
'Reply with ONLY a JSON array like '
'[{"owner": "browser|terminal|screen", "instruction": "..."}].
'
f"Mission: {state.mission}"
)
raw = self.llm.invoke(prompt).content.strip()
raw = raw.removeprefix("```json").removesuffix("```").strip()
payload = json.loads(raw)
tasks = [
Task(id=f"t{i:03d}", owner=t["owner"], instruction=t["instruction"])
for i, t in enumerate(payload)
]
return {"tasks": tasks}
def _dispatch(state: PlatformState) -> dict:
ledger = dict(state.ledger)
for task in state.tasks:
if task.status == TaskStatus.QUEUED:
task.status = TaskStatus.RUNNING
ledger[task.id] = {"attempts": task.attempts, "status": "running"}
return {"ledger": ledger}
def _run_browser(state: PlatformState) -> dict:
return _execute(state, "browser", browser_act)
def _run_terminal(state: PlatformState) -> dict:
return _execute(state, "terminal", terminal_run)
def _run_screen(state: PlatformState) -> dict:
return _execute(state, "screen", screen_observe)
def _execute(state: PlatformState, owner: str, fn) -> dict:
tasks, ledger = list(state.tasks), dict(state.ledger)
for task in tasks:
if task.owner != owner or task.status != TaskStatus.RUNNING:
continue
task.attempts += 1
try:
result: ToolResult = fn.invoke({"query": task.instruction})
task.result = result.output
task.status = TaskStatus.DONE if result.ok else TaskStatus.FAILED
except Exception as exc:
task.result = f"exception: {exc}"
task.status = TaskStatus.FAILED
ledger[task.id] = {"attempts": task.attempts, "status": task.status.value}
return {"tasks": tasks, "ledger": ledger}
def _retry_or_finish(state: PlatformState) -> str:
for task in state.tasks:
if task.status == TaskStatus.FAILED and task.attempts < MAX_ATTEMPTS:
task.status = TaskStatus.QUEUED
return "retry"
return "compile"
def _compile(state: PlatformState) -> dict:
lines = [
f"- {t.id}: {t.status.value} - {t.result or t.instruction}"
for t in state.tasks
]
return {"final_report": "
".join(lines)}
def build_platform() -> StateGraph:
g = StateGraph(PlatformState)
g.add_node("supervisor", Supervisor())
g.add_node("dispatch", _dispatch)
g.add_node("browser", _run_browser)
g.add_node("terminal", _run_terminal)
g.add_node("screen", _run_screen)
g.add_node("compile", _compile)
g.set_entry_point("supervisor")
g.add_edge("supervisor", "dispatch")
g.add_edge("dispatch", "browser")
g.add_edge("browser", "terminal")
g.add_edge("terminal", "screen")
g.add_conditional_edges(
"screen",
_retry_or_finish,
{"retry": "dispatch", "compile": "compile"},
)
g.add_edge("compile", END)
return g
Entry Point
main.py compiles the graph with a checkpointer so missions resume after a crash:
# main.py
import asyncio
import os
from dotenv import load_dotenv
from graph import build_platform
from schemas import PlatformState
load_dotenv()
async def main() -> None:
graph = build_platform().compile(checkpointer=InMemorySaver())
mission = os.getenv(
"MISSION",
"Scan the latest AI news, pick 5 headlines, write them to /tmp/headlines.md",
)
result = await graph.ainvoke(
PlatformState(mission=mission),
config={"configurable": {"thread_id": "mission-001"}},
)
print("=== FINAL REPORT ===")
print(result.get("final_report", "no report"))
if __name__ == "__main__":
asyncio.run(main())
Run it with:
MISSION="Pull today's MCP changelog and save a summary" python3 main.py
Retry Rules
Every layer of the platform has an explicit, bounded retry policy:
| Layer | Trigger | Action | Cap |
|---|---|---|---|
| Transport | HTTP 429 or 5xx | Exponential backoff 2**attempt + jitter, capped at 30 s |
4 attempts |
| Graph | Tool call throws or ToolResult.ok == False |
Re-queue task at Dispatch | MAX_ATTEMPTS = 3 |
| Browser | Element not visible after action | Re-observe screen, wait up to MAX_SCREEN_WAIT_SECONDS |
2 waits per step |
| Terminal | Non-zero exit code | NOT retried by default (stateful side effects) | flag to force |
| Ledger | Duplicate task id | Skip, log conflict | idempotent by construction |
The transport backoff lives in _post(); the graph-level re-queue lives in _retry_or_finish(). Terminal commands are deliberately not auto-retried because re-running rm, deploy, or git push can double-apply side effects — mark the task needs_retry=True explicitly if the command is idempotent.
Operating Notes
Three things break in practice. First, model drift: the supervisor's JSON output can deviate from the schema; validate with Pydantic and re-prompt on a ValidationError rather than trusting the model. Second, daemon availability: OpenClaw is local-first, so the bridge token and daemon must be supervised — treat them like a database connection, with health checks and reconnects. Third, cost accounting: model-agnostic routing means one mission can bill across three providers; log model per node invocation so chargebacks stay honest. LangGraph's checkpointer makes all of this auditable — replay a thread and you get the exact tool-call history.
Next Steps
Start with a single worker class (terminal is the easiest to validate), then layer browser and screen workers once the ledger logic is proven. If you plan to expose OpenClaw tool calls to external agents, standardize them as MCP servers first — our MCP directory has guides on registering local tool servers so any host can discover them. And since OpenClaw is a fast-moving acquisition target, track the platform's direction on the latest AI news page before committing to version-locked features.
The pattern here — a stateful orchestrator, a capability-fabric runtime, and typed retries — is the shape of most production agent platforms in 2026. Build the ledger, keep the retries bounded, and let the models disagree about strategy while your graph enforces the structure.
FAQ
What does OpenClaw add that LangGraph does not provide?
LangGraph provides orchestration state, checkpoints, and control flow. OpenClaw provides physical and OS-level capability: live browser, terminal, and desktop control plus 20,000+ tools. LangGraph decides when a tool should run; OpenClaw decides how that tool executes.
Can the same graph run on Claude, GPT, Gemini, and local Ollama models?
Yes. OpenClaw is model-agnostic and LangGraph lets you swap the LLM per node. The graph in this dispatch reads model names from .env, so a single codebase can route the supervisor to a frontier model and workers to a cheaper local checkpoint.
Is OpenClaw still open source after the OpenAI acquisition?
OpenClaw remains an open-source, local-first platform you can self-host on macOS, Linux, or Windows. The acquisition gave it a larger team and tighter integration with OpenAI's model line, but the daemon and tool registry stay installable and inspectable.
How do I prevent workers from colliding on shared state?
The ledger keyed by stable task ids is the answer. Every worker updates only the tasks it owns and writes outcomes into the ledger, so the supervisor can always reconstruct the mission's progress without a shared mutable filesystem.
What happens if the OpenClaw daemon restarts mid-mission?
With a LangGraph checkpointer, the graph resumes from the last completed node. Tasks that were mid-tool-call are re-queued under the Retry Rules; tasks that completed stay recorded in the ledger, so nothing is re-executed twice.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...