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 totmp/dev-headless-web.jsonwith permissions0600for 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-webto run OpenWork in headless mode, replacing the Electron shell with a browser-accessible Vite server - Use
--detachto run the stack as a background process independent of the launching terminal viaspawnLoggedwithdetached: true - Workspace data persists across restarts through
mergeHeadlessServerConfiginscripts/dev-headless-web-lib.ts - The runtime manifest at
tmp/dev-headless-web.jsonprovides URLs, tokens, and process IDs for external tooling integration - Control authentication token lifecycle with
--replaceand--keep-tokensflags managed byresolveHeadlessTokens - Integrate programmatically using
createHeadlessThreadClientfrom the@openwork/headless-threadspackage
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →