Get started

Overview

Bowse runs two kinds of work on one fleet of KVM microVMs. A CI runner borrows a fresh machine for a single GitHub Actions job and hands it back. An agent machine is one you keep: it has a name, a disk that survives stops, a shell, a browser, and a desktop.

CI runnerAgent machine
Started byA job with runs-on: bowseSDK, CLI, console, or MCP
LifetimeOne job, then destroyedUntil you delete it
DiskWiped; use caches and artifactsKept across stop and start
MeteredPer minutePer hour, prorated
AccessLogs in GitHubShell, CDP browser, desktop, public URLs

Get started

Machine quickstart

Boot a machine, run a command in it, and park it — about five minutes.

  1. 1

    Install

    The Python package ships the SDK and the bowse CLI. TypeScript projects add @bowse/sdk.

    pip install bowse
    # installs the Python SDK and the bowse CLI
  2. 2

    Sign in

    Use bowse login on your laptop, or an API key on servers and in CI. See Authentication.

    terminal
    # Option 1 — interactive: opens a browser, saves a session for the CLI and SDKs
    bowse login
    
    # Option 2 — API key from Console → Settings → API keys
    export BOWSE_API_KEY="bws_live_…"
  3. 3

    Create a machine and run something

    run() waits for the command and returns its exit code and output. The disk keeps the clone for next time.

    from bowse import Machine
    
    ada = Machine.create(name="ada", size="2x8", template="coding-agent")
    
    result = ada.run("git clone https://github.com/northwind/web && cd web && npm ci && npm test", timeout=600)
    print(result.exit_code)
    print(result.stdout[-500:])
    
    ada.stop()    # compute stops; the disk stays
    # later: Machine.get("ada").start()
  4. 4

    Stop it, or delete it

    A stopped machine isn’t charged for compute and keeps its files and installed packages. delete() removes the machine and its disk.

Get started

Authentication

The SDKs and CLI look for credentials in this order:

  1. An api_key / apiKey passed to the client.
  2. The BOWSE_API_KEY environment variable.
  3. The session saved by bowse login.

Keys belong to one workspace and carry a role — viewer can list and read, member can create and run machines, admin can also manage keys, billing, and the GitHub installation. Rotate or revoke keys in Console → Settings → API keys; revocation takes effect on the next call.

Get started

Core concepts

Workspace
Owns machines, CI installations, caches, API keys, members, and the spend limit. Nothing is shared across workspaces.
Machine
A KVM microVM with its own kernel, root disk, and name. CI creates one per job; agents keep theirs across sessions.
Template
The image a machine boots from: ubuntu, coding-agent, browser, or one you build.
Size
vCPU × GB RAM, from 1x2 to 16x64. Root disk is fixed per size.
Volume
Extra storage attached to a machine for datasets, models, or caches that should outlive it.
Published port
A port inside a machine exposed at an HTTPS URL under on.bowse.dev.

A machine moves through these states:

creating→running→stopping→stopped→starting→running·deleted

CI runners

CI quickstart

Bowse acts as a self-hosted runner pool for the repositories you pick. Jobs that ask for a Bowse label run on a clean microVM; every other job is untouched.

  1. 1

    Install the GitHub App

    In the console, open CI → Connect GitHub and choose Only select repositories. The app asks only for what runners need — the full list is on the security page.

  2. 2

    Change one label

    Edit runs-on on the job you want to move. Pick a size to see the label change.

    ~/.github/workflows/test.ymlOne-line change
    name: teston: [push, pull_request]jobs:  test:    runs-on: ubuntu-latest    runs-on: bowse    timeout-minutes: 20    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with: { node-version: 22, cache: npm }      - run: npm ci && npm test
    Runner label
    2 vCPU · 8 GB · $0.0012/min
  3. 3

    Push

    The job appears in the Actions tab as usual, on a runner named after its size. Logs, annotations, re-runs, and required checks all stay in GitHub.

CI runners

Labels and sizes

Labels read bowse-<vCPU>x<GB RAM>. Plain bowse means bowse-2x8. Put the label on the job that needs it, not the whole workflow.

Bowse runner labels and what each size suits
Size / labelvCPURAMRoot diskGood for
1x2bowse-1x212 GB16 GBLint, format checks, small scripts
2x4bowse-2x424 GB32 GBUnit tests, light builds
2x8defaultbowse-2x828 GB48 GBTypical app CI; what plain runs-on: bowse gets
4x16bowse-4x16416 GB96 GBDocker builds, browser-driving agents
8x32bowse-8x32832 GB160 GBMonorepos, parallel test shards
16x64bowse-16x641664 GB256 GBHeavy compiles, large integration suites
.github/workflows/ci.yml
jobs:
  lint:
    runs-on: bowse-1x2        # small and cheap
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run lint

  test:
    runs-on: bowse            # same as bowse-2x8
    strategy:
      matrix: { shard: [1, 2, 3, 4] }
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx vitest --shard=${{ matrix.shard }}/4

  image:
    runs-on: bowse-4x16       # Docker build gets more CPU
    steps:
      - uses: actions/checkout@v4
      - uses: docker/build-push-action@v6

Not sure which size? Run on the default for a week, then check peak CPU and memory per job in Console → CI. Most lint jobs fit on 1x2; Docker builds and big test shards benefit from 4x16 and up. Prices per size are on the pricing page.

CI runners

Runner image

Bowse ships its own lean image rather than GitHub’s hosted one. If a step assumes a tool that isn’t listed, install it in the step or use the matching setup-* action.

OSUbuntu 24.04 LTS, x64, fresh for every job
RunnerGitHub Actions runner, registered just in time for one job
Source controlgit, git-lfs, GitHub CLI
ContainersDocker Engine, Buildx, Compose plugin
Build toolsbuild-essential, make, cmake, pkg-config
LanguagesPython 3, Node.js LTS — pin other versions with setup-* actions
Utilitiescurl, wget, jq, unzip, zip, zstd, rsync, openssl

CI runners

Caching

Machines start empty; caches don’t. Bowse answers the cache API your actions already call, so these work without edits:

  • actions/cache and actions/cache/restore / save
  • The cache: input on setup-node, setup-python, setup-go, and setup-java
  • BuildKit layers through cache-from: type=gha / cache-to: type=gha,mode=max
.github/workflows/test.yml
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: npm          # no change needed
  - run: npm ci && npm test
  - uses: docker/setup-buildx-action@v3
  - uses: docker/build-push-action@v6
    with:
      cache-from: type=gha
      cache-to: type=gha,mode=max

Scoping follows GitHub’s rules: a branch reads its own entries and those saved on the default branch. Caches are isolated per workspace and repository, and entries nobody restores for a while expire on their own.

CI runners

Docker and services

Every runner has Docker Engine with Buildx and Compose, and the job owns the whole daemon — no shared socket with other jobs. GitHub service containers work as written.

.github/workflows/integration.yml
jobs:
  integration:
    runs-on: bowse-4x16
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: postgres }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-retries 10
    steps:
      - uses: actions/checkout@v4
      - run: docker compose -f compose.test.yml up -d redis
      - run: npm ci && npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres

CI runners

Troubleshooting

When a runner can’t start, Bowse posts a Bowse setup check on the commit explaining why. Fix the cause and re-run the job from GitHub.

Check saysWhyFix
Repository not connectedThe Bowse GitHub App isn’t installed on this repository.Add the repository in the app’s GitHub settings, then re-run the job.
No workspace linkedThe GitHub installation isn’t attached to a Bowse workspace.Console → CI → Link installation.
Spend limit reachedThe workspace hit its spend cap.Raise the limit in Console → Billing, then re-run.
Unknown runner labelThe job asked for a label that isn’t a Bowse size, such as bowse-3x12.Use a label from the size table.
Concurrency limitMore jobs are queued than the workspace may run at once.Nothing to fix — jobs start as others finish. Ask support to raise the limit.

For the runner’s own setup log, run bowse ci logs <job-id>.

Agent machines

Run commands

Agents are root inside their own machine, so apt, pip, npm, and Docker behave as they would on a server you own. Use run() for a result, run_stream() when the agent should react to output as it happens.

ada = Machine.get("ada")

# One-shot: waits for the command, returns output and exit code
r = ada.run("make check", timeout=900, cwd="/home/agent/web", env={"CI": "1"})
if r.exit_code != 0:
    print(r.stderr)

# Streaming: react to output while a long build runs
for line in ada.run_stream("pnpm build"):
    print(line.stream, line.text)

Every command is recorded with its exit code in Console → machine → History.

Agent machines

Browser and desktop

Machines created from the browser template run Chromium on a desktop session. browser() returns a CDP endpoint for Playwright or Puppeteer; display() returns a live view of the screen, read-only unless you pass control=True.

browser_agent.py
from bowse import Machine
from playwright.sync_api import sync_playwright

ada = Machine.create(name="ada-web", template="browser")
cdp_url = ada.browser().url            # Chromium inside the machine

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(cdp_url)
    page = browser.contexts[0].pages[0]
    page.goto("https://github.com/northwind/web/pull/42")
    print(page.title())

view = ada.display(control=False)      # read-only live view of the desktop
print(view.url)                        # open it, or embed it in your own UI

Agent machines

Public URLs

Publish a port and Bowse gives it an HTTPS URL with a certificate. Use it to hand a reviewer a running dev server, a notebook, or an API the agent just built. Unpublish when you’re done; stopping the machine takes the URL offline.

terminal
$ bowse port publish ada 3000
✓ https://ada-3000.on.bowse.dev → :3000

$ bowse port list ada
PORT  URL                              SINCE
3000  https://ada-3000.on.bowse.dev    2m

$ bowse port unpublish ada 3000

Agent machines

Lifecycle and persistence

Survives a stop

Root disk, installed packages, the persistent home directory, attached volumes, and the browser profile.

Doesn’t

Running processes and memory. Bring services back up after start() — a startup script in the template is the easy way.

Find a machine again by name with Machine.get("ada"). Stopped machines keep their disk until you delete them.

Agent machines

Templates

Three base templates cover most agents: ubuntu (bare), coding-agent (Git, Python, Node.js, build tools), and browser (adds Chromium and a desktop). Build your own when every machine needs the same setup.

terminal
# Start from a base, bake in your toolchain, reuse it for every agent
bowse template create web \
  --from coding-agent \
  --run "corepack enable && pnpm i -g turbo" \
  --run "git clone https://github.com/northwind/web /home/agent/web"

bowse machine create ada --template web --size 4x16

Agent machines

Network policy

Create a machine with network="off" to block all outbound traffic. Exec, files, and the shell keep working because they use Bowse’s control channel, not the machine’s network.

untrusted.py
# Code you didn't write: no outbound traffic at all
ada = Machine.create(name="sandbox", template="ubuntu", network="off")
ada.run("curl -sS https://example.com")   # fails: network is off
ada.run("python3 untrusted.py")           # exec still works over the control channel

SDKs and tools

Python and TypeScript

Both SDKs expose the same calls. Python is synchronous with an AsyncMachine twin; TypeScript returns promises.

PythonTypeScriptDoes
Machine.create(name, size, template, network, volumes)Machine.create({ … })Boot a new machine
Machine.get(name)Machine.get(name)Look up an existing machine
Machine.list()Machine.list()Every machine in the workspace
m.run(cmd, timeout, cwd, env)m.run(cmd, { … })Run and wait; returns exit code, stdout, stderr
m.run_stream(cmd)m.runStream(cmd)Iterate output lines as they arrive
m.files.write(path, data) · m.files.read(path)m.files.write · m.files.readMove files in and out
m.ports.publish(port)m.ports.publish(port)Expose a port at an HTTPS URL
m.browser() · m.display()m.browser() · m.display()CDP endpoint · live desktop URL
m.stop() · m.start() · m.delete()m.stop() · m.start() · m.delete()Lifecycle

SDKs and tools

CLI reference

Every command accepts --json for scripts and --workspace when you belong to more than one.

bowse loginSign in through the browser and save a session
bowse machine create <name> --size 2x8 --template coding-agentBoot a machine
bowse machine listList machines and their state
bowse machine exec <name> -- <cmd>Run one command and print its output
bowse machine ssh <name>Interactive shell over an authenticated HTTPS connection
bowse machine stop | start | delete <name>Lifecycle
bowse port publish | list | unpublish <name> <port>Manage public URLs
bowse template create <name> --from <base> --run <cmd>Build a custom template
bowse ci jobs [--repo owner/name]Recent CI jobs with size, duration, and result
bowse ci logs <job-id>Runner setup log for one job

SDKs and tools

MCP server

The hosted MCP server lets an assistant create machines, run commands, and publish ports on its own. It speaks streamable HTTP at https://bowse.dev/mcp and signs clients in with OAuth 2.1 and PKCE. A client only gets the scopes you approve — machines:read, machines:exec, machines:admin — in one workspace.

Claude Code
claude mcp add --transport http bowse https://bowse.dev/mcp
mcp.json · other MCP hosts
{
  "mcpServers": {
    "bowse": { "url": "https://bowse.dev/mcp" }
  }
}
ToolWhat it does
machine_createBoot a machine. Pass an idempotency_key so retries don’t create duplicates.
machine_list · machine_infoFind machines and check their state and size.
machine_execRun a shell command; returns stdout, stderr, and the exit code.
machine_start · machine_stopResume or park a machine. Stopping keeps the disk.
machine_deleteRemove a machine and its disk for good.
port_publish · port_list · port_unpublishManage the machine’s public URLs.

A good default flow for an agent: create with an idempotency key → poll machine_info until running → exec → publish a port if there’s something to show → stop when the task ends.

SDKs and tools

Errors

Both SDKs raise typed errors with the same names. Each carries request_id — include it when you write to support.

ErrorHTTPMeaning
AuthError401Missing or revoked key, or an expired CLI session. Run bowse login again.
ScopeError403The key or MCP client lacks the scope for this call, e.g. machines:exec.
NotFoundError404No machine, template, or port with that name in this workspace.
ConflictError409A machine with that name already exists, or it’s mid-transition.
LimitError402 / 429Spend or concurrency limit reached; e.limit says which.
RateLimitError429Too many API calls; wait e.retry_after seconds.
ServerError5xxSomething failed on our side. Safe to retry with the same idempotency key.
errors.py
from bowse import Machine
from bowse.errors import AuthError, NotFoundError, LimitError, RateLimitError

try:
    ada = Machine.get("ada")
except NotFoundError:
    ada = Machine.create(name="ada", size="2x8")
except LimitError as e:
    print("workspace limit:", e.limit)       # spend or concurrency
except RateLimitError as e:
    time.sleep(e.retry_after)

Account

Billing and limits

  • CI is metered per minute from boot to cleanup. Queue time isn’t billed.
  • Machines are metered per hour of runtime, prorated. Stopped machines aren’t charged for compute.
  • Both draw from one workspace balance, with no seats, platform fee, or minimum.
  • Spend limit: once reached, new machines don’t start, running machines stop with their disks kept, and CI jobs get a setup check.
  • Concurrency: each workspace has a limit shown in the console; ask support to raise it.

Rates for every size and an estimator are on the pricing page.

Account

Security model

Every CI job and every agent machine is its own KVM microVM with its own guest kernel. CI runners register with a just-in-time token that accepts exactly one job, and the machine and its disk are destroyed afterwards. Keys, caches, machines, and the GitHub installation each belong to one workspace. The security page covers GitHub App permissions, credentials, and data lifecycle in detail.

Account

Use cases

Coding agents

Clone the repo, install dependencies, run the suite, and publish the dev server so a reviewer can click through the change. Keep the machine between tasks so the next run starts warm.

Browser agents

Use the browser template, connect Playwright over CDP, and let a person watch the desktop — or take the mouse — when the agent hits a login wall.

Long-running agents

Jobs that run for hours write progress to disk. Stop the machine overnight and start it again tomorrow without losing the working tree.

Stuck on something?

Email support@bowse.dev with the request ID from the error. Agents can read the plain-text index at /llms.txt.