How to Run OpenWork in Headless Mode: Complete Automation and CI Guide

To run OpenWork in headless mode, execute pnpm dev:headless-web to launch a Vite-served web UI and local API server without the Electron desktop shell, enabling browser-based automation, CI pipelines, and agent-driven workflows.

OpenWork is an open-source workspace platform that typically operates within an Electron desktop environment. When you need to run OpenWork in headless mode for automated testing, continuous integration, or background agent operations, the repository provides a specialized headless web stack coordinated by scripts/dev-headless-web.ts that eliminates the desktop dependency while preserving full workspace functionality and authentication state.

Architecture of the Headless Web Stack

The headless implementation consists of three coordinated components that replace the traditional desktop shell:

  • Vite Dev Server: Serves the OpenWork web UI (@openwork/app) on a local port (default 5178), making the interface accessible via standard browsers instead of the Electron window
  • OpenWork Server: The core API and storage layer (apps/server/src/cli.ts) running on port 8778, handling workspace persistence, token management, and file operations
  • Launcher/Runtime Manifest: Orchestrates process management, port allocation, and authentication tokens through functions like buildHeadlessRuntimeManifest, writing operational state to tmp/dev-headless-web.json with permissions 0600 for credential security

Single-Instance Management and Workspace Persistence

The launcher enforces a single-instance-per-worktree policy through the readExistingManifest and probeStack functions. If a healthy instance already exists, the script reuses it and prints the existing URLs unless you specify the --replace flag.

Workspace data persists across restarts via mergeHeadlessServerConfig in scripts/dev-headless-web-lib.ts, which merges the isolated configuration at tmp/headless-server.json with user-created workspaces to prevent data loss during relaunch.

Starting the Headless Stack

Basic Execution

Run the headless web stack with the predefined script command:

pnpm dev:headless-web

This command spawns the Vite server and OpenWork server via buildHeadlessServerLaunch, then prints accessible URLs to the terminal. The launcher handles the initialization sequence and health checks automatically.

Detached Execution for Long-Running Processes

For automation scenarios where the stack must survive terminal closure, use the --detach flag:

pnpm dev:headless-web --detach

The script respawns itself using spawnLogged with detached: true, placing the process in its own process group. This ensures the headless stack continues running after the launching terminal exits, making it ideal for CI environments and background services.

Port and Token Configuration

The launcher automatically handles port conflicts through resolvePort and getFreePort, defaulting to 5178 for the web UI and 8778 for the API server. If these ports are busy, the script discovers free alternatives and updates the runtime manifest accordingly.

Control authentication token behavior during restarts using these flags:


# Force fresh tokens and new ports (useful for complete reset)

pnpm dev:headless-web --replace

# Restart server but preserve existing tokens (keeps browser tabs authenticated)

pnpm dev:headless-web --replace --keep-tokens

Token management occurs through resolveHeadlessTokens in scripts/dev-headless-web-lib.ts, ensuring authentication survives crash-restarts while allowing explicit token refresh when required.

Understanding the Runtime Manifest

The launcher writes critical operational metadata to tmp/dev-headless-web.json, including:

  • URLs for the web interface and API endpoints
  • Authentication tokens for seamless browser access
  • Process IDs for stack management and health checks

Access the manifest programmatically to extract connection details:

cat tmp/dev-headless-web.json

Programmatic Integration with Headless Threads

For agent-driven automation without browser interaction, use the @openwork/headless-threads client library to interact directly with the server:

import { createHeadlessThreadClient } from "@openwork/headless-threads";

const client = createHeadlessThreadClient({
  baseUrl: "http://127.0.0.1:8778",
  workspaceId: "ws_1",
  token: process.env.OPENWORK_TOKEN,
  defaultModel: { providerId: "anthropic", modelId: "claude-sonnet-5" },
});

const thread = await client.createThread({
  title: "Sample thread",
  prompt: "Explain the benefits of headless mode.",
});
await client.waitForThread(thread.id, { timeoutMs: 60_000 });

This client abstracts the HTTP sequence required to interact with apps/server/src/cli.ts, enabling direct API integration in scripts, test suites, and CI jobs without rendering the UI.

Optional Den Proxy Configuration

When integrating with Den authentication services in headless environments, enable the optional proxy to avoid CORS issues and maintain seamless sign-in flows:

OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY=1 pnpm dev:headless-web

The script handles denTarget and denApiUrl routing in scripts/dev-headless-web.ts, proxying /api/den requests to the real Den target. Disable this proxy when running isolated automation that doesn't require Den integration:

OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY=0 pnpm dev:headless-web

Summary

  • Execute pnpm dev:headless-web to run OpenWork in headless mode, replacing the Electron shell with a browser-accessible Vite server
  • Use --detach to run the stack as a background process independent of the launching terminal via spawnLogged with detached: true
  • Workspace data persists across restarts through mergeHeadlessServerConfig in scripts/dev-headless-web-lib.ts
  • The runtime manifest at tmp/dev-headless-web.json provides URLs, tokens, and process IDs for external tooling integration
  • Control authentication token lifecycle with --replace and --keep-tokens flags managed by resolveHeadlessTokens
  • Integrate programmatically using createHeadlessThreadClient from the @openwork/headless-threads package

Frequently Asked Questions

How do I keep OpenWork running in headless mode after closing the terminal?

Use the --detach flag when launching. The script calls spawnLogged with detached: true, creating a new process group that survives terminal closure. The stack continues running in the background and writes its state to tmp/dev-headless-web.json for reconnection by other processes or subsequent health checks.

Will my workspaces persist when I restart the headless server?

Yes. The launcher uses mergeHeadlessServerConfig to combine the isolated server configuration with existing workspace data from previous sessions. Unless you manually delete the workspace storage files, your data persists across --replace restarts. Use --keep-tokens to maintain authentication state for existing browser tabs.

How does the headless stack handle port conflicts?

The launcher automatically detects busy ports using getFreePort and resolvePort. If the default ports 5178 (web) or 8778 (server) are occupied, it allocates available alternatives and updates the runtime manifest accordingly. The single-instance-per-worktree check in readExistingManifest prevents accidental duplicate launches in the same directory.

Can I disable the Den proxy if I don't need authentication?

Yes. Set the environment variable OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY=0 before launching. This disables the /api/den proxy routing defined in scripts/dev-headless-web.ts, which is useful when running isolated automation that doesn't require Den integration or when avoiding external authentication flows entirely.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →