Breaking change (v0.2.0): TeamWork is now a pure display shell — all built-in AI agent logic (PM orchestration, coaching, persona generation, response generation) has been removed. TeamWork no longer calls any LLM APIs directly. Instead, an external agent (like Prax) provides the intelligence via TeamWork's REST API. If you were using the previous self-contained version with built-in agents, it is preserved at v0.1.0.
An open-source, agent-agnostic collaboration shell — a Slack-like web UI for AI agent teams. TeamWork provides the body (chat, channels, task board, file browser, embedded terminal, live browser screencast) while you bring the brains (your own agent framework).
Think of TeamWork as a dumb terminal: it displays messages, tracks tasks, and renders files — but it doesn't decide what to say or do. Your agent talks to TeamWork through a simple REST + WebSocket API, just like a human would use Slack.
Chat with your agent on the left while watching it browse the web in real time on the right. The embedded browser streams a live screencast from a headless Chrome instance running in the agent's sandbox — you see exactly what the agent sees, and can take over with mouse and keyboard at any time.
API Keys & Costs — TeamWork itself requires no AI API keys. However, the external agent you connect (e.g. Prax) will consume API credits. Monitor your usage dashboards and set spending limits.
- Features
- Embedded Terminal & Browser
- Quick Start
- Installation
- Connecting Your Agent
- Integration Guide
- Prax — First-Class Agent
- Architecture
- Workspace Structure
- Workspace Backup
- Database Management
- Full-Text Search (FTS5 + BM25)
- SQLite in Production
- Screenshots
- Environment Variables
- Development
- API Reference
- License
- Real-time chat — Channels and direct messages with WebSocket updates
- Channel mirroring — Dedicated #discord and #sms channels that mirror cross-channel conversations, so you can follow everything from one place
- Kanban task board — Drag-and-drop task management with status tracking
- File browser — View and edit workspace files in-browser
- Embedded terminal — Full PTY shell into the agent's sandbox container, right in the browser. Watch your agent run commands, or take over and type yourself. Powered by xterm.js with Docker exec under the hood
- Live browser screencast — Stream a real-time view of the headless Chrome running in the sandbox. See exactly what your agent sees as it browses, scrapes, or interacts with web apps. Click "Take Over" to control the browser with your own mouse and keyboard — then hand it back
- Execution graph visualization — Real-time tree view of agent delegation chains. See which spokes are running, tool call counts, timing, and status. Click any node to inspect live output
- Live agent output — Terminal-style real-time execution stream for each agent. Select any agent to watch its work; working agents highlighted and sorted to top. Full-width layout for maximum visibility
- Agent roster — Display agent names, roles, avatars, and online status
- External Agent API — REST endpoints for any agent to send messages, update tasks, manage files, push live output, and ensure channels
- Per-agent identity & governance — each agent gets its own credential (identity derived from the token, never from the request body), its own capability set, and optional Ed25519 request signing. Destructive actions can be put behind an approval gate: the agent proposes, a human decides, and the approval is bound to that exact action and single-use
- Append-only audit log — one ordered, hash-chained record of what happened across every agent. Editing, deleting or reordering an entry breaks verification, so the history can't be quietly rewritten
- Agent-first CLI —
teamwork-agent <command> '<json>': JSON in, JSON out, named errors and non-zero exits, so an agent can drive TeamWork as a tool without an HTTP integration - Installable —
uv pip installfrom GitHub; bundles the React frontend as static files - Single container — One Docker image serves both API and frontend (no nginx needed)
- Zero AI dependencies — No LLM API keys, no anthropic/openai packages
The highlights above are a subset — TeamWork has grown well past a "display shell." This is the fuller picture. Everything Prax-specific (memory, scheduling, plugins, model control) is surfaced as a proxy/display over the Prax backend, not reimplemented here.
Chat & messaging
- Channels, DMs, threads, reactions, typing indicators (WebSocket)
- Cross-channel mirror channels (
#discord,#sms) - FTS5 + BM25 message search, surfaced through a
Cmd+Kcommand palette
Tasks, workspace & files
- Drag-and-drop Kanban (status / assignee / subtasks / blockers + execution logs)
- In-browser file tree, read/write editor, git-log viewer, per-task diffs
- File uploads (extension allowlist + 10 MB cap) and one-click workspace backup zip
Embedded sessions
- PTY terminal into the sandbox container (
docker execover WebSocket, xterm.js) - Live headless-Chrome screencast (CDP proxy) with take-over mouse/keyboard
- Full Linux desktop tab via noVNC (HTTP + websockify + clipboard reverse-proxy)
Agent introspection (proxied from Prax)
- Real-time execution-graph / delegation-tree visualization + live output + per-trace inspect
- Observability panel — Grafana dashboards, Tempo trace links, health events
- Memory review panel — STM/LTM CRUD, knowledge-graph view, stats
- Scheduler panel — cron schedules, one-time reminders, timezone
- In-chat model picker (auto + per-tier override) + context-window stats / manual compaction
- Plugin management proxy
Knowledge & content
- Library / Spaces — notebooks, notes, wiki, flashcards, tags, backlinks, per-space tasks/files
- Public rendering for courses / notes / news at root URLs
Integration & platform
- External REST API (projects / agents / messages / tasks / channels / live-output) + webhook push
- Agent roster — status, avatars, profiles, activity logs, editable prompts
- Responsive mobile layout;
Cmd+Kpalette, keyboard shortcuts, dark mode, toasts, onboarding wizard - Single-container build (FastAPI + bundled React SPA, SQLite WAL) + DB maintenance (stats / cleanup / VACUUM / compactify)
- Surface the Prax MCP server (registered tools, connection status, per-tool call history) in a tab
- Eval / benchmark dashboard (pass rates, regression trends, per-suite drilldown) proxied from Prax
- Browser / OS push notifications via a service worker (new messages, task & schedule events)
- Multi-user (human) presence — who's viewing a project/channel, live cursors / read receipts
- Cross-surface global search (notes/library, files, tasks) beyond message FTS in the palette
- Notification preferences — per-channel mute + digest settings
Most agent UIs are chat-only — you talk to the agent but you can't see what it's doing. TeamWork fixes that.
Your agent runs a headless Chrome inside its sandbox container. TeamWork proxies the Chrome DevTools Protocol (CDP) and streams screenshots to the frontend over WebSocket. You get a real-time, low-latency view of whatever the agent is looking at — web pages, documentation, dashboards, anything.
Click "Take Over" in the top-right corner to seize control: your mouse clicks and keystrokes are relayed directly to the headless browser. When you're done, hand it back to the agent. This makes debugging, guiding, and collaborating with your agent seamless — you're not guessing what it did, you're watching it happen.
How it works: The sandbox container runs Chromium with --headless=new and exposes CDP on port 9222. A socat bridge forwards it to 0.0.0.0:9223 so TeamWork can reach it across the Docker network. TeamWork's /api/browser/ws/{project_id} endpoint captures screenshots via CDP and relays input events back.
TeamWork embeds a full terminal (xterm.js) that connects to a PTY inside the agent's sandbox container via docker exec. It's the same shell your agent uses — you see its files, its installed packages, its running processes.
Use it to:
- Watch the agent work — see commands execute in real time
- Debug issues — inspect files, check logs, run tests
- Take over — type commands yourself when the agent gets stuck
- Install tools — add packages or dependencies the agent needs
The terminal is not a toy — it's a full interactive shell with color support, tab completion, and scroll history. Multiple sessions can run simultaneously.
How it works: TeamWork creates a WebSocket-backed PTY session via docker exec -it <container> /bin/bash. The frontend renders it with xterm.js + the fit addon for responsive sizing.
git clone https://github.com/praxagent/teamwork.git
cd teamwork
cp .env.example .env
docker compose up -d
# UI: http://localhost:3000 — API: http://localhost:8000git clone https://github.com/praxagent/teamwork.git
cd teamwork
make install # uv pip install -e ".[dev]"
make build-frontend # builds React → src/teamwork/static/
make dev # uvicorn with hot reload on :8000- Python 3.11+
- Node.js 18+ + npm — required to build (or dev-serve) the React UI. Without it the API runs
but
/returns{"detail":"Not Found"}(no UI). Install:- macOS:
brew install node(or nodejs.org LTS installer) - Windows:
winget install OpenJS.NodeJS.LTS(orchoco install nodejs-lts, or the nodejs.org installer) - Linux (Debian/Ubuntu):
sudo apt install nodejs npm(install both — Ubuntu'snodejsomitsnpm); for newer Node use NodeSource or nvm - Any OS:
nvm install --lts
- macOS:
uv pip install git+https://github.com/praxagent/teamwork.gitgit clone https://github.com/praxagent/teamwork.git
cd teamwork
uv pip install -e ".[dev]"
# Build the React frontend into the package
cd frontend && npm ci && npx vite build && cd ..
# Run
teamworkcurl http://localhost:8000/health
# {"status":"healthy","connections":0}TeamWork exposes a REST API that any agent framework can call. Every request and response schema is documented in the interactive Swagger UI at:
http://localhost:8000/docs
Open it while the server is running — you can explore every endpoint, see required fields, and try requests directly from the browser.
A credential is required. The external API refuses requests (503) when none is configured — it does not fall back to accepting anyone.
Generate one (any high-entropy string; this is a shared secret, not a provider key):
python3 -c "import secrets; print(secrets.token_urlsafe(32))"Put the same value on both sides — TeamWork checks it, your agent sends it:
| Where | Variable |
|---|---|
TeamWork's .env |
EXTERNAL_API_KEY=<value> |
Your agent's config (Prax: its .env) |
TEAMWORK_API_KEY=<value> |
Pass it as the X-API-Key header on every request. If the two drift apart you get
401 Invalid API key; if neither is set you get 503.
For local development only, ALLOW_UNAUTHENTICATED_AGENTS=true restores the old
accept-anything behaviour. Never set it on a reachable deployment.
Full model: docs/security/agent-identity.md
covers all of it — credentials, capabilities, signed envelopes, the audit log and
approval gates — plus the rollout order and what each layer does not buy.
Per-agent credentials (recommended for multiple agents). A single shared key
authenticates a caller but carries no agent identity — any holder can act as,
and be audit-logged as, any agent. Point AGENT_CLIENTS_PATH at a JSON registry
to bind one token to one agent, with its own capability set:
[{"name": "research", "token_sha256": "…", "agent_id": "…",
"allow": ["message.post", "presence"]},
{"name": "ops", "token_sha256": "…", "agent_id": "…",
"allow": ["message.*", "task.write"]}]Identity is then derived from the token, so a request that claims a different
agent_id is rejected rather than believed. Optionally add a public_key per
entry to require Ed25519-signed requests (see src/teamwork/agent_signing.py).
The API is split into two groups:
External API (/api/external/...) — the primary interface for your agent:
| Area | What you can do |
|---|---|
| Projects | Create/list/update external-mode projects. Creating a project returns the project_id and a map of channel_ids you'll need for everything else. |
| Agents | Register agents (name, role, personality) and update their status (idle, working, offline). |
| Messages | Send messages to any channel as any agent. Requires channel_id + agent_id + content. Also send typing indicators. Forward external messages (Discord/SMS) to mirror channels. |
| Tasks | Create and update tasks on the Kanban board. Supports status (pending, in_progress, blocked, review, completed), assignment, priority, subtasks, and blockers. |
| Channels | Ensure channels exist (idempotent creation for mirror channels like #discord, #sms). |
| Live Output | Push real-time execution output for an agent, polled by the frontend every 1 second. |
Internal API (/api/...) — used by the frontend, also available to your agent:
| Area | What you can do |
|---|---|
| Channels | List channels in a project, create custom channels, manage DMs. |
| Messages | Read channel history (paginated), get threads, delete messages. |
| Tasks | Full CRUD with subtask trees, blocker dependencies, and execution logs. |
| Workspace | Browse the project file tree, read/write files, view git log, get diffs for completed tasks. |
| Browser | Check if Chrome CDP is reachable; stream a live browser screencast via WebSocket at /api/browser/ws/{project_id}. |
| Projects | Pause/resume/reset projects, update config. |
See
http://localhost:8000/docsfor the full list of 40+ endpoints with request/response schemas.
Connect to ws://localhost:8000/ws for real-time events. Subscribe to receive live updates:
{"action": "subscribe_project", "id": "<project-uuid>"}
{"action": "subscribe_channel", "id": "<channel-uuid>"}Events you'll receive: message_new, task_new, task_update, agent_status, agent_typing.
When creating a project, pass a webhook_url. TeamWork will POST to that URL whenever a user sends a message in the UI:
{
"type": "user_message",
"content": "Build a login page",
"channel_id": "ch-abc123",
"project_id": "proj-xyz",
"message_id": "msg-999"
}Your agent processes the message and responds via the messages endpoint. This is how Prax works — no polling needed.
Step-by-step walkthrough to wire up your own agent. All request/response schemas are in the Swagger docs — the examples below show the essential flow.
import httpx
TW = "http://localhost:8000"
HEADERS = {"X-API-Key": "your-key"}
# Create an external-mode project with a webhook for user messages
resp = httpx.post(f"{TW}/api/external/projects", headers=HEADERS, json={
"name": "My Agent Workspace",
"description": "Controlled by my custom agent",
"webhook_url": "http://my-agent:9000/webhook", # where user messages get forwarded
})
project = resp.json()
PROJECT_ID = project["project_id"]
# You get back channel IDs — you'll need these to send messages
CHANNELS = project["channels"] # {"general": "ch-xxx", "engineering": "ch-yyy", "research": "ch-zzz"}# Register a main agent
resp = httpx.post(
f"{TW}/api/external/projects/{PROJECT_ID}/agents",
headers=HEADERS,
json={
"name": "Atlas",
"role": "orchestrator",
"soul_prompt": "You are Atlas, a helpful AI assistant.",
},
)
AGENT_ID = resp.json()["agent_id"]
# Register specialist agents (optional — for multi-agent display)
for name, role in [("Researcher", "researcher"), ("Coder", "developer")]:
httpx.post(
f"{TW}/api/external/projects/{PROJECT_ID}/agents",
headers=HEADERS,
json={"name": name, "role": role},
)TeamWork forwards user messages to your webhook. The payload includes the channel_id so you know where to reply:
from fastapi import FastAPI
app = FastAPI()
@app.post("/webhook")
async def handle_teamwork_message(payload: dict):
if payload["type"] != "user_message":
return {"ok": True}
user_text = payload["content"]
channel_id = payload["channel_id"] # reply to this channel
project_id = payload["project_id"]
# Show typing indicator while processing
httpx.post(
f"{TW}/api/external/projects/{project_id}/typing",
headers=HEADERS,
json={"channel_id": channel_id, "agent_id": AGENT_ID, "is_typing": True},
)
# === Your AI logic here ===
response = my_llm_call(user_text)
# Send response back to the same channel
httpx.post(
f"{TW}/api/external/projects/{project_id}/messages",
headers=HEADERS,
json={
"channel_id": channel_id,
"agent_id": AGENT_ID,
"content": response,
},
)
return {"ok": True}Tasks appear on the drag-and-drop Kanban board in the UI. Your agent controls them via the API:
# Create a task and assign it to an agent
resp = httpx.post(
f"{TW}/api/external/projects/{PROJECT_ID}/tasks",
headers=HEADERS,
json={
"title": "Implement user authentication",
"description": "OAuth2 with Google provider",
"assigned_to": AGENT_ID,
"status": "in_progress", # pending | in_progress | blocked | review | completed
"priority": 1, # higher = more important
},
)
task_id = resp.json()["task_id"]
# Update task status as work progresses
httpx.patch(
f"{TW}/api/external/projects/{PROJECT_ID}/tasks/{task_id}",
headers=HEADERS,
json={"status": "completed"},
)
# Read back the full task board (internal API)
board = httpx.get(f"{TW}/api/tasks", params={"project_id": PROJECT_ID}).json()
# board["tasks"] contains all tasks with status, assignee, subtasks, etc.Tasks also support subtasks (
parent_task_id), blockers (blocked_by), and git commit tracking (start_commit/end_commit). See Swagger for the full schema.
# Show agent as working
httpx.patch(
f"{TW}/api/external/projects/{PROJECT_ID}/agents/{AGENT_ID}/status",
headers=HEADERS,
json={"status": "working"},
)
# Set back to idle when done
httpx.patch(
f"{TW}/api/external/projects/{PROJECT_ID}/agents/{AGENT_ID}/status",
headers=HEADERS,
json={"status": "idle"},
)Your agent's workspace is mounted into TeamWork and browsable in the UI. Your agent can also read/write files via the API:
# List files in the project workspace
files = httpx.get(f"{TW}/api/workspace/{PROJECT_ID}/files").json()
# Read a file
content = httpx.get(
f"{TW}/api/workspace/{PROJECT_ID}/file",
params={"path": "src/main.py"},
).json()
# Write a file
httpx.put(
f"{TW}/api/workspace/{PROJECT_ID}/file",
json={"path": "src/main.py", "content": "print('hello')"},
)
# View git history
log = httpx.get(f"{TW}/api/workspace/{PROJECT_ID}/git-log").json()If your agent runs inside a Docker container, users can open a live terminal directly in the TeamWork UI. Set SANDBOX_CONTAINER to the container name:
# .env
SANDBOX_CONTAINER=prax-sandbox # name of the Docker container to exec intoThat's it — TeamWork handles the rest. The frontend opens a WebSocket to /api/terminal/ws/{project_id}, which spawns a docker exec -it PTY session. Users see exactly the same filesystem and processes as the agent.
If your agent runs a headless Chrome in its sandbox, TeamWork streams a live screencast to the UI. Users can watch the agent browse and take over with mouse/keyboard.
# .env
SANDBOX_CONTAINER=prax-sandbox # container running Chrome
CHROME_CDP_HOST=sandbox # hostname of the container (Docker network name)
CHROME_CDP_PORT=9223 # CDP port exposed by the containerYour sandbox container needs to run Chrome headless with CDP enabled. Example entrypoint:
# In your sandbox Dockerfile / entrypoint:
chromium --headless=new --no-sandbox --disable-gpu --remote-debugging-port=9222 &
# socat bridge: Chrome only binds to 127.0.0.1, so forward to 0.0.0.0
socat TCP-LISTEN:9223,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:9222 &# Verify the browser is reachable from TeamWork:
info = httpx.get(f"{TW}/api/browser/info").json()
# {"available": true, "browser": "Chrome/146.0.7680.164"}The frontend connects via WebSocket at /api/browser/ws/{project_id} to stream screenshots and relay mouse/keyboard input back to Chrome.
If your sandbox runs a desktop (Xvfb + a VNC server + noVNC/websockify), TeamWork
proxies it so users get a full remote desktop in the UI. Point DESKTOP_VNC_URL
at the sandbox's noVNC HTTP endpoint:
# .env
DESKTOP_VNC_URL=http://sandbox:6080 # or http://localhost:6080 (see callout)TeamWork reverse-proxies GET /api/desktop/* to that URL and bridges the noVNC
WebSocket at /api/desktop/websockify to <DESKTOP_VNC_URL>/websockify. Unset →
the desktop panel returns 503 (no upstream).
⚠ Where does TeamWork run? (native vs. Docker)
The three panels above (terminal, browser, desktop) connect to the sandbox container, so the host/port you give them depends on where TeamWork itself runs:
- TeamWork inside Docker, on the sandbox's compose network → use the Docker network hostname:
CHROME_CDP_HOST=sandbox,DESKTOP_VNC_URL=http://sandbox:6080.- TeamWork running natively on the host, with the (split)
prax-sandboxcompose publishing ports on127.0.0.1→ use the container name + localhost:SANDBOX_CONTAINER=prax-sandbox-sandbox-1,CHROME_CDP_HOST=localhost,CHROME_CDP_PORT=9223,DESKTOP_VNC_URL=http://localhost:6080. The sandbox compose pinsname: prax-sandbox, so the container name is deterministic.The defaults (
chrome_cdp_host=sandbox, emptysandbox_container/DESKTOP_VNC_URL) are the in-Docker values; a native TeamWork left on the defaults shows the classic broken-panel triad: terminal 404 (nodocker exectarget → no session), desktop 503, browser CDP unreachable. Prax'smake run-local-all*targets set the native values automatically (see the Makefile'sTW_SANDBOX_ENV); you only need to set them by hand when running TeamWork natively without that Makefile. See.env.example.
TeamWork handles: UI rendering, message persistence, WebSocket broadcasting, task board state, file browsing, browser screencast, real-time updates.
Your agent handles: understanding user intent, generating responses, planning work, executing tasks, deciding what to say and when.
Prax is an AI agent built to use TeamWork as its web interface. If you want a ready-made agent with tool use, planning, multi-channel chat (SMS, Discord, web), and code execution — start with Prax.
Prax's docker-compose.yml includes TeamWork as a service. No separate clone needed:
git clone https://github.com/praxagent/gpt-transcriber.git prax
cd prax
cp .env-example .env # configure API keys
docker compose up -d
# Prax API: http://localhost:5001
# TeamWork: http://localhost:3000Prax automatically:
- Creates a project and registers its agents (Planner, Researcher, Executor, Skeptic, Auditor)
- Receives user messages via webhook and processes them through its LLM orchestrator
- Mirrors SMS/Discord conversations to TeamWork's #discord and #sms channels
- Pushes real-time execution output and agent delegation graphs to the UI
- Syncs its SQLite task board to TeamWork's Kanban
- Shows real-time agent status and typing indicators
teamwork/
├── src/teamwork/ # Python package (FastAPI)
│ ├── main.py # App entry + static file serving
│ ├── config.py # Pydantic settings
│ ├── cli.py # `teamwork` CLI entry point
│ ├── static/ # Bundled React build (gitignored)
│ ├── models/ # SQLAlchemy ORM
│ ├── routers/ # API endpoints
│ │ ├── agents.py # Agent CRUD + status + live output + execution graphs
│ │ ├── browser.py # CDP browser screencast proxy
│ │ ├── channels.py # Channel management
│ │ ├── external.py # External agent API (messages, tasks, status, channels, live output)
│ │ ├── messages.py # Chat messages (CRUD only, no AI)
│ │ ├── plugins.py # Plugin management proxy (delegates to Prax)
│ │ ├── projects.py # Project management
│ │ ├── tasks.py # Kanban task board
│ │ └── workspace.py # File browser
│ ├── services/ # Business logic
│ └── websocket/ # Real-time connection manager
├── frontend/ # React + TypeScript + Vite
│ ├── src/
│ └── vite.config.ts # Builds to ../src/teamwork/static/
├── pyproject.toml # Hatchling build, pip metadata
├── Dockerfile # Single-container build
├── docker-compose.yml # Standalone deployment
├── Makefile # build, install, dev, test, clean
└── dev.sh # One-command local dev server
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite |
| UI | Tailwind CSS |
| State | Zustand, React Query |
| Real-time | WebSocket |
| Backend | FastAPI (Python 3.11+) |
| Database | SQLite, SQLAlchemy (async) |
| Build | Hatchling |
| CI/CD | GitHub Actions, release-please |
When you install TeamWork, the React build is bundled inside the package at teamwork/static/. FastAPI serves static assets at /assets/ and uses a catch-all route to return index.html for all other paths, enabling client-side routing. No nginx, no separate frontend container.
TeamWork provides a file browser, editor, git log viewer, and backup download for each project's workspace. Your agent owns the workspace — TeamWork just serves it. But for the file browser, terminal, and backup to work correctly, the workspace must live at a known location.
Each project has a workspace directory on disk:
{WORKSPACE_PATH}/{workspace_dir}/
WORKSPACE_PATH— configured via environment variable (default:./workspace). This is the root directory containing all project workspaces.workspace_dir— a per-project subdirectory name, stored in the project record. Set it when creating the project via/api/external/projects, or let TeamWork auto-generate one from the project name.
Example: if WORKSPACE_PATH=/data/workspaces and a project's workspace_dir is my_project_abc12345, the full path is /data/workspaces/my_project_abc12345/.
TeamWork is agent-agnostic — it doesn't prescribe what goes inside the workspace. However, the following conventions are recommended for maximum compatibility with TeamWork's UI features:
{workspace_dir}/
├── .git/ # Optional — enables git log viewer and task diffs
├── .gitignore # Recommended — keep caches/artifacts out of git
│
├── active/ # Convention: current working files
│ ├── report.md # Documents, notes, generated content
│ ├── slides.tex # LaTeX, code, configs
│ └── diagram.png # Images, diagrams
│
├── archive/ # Convention: binary/media outputs
│ ├── presentation.mp4 # Videos, audio, large files
│ └── dataset.csv # Data exports
│
├── plugins/ # Convention: agent plugin storage
│ ├── custom/ # User-created plugins
│ └── shared/ # Imported plugin repos (git submodules)
│
├── user_notes.md # Convention: persistent user notes
├── config.yaml # Convention: agent/project configuration
└── ... # Anything else your agent needs
Required: The workspace directory must exist and be readable by the TeamWork process.
Optional but recommended:
- Git repository — enables the git log viewer (
/api/workspace/{project_id}/git-log) and per-task diffs. Initialize withgit initwhen the workspace is created. active/subdirectory — a well-known place for "current" files. Agents that use this convention can tell users "your file is inactive/report.md" and it will appear in the file browser.
What TeamWork ignores: The file browser automatically hides .git, __pycache__, node_modules, .venv, .DS_Store, and other common cache/build directories. These are also excluded from backups.
When your agent creates a project via the external API, pass workspace_dir to control the directory name:
resp = httpx.post(f"{TW}/api/external/projects", headers=HEADERS, json={
"name": "My Agent Workspace",
"workspace_dir": "user_12345", # Your agent's directory name
"webhook_url": "http://my-agent:9000/webhook",
})If workspace_dir is omitted, TeamWork generates one: {slugified_name}_{first_8_uuid_chars}.
Shared workspaces: If your agent has an existing workspace directory (e.g., a per-user directory at /data/workspaces/user_12345/), set TeamWork's WORKSPACE_PATH to the same parent directory and pass the matching workspace_dir. Both systems will then read from and write to the same directory — no symlinks or copies needed.
TeamWork provides a one-click workspace backup in the Settings panel. The backup downloads a zip file containing all workspace files, excluding version control (.git), caches, build artifacts, and environment files (.env).
Limits: 200 MB uncompressed maximum. If the workspace exceeds this limit, the download button is disabled with an explanation. This cap exists because the zip is built in memory on the server — for larger workspaces, use git directly or an external backup tool.
What's excluded:
.git/— version control history (usegit cloneto preserve this)__pycache__/,node_modules/,.venv/— caches and dependenciesbuild/,dist/— build artifacts.env— environment files (may contain secrets).DS_Store,Thumbs.db— OS junk
What's included: Everything else — code, configs, markdown, images, generated content, data files. TeamWork doesn't filter by file type because different agents produce different artifacts. If your workspace has large media files (videos, audio), they will be included in the zip and may push it past the 200 MB limit.
API endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/workspace/{project_id}/backup/info |
Pre-flight: file count, total size, whether it exceeds the limit |
GET |
/api/workspace/{project_id}/backup |
Download the zip (returns 413 if too large) |
TeamWork stores its application data (projects, messages, tasks, the FTS index) in a single SQLite file. This is separate from the workspace — WORKSPACE_PATH holds agent file content; the database holds TeamWork's own records.
-
Default:
{project_root}/data/vteam.db. TeamWork creates thedata/directory automatically on first run, so a fresh checkout just works — no manualmkdirneeded. -
Override: set
DATABASE_URL. Relative SQLite paths are resolved against the project root and the parent directory is created for you. Examples:DATABASE_URL="sqlite+aiosqlite:///./vteam.db" # → {project_root}/data/vteam.db DATABASE_URL="sqlite+aiosqlite:////var/lib/teamwork/tw.db" # absolute path (its dir is auto-created)
Because the database is independent of the workspace, the same TeamWork instance works for Prax or any other agent — point WORKSPACE_PATH at the agent's workspace root and leave the database wherever you like.
The Settings panel includes two tools for managing message history as your database grows:
Permanently removes all messages older than a chosen threshold (7 / 14 / 30 / 60 / 90 days). Runs VACUUM afterward to reclaim disk space. No external dependencies — this is pure SQL.
Replaces chunks of old messages with LLM-generated summaries. For every 50 messages, the LLM produces a concise bullet-point summary preserving key decisions, action items, and outcomes. The originals are deleted and replaced with a single [Summary of N messages] system message at the timestamp of the earliest message in the chunk.
Provider-agnostic: Compactify works with any OpenAI-compatible chat completions API. The user provides three fields per request:
| Field | Default | Examples |
|---|---|---|
| API Key | (required) | sk-... (OpenAI), Ollama doesn't need one but the field is required |
| Model | gpt-4o-mini |
claude-haiku-4-5-20251001, llama3, gemma2, grok-2 |
| API URL | https://api.openai.com/v1/chat/completions |
http://localhost:11434/v1/chat/completions (Ollama), http://localhost:1234/v1/chat/completions (LM Studio), https://api.groq.com/openai/v1/chat/completions |
TeamWork never stores the API key — it is used for the duration of the request and discarded. This preserves TeamWork's "zero AI dependency" principle: the core platform works without any LLM, but you can opt in to LLM-powered maintenance when you choose.
Safety rails:
- Minimum threshold is 7 days — you can't accidentally summarize active conversations.
- Summaries are clearly marked as
[Summary of N messages]so they're visually distinct. - A two-click confirmation flow prevents accidental triggers.
API endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/messages/stats/{project_id} |
Message count by age bracket (7d / 30d / 90d / older) + DB file size |
POST |
/api/messages/cleanup |
Delete messages older than N days + VACUUM |
POST |
/api/messages/compactify |
LLM summarization of old messages (accepts model and api_base_url) |
TeamWork uses SQLite FTS5 for message search, providing relevance-ranked results via the BM25 algorithm — the same ranking function used by Elasticsearch, Solr, and most modern search engines.
How it works:
A content-sync'd FTS5 virtual table (messages_fts) mirrors the content column of the messages table. SQLite triggers keep the index in sync on every INSERT, UPDATE, and DELETE — zero application-level bookkeeping. The tokenizer is porter unicode61, which means:
- Porter stemming — "running", "runs", and "ran" all match a search for "run"
- Unicode normalization — accented characters, CJK text, and mixed-script queries work correctly
Search queries use FTS5's MATCH syntax with bm25() ranking, which scores results by term frequency, inverse document frequency, and document length normalization. A query for "deploy fix" ranks a short message containing both terms higher than a long message mentioning "deploy" once in passing.
-- What the search endpoint executes:
SELECT m.id, m.content, m.channel_id, c.name, m.agent_id, a.name
FROM messages_fts fts
JOIN messages m ON m.rowid = fts.rowid
JOIN channels c ON c.id = m.channel_id
LEFT JOIN agents a ON a.id = m.agent_id
WHERE fts.content MATCH :query
AND m.project_id = :project_id
ORDER BY bm25(messages_fts)
LIMIT :limitMigration: The FTS5 table is created automatically on startup if it doesn't exist. Existing messages are backfilled into the index. No manual migration steps required — just restart the server.
Fallback: If FTS5 is unavailable (e.g., a SQLite build without the extension), the search endpoint falls back to LIKE '%query%' — functional but unranked.
TeamWork uses SQLite as its primary database. This is a deliberate architectural choice, not a prototype shortcut.
SQLite handles far more load than most developers expect. The official documentation states:
"SQLite works great as the database engine for most low to medium traffic websites (which is to say, most websites). The amount of web traffic that SQLite can handle depends on how heavily the website uses its database. Generally speaking, any site that gets fewer than 100K hits/day should work fine with SQLite." — sqlite.org/whentouse.html
Key performance characteristics relevant to TeamWork:
| Characteristic | SQLite capability | Reference |
|---|---|---|
| Maximum database size | 281 terabytes | sqlite.org/limits.html |
| Concurrent readers | Unlimited (with WAL mode) | sqlite.org/wal.html |
| Write throughput | ~60K–100K inserts/sec on modern SSDs | sqlite.org/speed.html |
| WAL mode | Readers never block writers; writers never block readers | sqlite.org/wal.html |
| Full-text search | FTS5 with BM25 ranking, porter stemming | sqlite.org/fts5.html |
| JSON support | Built-in JSON functions for structured data | sqlite.org/json1.html |
TeamWork enables WAL mode and sets a 5-second busy timeout at startup, which effectively eliminates write contention for single-user workloads.
TeamWork's architecture — one user, one project, one SQLite database — sidesteps the single-writer limitation entirely. Each user gets their own database file, their own containers, and their own agent processes. There is no shared write path.
This pattern has significant production precedent:
- Expensify runs a separate SQLite database per user, processing millions of expense reports daily. Their architecture treats each user's database as an isolated shard, eliminating cross-user write contention.
- Rails 8 adopted SQLite as a first-class production database, replacing Redis and PostgreSQL for queues (
solid_queue), caching (solid_cache), and pub/sub (solid_cable) — betting that per-process SQLite is simpler and faster than networked databases for the common case. See Rails 8 release notes. - Litestream (litestream.io) provides continuous SQLite replication to S3, enabling point-in-time recovery and cross-region disaster recovery without PostgreSQL's operational overhead.
- LiteFS (fly.io/docs/litefs) replicates SQLite databases across distributed nodes using FUSE, enabling read replicas at the edge.
The academic case for embedded databases in web applications is well-supported. Pavlo et al. (2017) in "What's Really New with NewSQL?" (SIGMOD Record) observe that most OLTP workloads are partitionable by user or tenant, making shared-nothing architectures (where each partition is an independent database) both simpler and faster than distributed transactions. SQLite-per-user is the logical endpoint of this observation.
The honest answer: probably never, for TeamWork's architecture. As long as each user has their own SQLite database, there is no write contention to resolve and no scaling wall to hit. A single SQLite database comfortably handles millions of messages, thousands of channels, and complex FTS5 queries.
Migration to PostgreSQL would become necessary if TeamWork moves to a multi-tenant shared database model — specifically:
| Trigger | Why PostgreSQL helps |
|---|---|
| Shared database across users | PostgreSQL's MVCC handles concurrent writers from multiple users gracefully; SQLite's single-writer lock becomes a bottleneck |
| Horizontal read scaling | PostgreSQL supports streaming replication to read replicas; SQLite is single-node |
| Advanced query patterns | Materialized views, window functions over large datasets, GIN/GiST indexes for geospatial or array queries |
| Operational tooling | pgBackRest, pg_stat_statements, connection pooling (PgBouncer), mature monitoring ecosystem |
For TeamWork's current per-user sandboxed architecture, SQLite + WAL + FTS5 is the right choice — simpler to deploy, zero network latency, zero connection pool management, and more than sufficient performance.
Chat with your agent while watching it browse in real time. The "Take Over" button (top right) lets you control the browser with your own mouse and keyboard.
| Chat | Kanban Board |
|---|---|
![]() |
![]() |
| File Viewer | Live Sessions |
|---|---|
![]() |
![]() |
View real-time agent delegation trees, launch terminal sessions, or watch the browser screencast directly in the UI. Agents run in isolated Docker containers with your workspace mounted. Multiple terminal sessions can run simultaneously.
Create a .env file (copy from .env.example):
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite+aiosqlite:///data/vteam.db |
Database connection string |
WORKSPACE_PATH |
./workspace |
Directory for generated code/files |
EXTERNAL_API_KEY |
— | Required. Shared key for external agent access; must equal the agent's TEAMWORK_API_KEY. Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))". Unset ⇒ the external API returns 503 |
AGENT_CLIENTS_PATH |
— | JSON registry binding one token to one agent + capability set (preferred over the shared key when running several agents) |
REQUIRE_SIGNED_REQUESTS |
false |
Require a valid Ed25519 envelope on every external request |
ALLOW_UNAUTHENTICATED_AGENTS |
false |
Dev only — restores accept-anything when no credential is configured |
CORS_ORIGINS |
localhost:5173,3000 |
Allowed CORS origins |
HOST |
0.0.0.0 |
Server bind address |
PORT |
8000 |
Server port |
DEBUG |
false |
Enable debug mode |
SANDBOX_CONTAINER |
— | Docker container name for terminal sessions |
CHROME_CDP_HOST |
sandbox |
CDP host for browser screencast |
CHROME_CDP_PORT |
9223 |
CDP port for browser screencast |
PRAX_URL |
— | Prax backend URL for plugin management proxy and execution graph API (e.g. http://app:5001) |
TeamWork itself does not require any AI API keys. All AI calls are made by the external agent you connect.
git clone https://github.com/praxagent/teamwork.git
cd teamwork
uv pip install -e ".[dev]"The Dockerfile builds the React frontend during docker build (line RUN cd frontend && npx vite build). However, Docker layer caching can serve stale JavaScript if it doesn't detect changes in the COPY frontend/ frontend/ layer. When you edit .tsx/.ts files and run docker compose up --build, Docker may reuse the cached build layer and your changes won't appear.
Symptoms: You changed frontend code, rebuilt the container, but the UI behaves exactly the same. The browser serves an old index-*.js bundle.
Fix — option A (recommended): Rebuild the frontend locally before Docker build. Since src/teamwork/static/ is copied into the image via COPY src/ src/, fresh local assets always invalidate the cache:
cd frontend && npm run build && cd ..
docker compose up --buildFix — option B: Force Docker to rebuild without layer cache:
docker compose build --no-cache && docker compose upWhy does this happen? Docker hashes the build context to decide whether a
COPYlayer has changed. Timestamp-only changes, editor swap files, or certain filesystem behaviors can cause Docker to consider the layer unchanged even when source files differ. The local build workaround sidesteps this entirely because the output JS bundle gets a new content hash (e.g.,index-DmPi-rii.js→index-Bx7kQ2f1.js), which Docker always detects as a change in theCOPY src/ src/layer.
./dev.sh
# Backend: http://localhost:8000
# Frontend: http://localhost:5173 (Vite dev server with HMR)# Terminal 1 — backend
make dev
# Terminal 2 — frontend
cd frontend && npm run devmake test
# or: pytest tests/ -x -q
# Playwright UI smoke tests (requires running docker-compose stack)
cd frontend && npx playwright testruff check .make build # builds frontend + Python wheel- CI: Runs on PRs to
main— lints with ruff, runs pytest - Release: Uses release-please for automatic semver releases on merge to
main
Full API docs with request/response schemas are auto-generated at http://localhost:8000/docs (Swagger UI) when the server is running. This is the authoritative reference for all 40+ endpoints — every field, type, and constraint is documented there.







