Running Locally: Startup Flow ๐ข
Understanding how OpenClaw starts up helps you know where things happen and makes debugging much easier. This chapter traces the complete startup chain from
openclaw.mjsto a running Gateway.
Learning Objectives
After reading this chapter, you'll be able to:
- Trace the complete startup chain:
openclaw.mjsโentry.tsโrunCli() - Understand the respawn mechanism (why there are two Node.js processes)
- Know where the Gateway startup sequence happens
- Read and understand the CLI command tree
I. The Entry Point: openclaw.mjs
Everything starts with openclaw.mjs, a pure JavaScript wrapper (no TypeScript, runs directly with Node.js):
// openclaw.mjs โ simplified
const require = createRequire(import.meta.url);
// Node.js version check
const [major] = process.versions.node.split('.').map(Number);
if (major < 20) {
console.error('OpenClaw requires Node.js 20+');
process.exit(1);
}
// Enable V8 compile cache
require('v8-compile-cache');
// Load jiti for TypeScript support, then run entry.ts
const { runCli } = require('./src/entry.ts');
runCli();
Why a .mjs wrapper? Because entry.ts uses TypeScript syntax and requires jiti to run directly โ but jiti itself needs to be loaded first.
II. Respawn Mechanism
OpenClaw uses a two-process architecture:
flowchart TB
FIRST["Process 1\n(Launcher)"]
CHECK{"Need respawn?\n(version, config update)"}
SPAWN["Spawn Process 2\n(Main Worker)"]
SECOND["Process 2\n(Main Worker)\nโ Runs Gateway\nโ Handles requests"]
RESTART["Process 1 detects\nProcess 2 exit\nโ Re-spawn"]
FIRST --> CHECK
CHECK -->|"Yes"| SPAWN
CHECK -->|"No"| SECOND
SPAWN --> SECOND
SECOND -->|"Crash or exit"| RESTART
RESTART --> SPAWN
Source: src/entry.respawn.ts
The respawn mechanism ensures:
- Auto-restart on crash: The main worker is automatically respawned
- Config hot reload: After config changes, restarting the worker picks up new configuration
- Version updates: After an npm update, the launcher detects version changes and triggers respawn
III. runCli() โ The Main Entry
src/entry.ts exports runCli(), the actual startup logic:
// src/entry.ts (simplified)
// 1. Parse command-line arguments
const args = process.argv.slice(2);
// 2. Handle fast paths (help, version)
if (args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
// 3. Load configuration
const cfg = await loadConfig();
// 4. Route to the correct CLI command
const command = resolveCommand(args);
await command.execute(cfg, args);
}
IV. CLI Command Tree
OpenClaw provides a rich CLI command set:
openclaw
โโโ serve โ Start the Gateway server (most important)
โโโ chat โ Start a direct chat session (bypasses Gateway)
โโโ onboard โ Interactive setup wizard
โโโ status โ Show current status
โโโ config โ Configuration management
โ โโโ get <key>
โ โโโ set <key> <value>
โ โโโ validate
โโโ secrets โ Secret management
โ โโโ set <key>
โ โโโ list
โโโ cron โ Cron job management
โ โโโ list
โ โโโ run <job-id>
โ โโโ status
โโโ session โ Session management
โ โโโ list
โ โโโ clear <session-key>
โโโ plugin โ Plugin management
โโโ list
โโโ install <plugin-id>
V. Gateway Startup Sequence
When you run openclaw serve, the Gateway starts through these steps:
flowchart TB
START["openclaw serve"]
LOAD_CFG["Load config.yaml\n(+ environment variable substitution)"]
DISCOVER["Plugin discovery\n(scan extensions/ directories)"]
LOAD_PLUGINS["Load plugins\n(jiti + SDK alias resolution)"]
subgraph SERVERS["Start Servers"]
HTTP["HTTP Server\n(:4000 default)"]
WS["WebSocket Server\n(same port)"]
CHAN["Channel Manager\n(start all channel adapters)"]
end
HEARTBEAT["Start heartbeat timer\n(default 30 min)"]
CRON["Start cron scheduler"]
READY["Gateway Ready โ
\nListening for connections..."]
START --> LOAD_CFG --> DISCOVER --> LOAD_PLUGINS
LOAD_PLUGINS --> SERVERS
SERVERS --> HEARTBEAT --> CRON --> READY
VI. isMainModule Pattern
You'll see this pattern throughout src/:
// src/entry.ts
if (import.meta.url === `file://${process.argv[1]}`) {
// Only run when this file is the main entry point
// Prevents double execution when imported as a module
runCli();
}
This is the ESM equivalent of Node.js's require.main === module, preventing the startup function from running when the file is imported as a dependency.
Key Source Files
| File | Size | Role |
|---|---|---|
openclaw.mjs | ~30 lines | CLI wrapper, Node.js version check |
src/entry.ts | 11KB | Main entry, runCli() |
src/entry.respawn.ts | 11KB | Respawn mechanism |
src/cli/ | - | CLI command implementations |
src/gateway/server-http.ts | 1111 lines | HTTP/WebSocket server |
src/gateway/server-channels.ts | 20KB | Channel lifecycle management |
Summary
- Two-process architecture: Launcher (process 1) watches and respawns the Main Worker (process 2).
openclaw.mjsโentry.tsโrunCli(): The startup chain is straightforward.- Gateway startup is ordered: config โ plugin discovery โ plugin loading โ servers โ heartbeat/cron.
isMainModuleprevents double execution: Standard ESM pattern, seen throughout the codebase.