How to Run OpenWork in Headless Web Mode Without Electron for CI/CD Environments
Use pnpm dev:headless-web --detach to launch OpenWork's UI in a regular browser with a local openwork-server backend, bypassing Electron entirely for automated CI/CD pipelines.
Running OpenWork in headless web mode eliminates the Electron desktop dependency, making it ideal for continuous integration environments where graphical displays are unavailable. This guide explains the complete setup based on the implementation in the different-ai/openwork repository.
What Is Headless Web Mode?
Headless web mode runs OpenWork's Vite-powered UI in a standard browser while a local openwork-server process provides the backend API. According to the OpenWork source code, this architecture avoids Electron's Chromium shell entirely—critical for Docker containers, GitHub Actions, and other headless CI runners.
The mode is controlled by scripts/dev-headless-web.ts, which orchestrates process spawning, health checks, and graceful shutdown handling.
Quick Start for CI/CD Pipelines
Basic Detached Launch
# Install dependencies
pnpm install
# Start in detached mode (survives the spawning shell)
pnpm dev:headless-web --detach
The launcher outputs URLs like:
[dev:headless-web] Web URL: http://127.0.0.1:5178
[dev:headless-web] OpenWork server: http://127.0.0.1:8778
Store these URLs in environment variables for downstream test steps.
Complete CI Workflow Example
#!/bin/bash
set -e
# 1. Dependencies
pnpm install
# 2. Launch headless web stack
pnpm dev:headless-web --detach
# 3. Extract endpoints from runtime manifest
WEB_URL=$(cat tmp/dev-headless-web.json | jq -r .url)
API_URL=$(cat tmp/dev-headless-web.json | jq -r .apiUrl)
# 4. Run tests against the live UI
pnpm test:e2e --base-url="$WEB_URL"
How the Launcher Works Internally
The scripts/dev-headless-web.ts script performs eight key operations when you invoke pnpm dev:headless-web:
1. Detect Existing Instances
Reads tmp/dev-headless-web.json to check for healthy running stacks before deciding to reuse or replace them L71-L90.
2. Optional Detachment
With --detach, respawns itself in a new process group so the stack outlives the CI agent's job step L113-L139.
3. Dynamic Port Selection
Resolves free ports using OPENWORK_WEB_PORT (default 5178) and OPENWORK_PORT (default 8778), falling back to available ports if defaults are occupied L166-L187.
4. Secure Token Rotation
Generates fresh bearer tokens unless --keep-tokens is specified, preventing credential leakage between CI runs L207-L226.
5. Isolated Server Configuration
scripts/dev-headless-web-lib.ts merges workspace roots into tmp/headless-server.json instead of polluting the global config, ensuring workspaces survive --replace restarts L84-L130.
6. Process Spawning
Launches the Vite dev server and openwork-server with arguments built in dev-headless-web-lib.ts L139-L162.
7. Runtime Manifest Creation
Writes tmp/dev-headless-web.json with mode 0600 (private permissions), containing:
url: Vite UI endpointapiUrl:openwork-serverendpointtokens: Bearer credentialspids: Process IDs for cleanup- Optional Den proxy settings
8. Graceful Shutdown
Installs SIGINT and SIGTERM handlers to terminate child processes cleanly with logged exit reasons L311-L334.
Environment Variable Configuration
Override defaults in your CI configuration:
| Variable | Purpose | Default |
|---|---|---|
OPENWORK_WEB_PORT |
Vite dev server port | 5178 |
OPENWORK_PORT |
openwork-server port |
8778 |
OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY |
Enable Den Cloud proxy | 1 |
OPENWORK_DEV_MODE |
Set automatically by npm script | 1 |
GitHub Actions Example
name: OpenWork E2E Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- name: Start OpenWork headless web
run: pnpm dev:headless-web --detach
env:
OPENWORK_WEB_PORT: 5179
OPENWORK_PORT: 8780
OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY: "0"
- name: Run E2E tests
run: pnpm test:e2e
Authentication in Headless Environments
The Den Cloud proxy enables same-origin authentication flows when browser redirects aren't feasible. Set OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY=1 (default) to use copy-paste handoff instead of OAuth redirects—essential for containerized CI runners where callback URLs cannot reach the browser README.md – Headless web section.
Cleanup Strategies
Manual Process Termination
# Kill the detached launcher and its children
kill $(cat tmp/dev-headless-web.json | jq .pids.launcher)
Automatic CI Cleanup
Most CI runners terminate all processes when the job completes. For persistent runners, add a teardown step:
- name: Stop OpenWork
if: always()
run: |
[ -f tmp/dev-headless-web.json ] && \
kill $(jq .pids.launcher tmp/dev-headless-web.json) || true
Key Source Files Reference
| File | Role |
|---|---|
scripts/dev-headless-web.ts |
Main launcher logic, signal handling, manifest I/O |
scripts/dev-headless-web-lib.ts |
Token management, server arguments, config merging |
package.json |
npm script definition (dev:headless-web) |
tmp/dev-headless-web.json |
Runtime manifest (auto-generated) |
tmp/headless-server.json |
Isolated server config (auto-generated) |
Summary
pnpm dev:headless-web --detachstarts OpenWork without Electron in CI-friendly detached mode.- The launcher in
scripts/dev-headless-web.tshandles health checks, port selection, token rotation, and graceful shutdown automatically. - Runtime state is stored in
tmp/dev-headless-web.jsonwith restricted permissions for secure credential access. - Environment variables
OPENWORK_WEB_PORTandOPENWORK_PORTcustomize network binding. - The Den proxy enables authentication flows in containerized environments where standard OAuth redirects fail.
Frequently Asked Questions
What command starts OpenWork without the Electron desktop window?
Run pnpm dev:headless-web to launch the UI in a regular browser with a local openwork-server backend. Add --detach for CI environments where the process must survive the spawning shell.
How do I access the URL and API endpoints in a CI script?
The launcher writes tmp/dev-headless-web.json containing url, apiUrl, and tokens. Use jq to extract values: cat tmp/dev-headless-web.json | jq -r .url.
Can I reuse an existing OpenWork instance between test runs?
Yes—the launcher detects healthy instances via tmp/dev-headless-web.json and reuses them unless you pass --replace. Use --keep-tokens to preserve authentication across restarts.
What if the default ports 5178 and 8778 are already occupied?
Set OPENWORK_WEB_PORT and OPENWORK_PORT environment variables, or let the launcher automatically find free ports and report the actual bindings in the runtime manifest.
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 →