An AI agent that reads your requirements, tests your staging app, and alerts your team β automatically.
Stop manually testing before every deploy. This agent reads your Notion docs, drives a real browser through your user flows, spots bugs using Claude, and pings your Slack with a one-click approve button that ships to prod.
View Demo Β· Quick Start Β· Architecture Β· Contributing
- API base URL:
https://<your-domain> - Health check:
https://<your-domain>/health
Stage 1 β Pre-deploy QA (runs on every PR merge to staging)
- Reads your feature requirements from a Notion page
- Claude extracts structured acceptance criteria from your prose
- Claude generates a human-readable browser action plan
- Playwright executes every step, taking screenshots along the way
- Claude evaluates the results against your requirements
- If a bug is found β Slack alert with the action plan, screenshot, severity label, and one-click Approve & Deploy / Dismiss buttons
Stage 2 β Post-deploy validation (runs after prod deploy)
- Pulls the last hour of Amplitude event streams from production
- Claude reads the session sequences against the same Notion requirements
- If real users are hitting unexpected paths β alerts the developer who pushed
Notion βββΊ requirementsParser βββΊ planGenerator βββΊ Playwright βββΊ resultEvaluator βββΊ Slack
β
Approve βββββ€
βΌ
GitHub Actions
β
βΌ
Amplitude βββΊ Claude βββΊ Slack
Stage 1 fires when a PR is merged to staging. The agent navigates the staging URL, fills the form, clicks submit, and catches the bug:
π΄ Bug Detected β demo-contact-form
Severity: critical | Confidence: high
What went wrong:
After clicking Submit, the page redirected to /error instead of showing
the success message "Thank you! We'll be in touch." as required.
Action plan Claude executed:
1. Navigate to https://staging.example.com/contact β Contact form visible
2. Fill "Name" field with "Test User" β Name field populated
3. Fill "Email" field with "test@example.com" β Email field populated
4. Fill "Message" field with "Hello" β Message field populated
5. Click "Submit" button β Success message appears
Triggered by: [View Notion requirement β]
[ β
Approve & Deploy to Prod ] [ β Dismiss ]
qa-agent/
βββ agent/
β βββ claudeClient.js # Shared Anthropic API wrapper
β βββ requirementsParser.js # Claude Call 1 β Notion prose β structured JSON
β βββ planGenerator.js # Claude Call 2 β requirements β action plan
β βββ resultEvaluator.js # Claude Call 3 β screenshots + page state β bug report
β βββ stage1.js # Stage 1 orchestrator
β βββ stage2.js # Stage 2 orchestrator
βββ browser/
β βββ playwrightRunner.js # Headless browser β executes action plans
βββ integrations/
β βββ notionClient.js # Fetches page content
β βββ amplitudeClient.js # Pulls session event streams
β βββ slackClient.js # Sends alerts + interactive buttons
β βββ githubClient.js # Dispatches workflow runs
βββ db/
β βββ schema.sql # feature_runs table
β βββ queries.js # Raw pg queries
βββ server/
β βββ index.js # Express: /slack/interact + /run-check
βββ config/
β βββ team.config.json # GitHub username β Slack user ID map
βββ scripts/
β βββ seed-demo.js # Seeds a demo run + instructions to add a bug
βββ .github/workflows/
βββ stage1-qa-check.yml # Triggers on PR merge to staging
βββ stage2-prod-verify.yml # Triggers via workflow_dispatch after prod deploy
Three focused Claude calls, each independently logged to Postgres. If one fails, the others don't re-run. Every input and output is stored β full audit trail per run.
- Node.js 20+
- A Railway account (free tier β for Postgres + Express hosting)
- A Notion workspace with API access
- An Amplitude account (free tier works)
- A Slack workspace where you can create apps
git clone https://github.com/your-username/qa-agent.git
cd qa-agent
npm install
npx playwright install chromiumcp .env.example .envOpen .env and fill in every value. See the Environment Variables section for where to get each one.
In Railway: create a new Postgres service β copy the connection string into DATABASE_URL. Then:
psql $DATABASE_URL -f db/schema.sqlExpected output: CREATE TABLE
Edit config/team.config.json:
{
"userMap": {
"your-github-username": "YOUR_SLACK_USER_ID"
}
}Find your Slack user ID: Slack β your profile β three-dot menu β Copy member ID.
Create a Notion page with this structure (prose is fine β no database needed):
Feature: Contact Form
Users should be able to submit a contact form with their name, email, and message.
Acceptance Criteria:
- The form has fields for Name, Email, and Message
- Clicking Submit sends the form
- A success message "Thank you! We'll be in touch." appears after submission
- The form fields are cleared after successful submission
Relevant URL: https://your-staging-url.com/contact
Copy the page ID from the Notion URL (the 32-character string after the last -). Add it to .env as NOTION_PAGE_ID.
- Go to api.slack.com/apps β Create New App β From scratch
- Incoming Webhooks β Activate β Add to workspace β copy URL β
SLACK_WEBHOOK_URL - Interactivity & Shortcuts β Enable β set Request URL to
https://<your-domain>/slack/interact - OAuth & Permissions β Bot scopes:
chat:write,files:writeβ Install to workspace β copy Bot Token βSLACK_BOT_TOKEN - Basic Information β copy Signing Secret β
SLACK_SIGNING_SECRET
Run these in order. Each group depends on the previous passing.
# Infrastructure
npm test tests/db.test.js
npm test tests/claudeClient.test.js
# Integrations
npm test tests/notionClient.test.js
npm test tests/amplitudeClient.test.js
# Agent modules
npm test tests/requirementsParser.test.js
npm test tests/planGenerator.test.js
npm test tests/playwrightRunner.test.js
npm test tests/resultEvaluator.test.js
# Slack + GitHub
npm test tests/slackClient.test.js # check Slack channel for the message
npm test tests/getSlackUser.test.js
npm test tests/githubClient.test.js
# End-to-end
npm test tests/stage1.test.js # full pipeline β Slack alert should fire
npm test tests/stage2.test.js # requires stage1 run to exist firstPush to GitHub β Railway β New Project β Deploy from GitHub repo β add all .env values as Railway environment variables. Railway auto-detects npm start.
Set your Slack app's Interactivity Request URL to https://<your-domain>/slack/interact.
Option A β Manual trigger (fastest for demos)
npm start
curl -X POST "http://localhost:3000/run-check?feature=<your-notion-page-id>"Option B β Automated via PR
Open a PR against the staging branch. Paste your Notion page ID as the PR body. Merge it β Stage 1 fires automatically.
Introduce a deliberate bug for the demo
In your staging app, make the form submit handler redirect to /error instead of showing the success message. Stage 1 will catch it, fire the Slack alert, and show the Approve/Dismiss buttons.
Seed demo data
npm run seedUse this to onboard the QA agent into a new team/project quickly.
- Fork or clone this repository into the target GitHub account.
- Create and fill
.envfrom.env.example. - Set up Postgres and run schema:
psql $DATABASE_URL -f db/schema.sql
- Configure Slack app:
- webhook URL
- bot token
- signing secret
- interactivity URL:
https://<your-domain>/slack/interact
- Configure Notion:
- create integration
- share requirements page with integration
- set
NOTION_PAGE_ID
- Configure Amplitude keys for prod session verification.
- Configure GitHub token and repo envs (
GITHUB_OWNER,GITHUB_REPO). - Deploy backend service (Railway or equivalent persistent Node host).
- Set custom domain and verify health:
https://<your-domain>/health
- Add GitHub Actions secrets and enable workflows.
- Update
config/team.config.json:- map GitHub usernames to Slack member IDs.
- Run validation tests in order:
npm test tests/db.test.jsnpm test tests/stage1.test.jsnpm test tests/stage2.test.js
After this checklist, teams can trigger Stage 1 from PR merges or manual API calls and use Slack Approve/Dismiss for controlled promotion to Stage 2 verification.
| Variable | Where to get it |
|---|---|
ANTHROPIC_API_KEY |
console.anthropic.com β API Keys |
NOTION_API_KEY |
notion.so/my-integrations β New integration |
NOTION_PAGE_ID |
From your Notion page URL |
AMPLITUDE_API_KEY |
Amplitude β Settings β Projects β API Key |
AMPLITUDE_SECRET_KEY |
Amplitude β Settings β Projects β Secret Key |
AMPLITUDE_EXPORT_DELAY_HOURS |
Optional. Defaults to 4 (handles Export API lag) |
AMPLITUDE_EXPORT_WINDOW_HOURS |
Optional. Defaults to 24 (query window size in hours) |
SLACK_WEBHOOK_URL |
Slack app β Incoming Webhooks |
SLACK_SIGNING_SECRET |
Slack app β Basic Information |
SLACK_BOT_TOKEN |
Slack app β OAuth & Permissions |
AI_PROVIDER |
anthropic, gemini, or openai |
ANTHROPIC_MODEL |
Optional Anthropic model override |
GEMINI_API_KEY |
Required only if AI_PROVIDER=gemini |
GEMINI_MODEL |
Optional Gemini model override |
OPENAI_API_KEY |
Required only if AI_PROVIDER=openai |
OPENAI_MODEL |
Optional OpenAI model override |
OPENAI_BASE_URL |
Optional OpenAI-compatible base URL (for example, ChatMock) |
GITHUB_TOKEN |
GitHub β Settings β Developer Settings β Fine-grained tokens (needs Actions: write) |
GITHUB_OWNER |
Your GitHub username |
GITHUB_REPO |
Your repo name |
DATABASE_URL |
Railway Postgres β connection string |
STAGING_BASE_URL |
e.g. https://your-app.vercel.app |
PORT |
Default 3000 |
Add all of these as GitHub Actions secrets too (Settings β Secrets β Actions). Use GH_TOKEN for your personal access token β GITHUB_TOKEN is reserved by GitHub Actions.
Three separate, stateless API calls β each with a focused system prompt:
| Call | Input | Output |
|---|---|---|
requirementsParser |
Raw Notion page content | [{ requirement, acceptanceCriteria, relevantUrl }] |
planGenerator |
Structured requirements | [{ stepNumber, action, expectedOutcome }] |
resultEvaluator |
Action plan + page states | { passed, severity, confidence, findings[], summary } |
Each call's input and output is logged to Postgres independently. If evaluation fails, you don't re-run the browser session. Every run is a full audit trail.
Default provider: anthropic (claude-sonnet-4-6). You can switch to Gemini/OpenAI via env.
Contributions are welcome. A few areas where help would be great:
- Better Playwright action parsing β the current step executor uses regex heuristics. A proper NLP-to-selector mapper would make it more reliable across diverse UIs
- Screenshot diffing β attach screenshots to the Slack alert as image attachments
- Multi-page requirement support β currently one Notion page per feature run; supporting linked pages would help larger teams
- Structured Notion DB support β the requirements fetcher is abstracted behind
getPageContent()β a DB adapter would be a clean swap
To contribute:
git checkout -b feat/your-feature
# make changes
npm test
git commit -m "feat: description"
# open a PRPlease run the full test suite before opening a PR.
MIT β use it, fork it, build on it.
Built with Claude API Β· Playwright Β· Notion API Β· Amplitude Β· Slack API