# How to Run OpenWork in Headless Web Mode Without Electron for CI/CD Environments

> Run OpenWork in headless web mode without Electron for CI/CD. Launch the UI with pnpm dev:headless-web --detach for automated pipelines.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-15

---

**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`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts), which orchestrates process spawning, health checks, and graceful shutdown handling.

## Quick Start for CI/CD Pipelines

### Basic Detached Launch

```bash

# 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

```bash
#!/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json) to check for healthy running stacks before deciding to reuse or replace them [L71-L90](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#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](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#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](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#L166-L187).

### 4. Secure Token Rotation

Generates fresh bearer tokens unless `--keep-tokens` is specified, preventing credential leakage between CI runs [L207-L226](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#L207-L226).

### 5. Isolated Server Configuration

**[`scripts/dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web-lib.ts)** merges workspace roots into [`tmp/headless-server.json`](https://github.com/different-ai/openwork/blob/main/tmp/headless-server.json) instead of polluting the global config, ensuring workspaces survive `--replace` restarts [L84-L130](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web-lib.ts#L84-L130).

### 6. Process Spawning

Launches the Vite dev server and `openwork-server` with arguments built in [`dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web-lib.ts) [L139-L162](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web-lib.ts#L139-L162).

### 7. Runtime Manifest Creation

Writes [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json) with mode `0600` (private permissions), containing:
- `url`: Vite UI endpoint
- `apiUrl`: `openwork-server` endpoint
- `tokens`: Bearer credentials
- `pids`: Process IDs for cleanup
- Optional Den proxy settings

[L250-L272](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#L250-L272)

### 8. Graceful Shutdown

Installs `SIGINT` and `SIGTERM` handlers to terminate child processes cleanly with logged exit reasons [L311-L334](https://github.com/different-ai/openwork/blob/dev/scripts/dev-headless-web.ts#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

```yaml
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](https://github.com/different-ai/openwork/blob/dev/README.md#headless-web-no-electron).

## Cleanup Strategies

### Manual Process Termination

```bash

# 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:

```yaml
- 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`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts) | Main launcher logic, signal handling, manifest I/O |
| [`scripts/dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web-lib.ts) | Token management, server arguments, config merging |
| [`package.json`](https://github.com/different-ai/openwork/blob/main/package.json) | npm script definition (`dev:headless-web`) |
| [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json) | Runtime manifest (auto-generated) |
| [`tmp/headless-server.json`](https://github.com/different-ai/openwork/blob/main/tmp/headless-server.json) | Isolated server config (auto-generated) |

## Summary

- **`pnpm dev:headless-web --detach`** starts OpenWork without Electron in CI-friendly detached mode.
- The launcher in **[`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts)** handles health checks, port selection, token rotation, and graceful shutdown automatically.
- Runtime state is stored in **[`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json)** with restricted permissions for secure credential access.
- Environment variables **`OPENWORK_WEB_PORT`** and **`OPENWORK_PORT`** customize 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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.