diff --git a/.github/workflows/embed.yml b/.github/workflows/embed.yml
new file mode 100644
index 000000000..62c347c39
--- /dev/null
+++ b/.github/workflows/embed.yml
@@ -0,0 +1,101 @@
+name: Embed widget
+
+# Builds and tests the WebBrain for websites widget (embed/) whenever it or
+# the extension code it bundles changes, including loading embed.js in Chrome.
+# Pushing a tag like embed-v1.0.0 publishes a GitHub release with embed.js,
+# tools.json, the icons, VERSION.json and SHA256SUMS, which sites.webbrain.one
+# pins by version.
+
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/embed.yml"
+ - "embed/**"
+ - "package.json"
+ - "package-lock.json"
+ - "src/chrome/**"
+ push:
+ branches:
+ - main
+ paths:
+ - ".github/workflows/embed.yml"
+ - "embed/**"
+ - "package.json"
+ - "package-lock.json"
+ - "src/chrome/**"
+ tags:
+ - "embed-v*"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: embed-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ defaults:
+ run:
+ working-directory: embed
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Build and test
+ run: |
+ npm run build
+ npm test
+
+ # Playwright comes from the repository root; the widget itself has no dependencies.
+ - name: Install Playwright
+ working-directory: .
+ run: npm ci
+
+ - name: Run embed.js in Chrome
+ env:
+ EMBED_CHROME_PATH: /usr/bin/google-chrome
+ run: npm run test:browser
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/embed-v')
+ needs: test
+ runs-on: ubuntu-24.04
+ timeout-minutes: 5
+ permissions:
+ contents: write
+ defaults:
+ run:
+ working-directory: embed
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Check the tag matches embed/package.json
+ run: |
+ version="$(node -p "require('./package.json').version")"
+ if [ "${GITHUB_REF_NAME}" != "embed-v${version}" ]; then
+ echo "Tag ${GITHUB_REF_NAME} does not match embed/package.json version ${version}" >&2
+ exit 1
+ fi
+
+ - name: Build
+ run: npm run build
+
+ - name: Publish the release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "${GITHUB_REF_NAME}" dist/* \
+ --title "WebBrain for websites widget ${GITHUB_REF_NAME#embed-v}" \
+ --notes "The script site owners add with
+```
+
+It is built from the WebBrain extension's own code in this repository, so the
+widget reads and acts on pages exactly the way the extension does:
+
+| Extension file (`src/chrome/`) | What the widget uses it for |
+| --- | --- |
+| `src/content/accessibility-tree.js` | Reading the page as an accessibility tree |
+| `src/content/content.js` | Clicking, typing, scrolling and extracting on the page |
+| `src/content/agent-visual-indicator.js` | The on-page activity glow |
+| `src/ui/activity-brain.js` | The thinking animation |
+| `src/ui/markdown-render.js` | Rendering answers |
+| `src/agent/calculator.js` | Exact arithmetic |
+| `src/agent/read-page-window.js` | Paging `read_page` results |
+| `src/agent/credential-fields.js` | Keeping credential field values out of tool results |
+| `src/agent/tools.js`, `adapters.js` | The Ask and Act tools and prompts in `tools.json` |
+
+`src/widget-runtime.js` adds the launcher, the chat panel and the agent loop
+that talks to the server. The widget only runs the tools `tools.json` lists
+for the turn's mode, and Act is off unless the site's config sets
+`modePolicy: "ask_and_act"`.
+
+Elements with `data-webbrain-ask="…"` start an Ask run when a visitor clicks
+them. `data-webbrain-act="…"` only puts its prompt in the composer for the
+visitor to send, since any markup on the page, including content the site's
+users wrote, can carry the attribute.
+
+The widget is licensed GPL-3.0-or-later, like the rest of WebBrain. Every copy
+of `embed.js` states its license and links back to this folder.
+
+## Build and test
+
+No dependencies to install; Node 20 or newer.
+
+```bash
+cd embed
+npm run build # writes dist/
+npm test
+npm run test:browser # loads embed.js in Chrome; needs `npm ci` at the repository root
+```
+
+`dist/` contains:
+
+| File | Purpose |
+| --- | --- |
+| `embed.js` | The widget |
+| `tools.json` | System prompts and tool definitions for the server that answers the widget |
+| `icon48.png`, `icon128.png` | WebBrain icons the widget shows |
+| `VERSION.json` | Widget version, the commit it was built from, and a link to that source |
+| `SHA256SUMS` | Checksums of the files above |
+
+## Releases
+
+1. Bump `version` in `embed/package.json`.
+2. Push a tag `embed-v`, for example `embed-v1.0.0`.
+
+The **Embed widget** workflow builds, tests and publishes a GitHub release
+with the files above. It is never marked as the repository's latest release,
+so extension releases stay on top.
+
+The server behind sites.webbrain.one pins one of these releases by version and
+checks it against `SHA256SUMS`. Pull requests that change the widget or the
+extension files it bundles run the same build and tests.
diff --git a/embed/package.json b/embed/package.json
new file mode 100644
index 000000000..d7200d9a7
--- /dev/null
+++ b/embed/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@webbrain/embed",
+ "version": "1.0.0",
+ "private": true,
+ "description": "WebBrain for websites: the script site owners add to their pages. Built from the WebBrain extension's own content scripts.",
+ "type": "module",
+ "license": "GPL-3.0-or-later",
+ "scripts": {
+ "build": "node scripts/build.mjs",
+ "test": "node --no-warnings --test test/*.test.mjs",
+ "test:browser": "node --no-warnings --test test/browser.mjs"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/webbrain-one/webbrain.git",
+ "directory": "embed"
+ }
+}
diff --git a/embed/scripts/build.mjs b/embed/scripts/build.mjs
new file mode 100644
index 000000000..2344a6f04
--- /dev/null
+++ b/embed/scripts/build.mjs
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+/**
+ * Builds the WebBrain for websites widget into embed/dist/:
+ *
+ * embed.js the script site owners add with
+
+