Run Open SWE on your machine: the backend and the dashboard on http://localhost:2024, with GitHub and Slack webhooks arriving through a tunnel. Deploying it for a team is the installation guide.
- Python 3.14+ and uv
- LangGraph CLI (installed by
uv sync) - Node 22.22.2+ and pnpm for the dashboard
- A free ngrok account, so GitHub and Slack can reach your local backend (step 3)
- A Slack workspace where you may create apps, and a GitHub account or organization where you may create a GitHub App
git clone https://github.com/langchain-ai/open-swe.git
cd open-swe
uv venv
source .venv/bin/activate
uv sync --all-extrasFollow Create a GitHub App in the installation guide with these values, and install it on the repositories you want to test against:
- Callback URL:
http://localhost:2024/dashboard/api/auth/callback. This is where GitHub sends the browser after "Sign in with GitHub" on your local dashboard. - Webhook URL:
https://<name>.ngrok-free.dev/webhooks/githubwith your ngrok domain from step 3, or leave the webhook off (untick Active) if you only start runs from the dashboard. GitHub cannot deliver tolocalhost.
Use a name of your own (GitHub App names are unique), and give it a distinct mention handle (OPEN_SWE_MENTION_TAGS) if a shared deployment already answers to @openswe in the same repositories.
Always run an ngrok tunnel when starting Open SWE locally. GitHub and Slack need a public HTTPS hostname that stays the same across restarts. Reuse an existing tunnel and its exact configured domain, forwarding to the active backend (normally localhost:2024). If no tunnel is running, recover the domain from configuration or prior local runtime notes in the primary checkout before starting one; an automatically assigned hostname will not match existing webhook settings.
For a first-time setup, the free ngrok plan gives you a static domain:
-
Sign up at dashboard.ngrok.com and install the agent (
brew install ngrok, or the download the dashboard offers). -
Connect the agent to your account with the
ngrok config add-authtoken …command shown under Getting Started → Your Authtoken. -
Under Domains, claim the free static domain. It looks like
<name>.ngrok-free.dev. -
Start the tunnel and leave it running while you develop:
make tunnel NGROK_DOMAIN=<name>.ngrok-free.dev # or export NGROK_DOMAIN once in your shell
make tunnel runs ngrok http 2024 on that domain with examples/ngrok/webhooks-only.yml as its traffic policy, so only /webhooks/* is reachable from the internet. That restriction is not optional. Under langgraph dev the LangGraph API itself (/threads, /runs, /assistants, /store, …) has no authentication at all: the dashboard API checks its session cookie and the webhook endpoints check their signatures, but anyone who can reach port 2024 can read and create threads and runs. A tunnel that forwards the whole port publishes exactly that. Everything except the webhooks stays on http://localhost:2024, where you keep opening the dashboard. Check the policy once the backend is up (step 6): curl https://<name>.ngrok-free.dev/webhooks/slack answers {"status":"ok", …} from the backend, while /ok gets ngrok's own 404.
Preserve the existing webhook traffic policy. For local Slack OAuth, also preserve the callback relay: requests to /dashboard/api/slack/callback on the ngrok domain must redirect to http://localhost:2024/dashboard/api/slack/callback with the complete query string intact. The stock webhooks-only policy blocks this path, so reuse the local policy containing that redirect when Slack OAuth is configured. The callback is a redirect to localhost; dashboard pages and API routes must remain inaccessible through the tunnel.
Slack delivers events to one URL per app, so a local backend needs its own Slack app rather than the one a shared deployment uses. Follow Create the Slack app in the installation guide with your ngrok domain from step 3, <name>.ngrok-free.dev, as <your-url> (the manifest supplies the https://), and give it a name that says it is yours, for example open-swe-<you>; the bot's handle follows from it. Copy the four values it lists into .env in the next step.
Slack checks the events Request URL against a running backend. If you create the app before make dev is up, open Event Subscriptions afterwards and press Retry. The same applies whenever you change SLACK_SIGNING_SECRET: restart the backend, then Retry.
Create .env in the repository root; langgraph dev loads it.
LANGSMITH_API_KEY="" # LangSmith → Settings → API Keys; also used for sandboxes and trace links
LANGSMITH_TRACING="true" # trace runs to LangSmith
LANGSMITH_PROJECT="" # optional project for traces and "View trace" links; default "default"
ANTHROPIC_API_KEY="" # any provider key; not needed if you use an LLM gateway (e.g. LangSmith Gateway; see the installation guide)
GITHUB_APP_ID="" # step 2
GITHUB_APP_CLIENT_ID=""
GITHUB_APP_CLIENT_SECRET=""
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" # one double-quoted line, \n between the PEM lines
GITHUB_WEBHOOK_SECRET=""
GITHUB_APP_INSTALLATION_ID=""
ALLOWED_GITHUB_USERS="" # your GitHub login; required unless ALLOWED_GITHUB_ORGS is set
SLACK_BOT_TOKEN="" # step 4: OAuth & Permissions → Bot User OAuth Token (xoxb-...)
SLACK_SIGNING_SECRET="" # Basic Information → App Credentials
SLACK_BOT_USER_ID="" # the bot's member id (bot profile → ⋮ → Copy member ID)
SLACK_BOT_USERNAME="" # the bot's handle, e.g. open_swe_you
SLACK_PUBLIC_BASE_URL="https://<name>.ngrok-free.dev" # your existing domain from step 3; used by Slack OAuth and the admin manifest
TOKEN_ENCRYPTION_KEY="" # openssl rand -base64 32 (encrypts stored GitHub and Slack tokens)
DASHBOARD_JWT_SECRET="" # openssl rand -hex 32 (signs the session cookie and OAuth state)
CONFIGURED_ADMINS="" # your GitHub login or email; admins see the Admin pages
POSTGRES_URI="" # local dev defaults to localhost:5433; set this to use another databaseLANGGRAPH_URL defaults to http://localhost:2024, and DASHBOARD_BASE_URL / DASHBOARD_API_BASE_URL default to it, so none of the three is needed locally. Keep them on localhost when setting SLACK_PUBLIC_BASE_URL to the tunnel. You only need one model credential: either a provider key or a gateway key if you route model calls through an LLM gateway, such as the LangSmith Gateway. How the running model is chosen is covered in the same section. Linear, if you use it, comes from the Linear section of the installation guide, with your ngrok domain as the URL.
Open SWE needs a PostgreSQL database for its own tables, and langgraph dev does not provide one: it keeps LangGraph's threads and Store in memory, so the platform's Postgres is not there locally. In LangGraph's local_dev runtime, Open SWE defaults POSTGRES_URI to postgresql://postgres:postgres@127.0.0.1:5433/postgres; make dev and make dev-ui run the matching postgres:16 container named open-swe-postgres when no explicit value is set. Docker Compose binds the container to loopback only and keeps its data in the open-swe-postgres volume, so stopping or removing the container preserves your local users, workspaces, and settings. make postgres starts it on its own; use docker compose down to stop it. Set POSTGRES_URI to skip the container and use any database you can create schemas in — see Analytics storage for what startup migrations create there, including the repository, users, and workspace tables.
TEST_ANALYTICS_POSTGRES_URI is the same thing for the test suite, and only for it: the tests that exercise those tables create a throwaway schema per test, migrate it, and drop it afterwards, so point it at a separate database (postgresql+asyncpg://<user>@localhost:5432/open_swe_test) rather than the one make dev uses. Unset, every such test skips rather than fails, so a run without it proves less than it appears to; CI sets it, so a regression in that code is caught there either way.
make dev refuses to start while something else listens on port 2024, and names the process. When switching worktrees, stop the previous backend gracefully and wait for it to release port 2024 before starting the replacement. Prepare the worktree state before startup.
make build-dashboard # pnpm install + Vite build into ui/.output/public
make dev # langgraph dev on http://localhost:2024, serving the API and the dashboard (starts the Postgres container first)langgraph dev serves the graphs, the FastAPI app, and the dashboard build together on port 2024. The bundled UI is a static build, so rebuild it when you pull UI changes, or skip make build-dashboard if you only need webhooks and the API. It reloads on code changes only: after editing .env, restart it.
Working on the UI? Have the backend front the Vite dev server instead of serving a build:
make dev-ui # Vite on :3000 and the backend on :2024 forwarding UI requests to it, in one terminalmake dev-ui runs make web and make dev side by side, the backend with DASHBOARD_DEV_SERVER_URL=http://localhost:3000; Ctrl-C stops both. Open http://localhost:2024 as usual: the page, its modules, and hot module replacement come from Vite, while /dashboard/api/* and the LangGraph routes stay with the backend. Nothing else changes, because the browser never leaves port 2024. The HMR WebSocket connects straight to Vite's port; the UI's Vite config points the client there.
| Endpoint | Purpose |
|---|---|
/ |
Dashboard |
POST /webhooks/github |
GitHub issue, PR, and comment webhooks |
POST /webhooks/slack, POST /webhooks/slack/interactivity |
Slack events and Block Kit interactions |
POST /webhooks/slack/commands |
The /oswe slash command |
POST /webhooks/linear |
Linear comment webhooks |
GET /dashboard/api/auth/login, GET /dashboard/api/auth/callback |
GitHub login |
/dashboard/api/* |
Dashboard API |
GET /ok, GET /health |
Health checks |
make runserves the FastAPI app alone with uvicorn on port 8000, without the LangGraph runtime. Nothing that creates runs works there; usemake dev.
Before reporting readiness, verify /ok on localhost, open the dashboard in a browser, and check tunnel forwarding and the OAuth callback redirect. A healthy /ok does not mean the UI is ready: make dev needs a dashboard build or a running Vite server to serve it.
Dashboard. Open http://localhost:2024, click Sign in with GitHub, and you should land logged in. With your login in CONFIGURED_ADMINS, the Admin pages appear. Set Admin → Global defaults → Default Repository, then start a task from the composer.
Slack. With the tunnel running and the Request URL verified, invite your bot to a channel and mention it: @open_swe_you what's in the repo?. It replies in a thread; ngrok's inspector at http://localhost:4040 shows the event arriving.
GitHub. With the tunnel running and the App's webhook pointed at it, comment @openswe what files are in this repo? on an issue in a repository where the App is installed. Within a few seconds you should see a 👀 reaction, a run in your LangSmith project, and a reply comment. GitHub-triggered runs act as the commenting user, so that account has to have signed in to your local dashboard once. The App's Advanced tab lists every delivery and its response, and ngrok's inspector at http://localhost:4040 shows what arrived.
With only the seeded default workspace, Slack and GitHub runs land there by default. Create additional workspaces from the Workspaces page to exercise routing locally: the same order applies as in a deployment (thread, workspace:<slug> tag on the opening message — env:<slug> remains an alias, owning repository, bound Slack channel, user default, then default); see How a run picks its workspace in the installation guide.
Incidents. Follow Incidents setup to enroll Slack channels. Set SLACK_APP_ID; anyone in a channel can pause or complete its incident, and asking the agent requires a connected Open SWE account. Incident turns run on the main agent graph and are dispatched straight from the Slack webhook, so make dev or make dev-ui is all that is needed.
Record the worktree, process IDs, fixed tunnel domain, and state location in ignored logs/local-dev/ notes in the primary checkout so the next session can reuse them.
langgraph dev persists local threads, checkpoints, and Store data under .langgraph_api in its working directory. Preserve existing local data when moving development to a new worktree unless you want a clean start.
The repository's .worktreeinclude includes the root .langgraph_api directory. Local Codex-managed worktrees copy matching ignored files when they are created. Stop the source backend gracefully so it flushes persistence, and back up its state before creating the worktree. This is a snapshot, not ongoing synchronization.
Codex skips source symlinks and preserves files already present in the destination. Create the worktree from the primary checkout containing the actual state directory. For worktrees created with git worktree add, or when the source is a symlink, copy the stopped primary checkout's entire .langgraph_api directory manually, including hidden files. Preserve any existing destination state rather than overwriting it automatically.
For one continuous local instance across worktrees, link .langgraph_api to the primary checkout's state directory instead of copying it. Only one backend may use that shared directory at a time; restart from the desired worktree to switch code. Independently copied state diverges after creation and also retains schedules and integration settings, so avoid running multiple copies against the same live integrations, which can duplicate background work.
Keep state, backups, and environment files ignored by Git. Reuse the existing local environment, including its token-encryption settings, without printing credentials. .worktreeinclude copies local state into worktrees; it does not add that state to version control.
make dev-ui is the simple way to develop the UI. Opening Vite on http://localhost:3000 directly also works:
pnpm install # from the repo root: ui/ and desktop/ are one pnpm workspace
make web # Vite on http://localhost:3000, proxying /dashboard/api/* to DASHBOARD_API_URL (default http://localhost:2024)The browser now talks to http://localhost:3000, so the session cookie has to be set on that origin and the login callback has to return there:
DASHBOARD_BASE_URL="http://localhost:3000" # the frontend origin; allowed for the CSRF check and post-login redirects
DASHBOARD_API_BASE_URL="http://localhost:3000" # what browsers use for /dashboard/api/* and the OAuth callbackand the GitHub App needs http://localhost:3000/dashboard/api/auth/callback as an additional callback URL. Keep both URLs on http:// locally so the cookie is SameSite=Lax. DASHBOARD_ALLOWED_ORIGINS lists additional origins that may call the API with credentials; credentialed CORS is only enabled when it is set, and * is rejected.
pnpm run dev:prod runs the Vite dev server, hot reload and all, against a deployment instead of a local backend — UI work against real threads, repositories, and settings without running the agent locally. Put the deployment in .env (step 5's file, gitignored):
DASHBOARD_API_URL=https://your-deployment.example.com # a preview deploy, not productionthen pnpm run dev:prod. There is no default, because a fallback would be someone else's production. DASHBOARD_API_URL in the environment overrides the file, and it is the only key read out of .env: the rest of it is the local backend's secrets, which have no business in the environment of a dev server the browser talks to.
The first run opens your browser to sign in. That leg still runs against the deployment's own registered GitHub callback — nothing changes on the GitHub App, and nothing changes on the deployment. It is the PKCE loopback handoff the desktop app uses (?desktop_handoff=…&desktop_port=…): the deployment redirects your browser to http://127.0.0.1:<port>/callback with a code that lives 120 seconds, and the script exchanges it at /dashboard/api/auth/desktop/exchange for a session. That tab then waits for Vite and sends you on to the dev server, so signing in ends where you want to be. The session lasts a week and is cached in ~/.cache/open-swe/dev-session.json; after that you sign in again.
The dev server then attaches that session to everything it proxies, and presents the deployment's own origin so its CSRF check accepts mutations. The session is real. A task started from http://localhost:3000 is a real run on that deployment, and the Admin pages edit its real settings.
pnpm run build, pnpm run typecheck, and pnpm run test run across the workspace through Turborepo (pnpm --filter open-swe-dashboard run <script> scopes one); pnpm run lint (oxlint) and pnpm run format / pnpm run format:check (oxfmt) run once from the root over every JS and TS file.
The shared preview environment combines main, preview-manual, and open PRs labeled preview whose branch lives in this repository (anyone with write access; a fork's code stays out); it is not an isolated deployment per PR. (Note: staging follows main and is for post-merge testing.)
- Add the
previewlabel to your PR. - Run Deploy open-swe preview on
mainwith force unchecked, or wait for a scheduled run at :04, :19, :34, or :49 each hour. Labeling alone does not deploy. - In the run summary, confirm your PR and head commit appear under Preview tree → Merged, not Skipped. A successful run may still omit a PR.
- Wait for the dashboard rollout, then confirm in LangSmith Deployments that the preview backend revision matches the published
previewcommit and deployed successfully. - Open preview and test with non-production tasks and repositories. For local UI iteration against that backend, see Dashboard against a deployed backend.
Conflicting PRs are skipped, unlabeled, and receive resolution instructions for the shared preview-manual branch. Resolve the conflict before reapplying the label. Use force only to rebuild an unchanged preview tree.
To remove a PR, remove its label and trigger or await another run; the current deployment remains until its replacement deploys, and changes in main or preview-manual remain. Every seven days, a scheduled run between 07:00 and 07:59 America/New_York resets preview to main, removes labels, and deletes preview-manual.
The Electron app in desktop/ includes the compiled dashboard UI. Run it next to the backend:
pnpm install # from the repo root
make dev # terminal 1
pnpm run dev:desktop # terminal 2Development connects to http://localhost:2024. For a hosted backend run pnpm --dir desktop run start -- --backend-url=https://your-backend.example.com or set OPEN_SWE_BACKEND_URL. pnpm --dir desktop run pack creates an unpacked application and pnpm --dir desktop run dist an installer. Packaged builds ask for the organization's backend URL on first launch and never default to the maintainers' deployment. The GitHub App must allow <backend-url>/dashboard/api/auth/callback for desktop login.
The dashboard records two performance spans, in every build, with the same code path locally and in production (ui/src/lib/perf/):
| Span | Starts | Steps | Ends |
|---|---|---|---|
thread_load |
The navigation to /agents/:threadId (or the document's time origin on a full page load, cold=true) |
detail (thread summary, GET /threads/:id), hydrate (SDK state fetch, GET /threads/:id/state), paint |
First frame after the transcript rendered |
agent_run |
Pressing send (joined=true when the run was started elsewhere, e.g. a queued message) |
accepted, stream_open, first_event, generation_start (first assistant message, so the run is visibly producing thinking or a tool call), first_text (first streamed assistant text) |
The run's streaming phase ends (reason: success, error, interrupt, stopped) |
Each span carries attributes: request time-to-first-byte and the backend's Server-Timing phases for the detail and state requests (detail_srv_thread_get_ms, state_srv_get_state_ms, …), whether the detail came from the sidebar cache, message and chunk counts, time spent in streamMessagesToUi (build_ms), protocol event and text-delta counts, the lag between the server's event timestamp and receipt (lag_avg_ms, includes clock skew, read as a trend), and, in development builds only, React commit time for the transcript while streaming (commit_ms).
Locally. Spans print to the console in dev builds ([perf] thread_load 812ms — detail 120 · hydrate 640 · paint 812). Open a thread with ?perf=1 for an overlay listing recent spans with a copy-as-JSON button (?perf=0 hides it again; the flag persists in localStorage). window.__openSwePerf.spans() and .export() return the same data for scripts and Playwright. Every span is also a User Timing mark and measure named osw:*, so it appears on the Timings track of the Chrome Performance panel next to long tasks and network requests, which is where to look once a span says what is slow. Compare cold and warm loads separately (cold, detail_cached), and reload a few times per change: single samples are noisy.
In production. With Datadog RUM configured, ended spans are sent as custom duration vitals named thread_load and agent_run, attributes in the vital context and client (web or desktop) in the global context. Abandoned spans (navigated away, hydration failed) stay local only. The backend side of the same picture is the Server-Timing header on GET /dashboard/api/threads/{id} and /state (logged as thread state timings) and the open_swe_dashboard_thread_ttft histogram, which measures the same thing as first_text rather than the first token of any kind.
For "how long until the agent answers", read @context.step_generation_start_ms; @context.step_first_text_ms sits after the opening tool calls and is much larger.
Desktop diagnostics. Installed builds keep View → Toggle Developer Tools and add Help → Save Diagnostics Report…, which writes the renderer's recent console output, the main process's warnings, app and OS versions, and the exported perf spans to a text file, with session cookies, bearer tokens and provider keys redacted. Ask users to attach that file to a report.
| Target | What it does |
|---|---|
make dev |
langgraph dev on port 2024: graphs, webhooks, dashboard API, and the bundled dashboard when a build exists |
make dev-ui |
make web and make dev together, the backend fronting Vite so the UI hot-reloads on port 2024 |
make web |
The Vite dev server alone on port 3000 |
make build-dashboard |
Installs the dashboard's dependencies and builds it into ui/.output/public |
make tunnel NGROK_DOMAIN=… |
ngrok http 2024 on your static domain, exposing only /webhooks/* |
make run |
The FastAPI app alone on port 8000, no LangGraph runtime |
make desktop |
The Electron app in development, against a backend on port 2024 |
make install |
uv sync --extra dev |
make test [TEST_FILE=tests/…] |
pytest -vvv on tests/ or the given path |
make lint, make format, make format-check |
ruff check and format (format rewrites files) |
make typecheck |
ty check agent tests |
- The tunnel must be running (
make tunnel) against port 2024, and the URL in GitHub or Slack must be your ngrok domain. Do not swap in a tunnel that forwards the whole port; see step 3. GitHub shows each delivery under the App's Advanced tab; ngrok's inspector athttp://localhost:4040shows what arrived. With the webhooks-only policy, ngrok itself answers 404 for anything outside/webhooks/*, so test with/webhooks/slack, not/ok. - Restart the backend after changing
.env:langgraph devreloads on code changes only, so a newGITHUB_WEBHOOK_SECRETorSLACK_SIGNING_SECRETis not picked up until then, and every delivery is rejected asInvalid signaturein the meantime. Slack then needs Retry on its Request URL under Event Subscriptions. - Webhook secrets are required: without
GITHUB_WEBHOOK_SECRET,SLACK_SIGNING_SECRET, orLINEAR_WEBHOOK_SECRET, every request to that endpoint is rejected with 401.
redirect_uri is not associated with this application: the App must listhttp://localhost:2024/dashboard/api/auth/callback(or the:3000one when you open Vite directly). Add it in the App's settings.- Login redirects but the session does not stick: keep local URLs on
http://so the cookie isSameSite=Lax. DASHBOARD_BASE_URL not configuredon Sign in with Slack or Notion: the backend has neither a dashboard build norDASHBOARD_DEV_SERVER_URL, so it does not know where the dashboard is. Runmake build-dashboardor usemake dev-ui.- Admin pages 403: add your GitHub login or email to
CONFIGURED_ADMINS.
- There is no dashboard build: run
make build-dashboard, or usemake dev-ui. - With Vite on port 3000,
curl -i http://localhost:3000/dashboard/api/meshould return the backend's401, not HTML; otherwise exportDASHBOARD_API_URLbeforemake web.
make dev-ui and make web refuse to start when another Vite is still running (Vite is configured with strictPort). Stop the old one or check lsof -iTCP:3000 -sTCP:LISTEN.
For sandbox, token-encryption, and "agent not responding" problems, see the installation guide's Troubleshooting.