CI runnersAgent machinesone microVM fleet
Cut CI cost.Give every agent its own machine.
Bowse puts your GitHub Actions jobs and your AI agents on one fleet of isolated Linux microVMs. A CI job borrows a fresh machine and hands it back. An agent gets one to keep — shell, browser, desktop, and a disk that remembers.
Prefer reading first? Open the docs — quickstart, CI guide, SDK, and MCP.

[ci] Swap one label — each job lands on a fresh microVM
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[agent] Boot a machine — create, exec, browse
- 01createqueued
bowse machine create ada --size 4x16 --keep-diskada is running · 4 vCPU · 16 GB · disk kept on stop
- 02execqueued
bowse machine exec ada -- pnpm test✓ 214 passed · exit 0
- 03browserqueued
chromium.connect_over_cdp(ada.browser().url)CDP attached · 1 page open
Agent machines
Hand the agent a whole computer, not a code cell.
Function sandboxes are fine for a quick eval. Real agent work — cloning a repo, signing into a dashboard, leaving a dev server up for review — needs a machine with a filesystem that’s still there tomorrow.

$ bowse machine create ada --size 2x8✓ ada is running · 2 vCPU · 8 GB$ bowse machine ssh adaagent@ada:~$ git clone https://github.com/northwind/web && cd webCloning into 'web'... done.agent@ada:~/web$ sudo apt-get install -y ripgrep✓ ripgrep installedagent@ada:~/web$ pnpm test✓ 96 passed · 3 skippedagent@ada:~/web$ 01 / 05 · Shell
A real shell, with sudo, that nobody else shares.
Attach from the console or your own terminal. Whatever the agent installs stays inside its machine — none of it lands on your laptop or your servers.
- Ubuntu userland, root inside the machine
- Docker runs inside, too
- Every command keeps its exit code
bowse machine ssh ada# the machine's Chromium, driven from your codecdp = ada.browser().urlb = pw.chromium.connect_over_cdp(cdp)page = b.contexts[0].pages[0]page.goto("https://vendor.example.com/billing")page.get_by_text("Oct 2026").click()02 / 05 · Browser
Chromium on board, reachable over CDP.
Point Playwright or Puppeteer at the machine’s browser endpoint and let the agent work through the parts of the web that never shipped an API.
- Cookies and logins live on the machine
- Headless or visible on the desktop
- Your code drives it; Bowse just hosts it
ada.browser().url # CDP endpoint03 / 05 · Desktop
Watch the screen. Take the mouse when it gets stuck.
A live view of the machine’s display, read-only by default. When the agent hits a CAPTCHA or a confusing form, a person can step in without copying state anywhere.
- Read-only or read-write sessions
- Same machine, same browser session
- Hand control back when done
Console → ada → Display$ bowse port publish ada 3000✓ https://ada-3000.on.bowse.dev → :300004 / 05 · Ports
Hand reviewers a link from inside the box.
Publish a port and get an HTTPS URL. Reviewers click the link the agent posts in the PR instead of pulling a branch to see the change.
- HTTPS by default
- A few ports per machine
- Unpublish with one command
bowse port publish ada 3000- runningcompute billed
- stoppedcompute off · disk kept
- runningsame disk, same files
- ~/web (git working tree)kept
- node_modules, apt packageskept
- Chromium profile + loginskept
- Running processesrestart
05 / 05 · Persist
Stop the meter. Keep the disk.
Stopping a machine stops compute charges. Start it again and the repo, the packages, and the browser profile are where the agent left them — no re-clone, no re-login.
- Root disk survives stop/start
- Optional persistent home directory
- Processes restart; files don’t vanish
bowse machine stop ada && bowse machine start adaFour handles, one machine
Boot it from Python, TypeScript, the CLI, or an MCP client.
Your orchestration code gets typed SDKs. Agents that speak MCP can create, run, and stop machines on their own — with only the scopes you grant.
Everything a machine can do# pip install bowse
from bowse import Machine
ada = Machine.create(name="ada", size="2x8", keep_disk=True)
ada.run("git clone https://github.com/northwind/web")
result = ada.run("cd web && pnpm test")
print(result.exit_code, result.stdout[-200:])
ada.stop() # compute stops; the disk stays// npm i @bowse/sdk
import { Machine } from "@bowse/sdk";
const ada = await Machine.create({ name: "ada", size: "2x8", keepDisk: true });
await ada.run("git clone https://github.com/northwind/web");
const result = await ada.run("cd web && pnpm test");
console.log(result.exitCode, result.stdout.slice(-200));
await ada.stop(); // compute stops; the disk staysbowse login
bowse machine create ada --size 2x8 --keep-disk
bowse machine exec ada -- git --version
bowse machine ssh ada
bowse port publish ada 3000
bowse machine stop ada# works with MCP hosts that speak streamable HTTP
claude mcp add --transport http bowse https://bowse.dev/mcp
# then ask your agent:
# "Boot a Bowse machine called ada, clone northwind/web,
# run the tests there, and publish port 3000 if they pass."pip install bowse · npm i @bowse/sdk · MCP over streamable HTTPCI runners
Keep the workflow file. Change who runs it.
Bowse acts as a self-hosted runner pool for the repositories you choose. Jobs that ask for runs-on: bowse land on a clean microVM; every other job keeps running wherever it runs today.
- 01
Connect the GitHub App
Pick the repositories Bowse may see. Nothing runs on Bowse until a job asks for it.
- 02
Swap one label
Change
ubuntu-latesttobowse, or a sized label such asbowse-4x16. - 03
Push like always
Checks, logs, annotations, and re-runs stay in GitHub. Bowse only supplies the machine.
- Docker Engine, Buildx, Compose
- actions/cache compatible
- BuildKit type=gha layers
- Ubuntu 24.04 · x64
- One job per machine
- lintbowse-1x2
- test (node 22)bowse-2x8
- test (node 24)bowse-2x8
- docker buildbowse-4x16
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=maxCost clarity
Check the math against your own invoice.
Savings headlines age badly. Type in what you pay per minute today and how much CI you run, and set it beside Bowse’s per-minute rate — no competitor price list required.
Isolation
Every workload boots its own kernel.
Tenants never share a container runtime. Each CI job and each agent machine is a KVM microVM with its own guest kernel, and access is scoped to the workspace that created it.
One microVM per CI job
A runner serves exactly one job. When it finishes, the machine and its disk are destroyed.
Workspace-scoped keys
API keys and roles belong to a single workspace. MCP clients sign in with OAuth and ask for scopes.
Just-in-time runner tokens
Runner registrations are minted per job and are useless once that job is done.
Network off switch
Create a machine with outbound internet disabled when the code inside isn’t yours.
Bowse host
CI job
ci-job 48121
wiped on exit
Agent machine
ada
disk kept on stop
Agent machine
lin
outbound network off
Sizes
Six machine shapes, named by what’s inside.
Labels read as vCPU × GB of RAM, so a workflow file tells you what it runs on. CI jobs and agent machines share the same six shapes.
- Label
- bowse-2x8
- Root disk
- 48 GB
- Per minute
- $0.0012
- Per hour
- $0.072
- Good for
- Typical app CI; what plain runs-on: bowse gets
# GitHub Actions
runs-on: bowse-2x8
# Agent machine (Python SDK)
Machine.create(size="2x8")Start with one workflow and one agent.
Point a single job at runs-on: bowse and give one agent a machine of its own. Questions go straight to support@bowse.dev.