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.mjs to a running Gateway.

Learning Objectives

After reading this chapter, you'll be able to:


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:


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

FileSizeRole
openclaw.mjs~30 linesCLI wrapper, Node.js version check
src/entry.ts11KBMain entry, runCli()
src/entry.respawn.ts11KBRespawn mechanism
src/cli/-CLI command implementations
src/gateway/server-http.ts1111 linesHTTP/WebSocket server
src/gateway/server-channels.ts20KBChannel lifecycle management

Summary

  1. Two-process architecture: Launcher (process 1) watches and respawns the Main Worker (process 2).
  2. openclaw.mjs โ†’ entry.ts โ†’ runCli(): The startup chain is straightforward.
  3. Gateway startup is ordered: config โ†’ plugin discovery โ†’ plugin loading โ†’ servers โ†’ heartbeat/cron.
  4. isMainModule prevents double execution: Standard ESM pattern, seen throughout the codebase.

โ† Codebase Navigation | โ†’ System Architecture Layers