An end-to-end sample that triages a Gmail inbox, routes each thread to a GitHub repository, and waits for human approval before filing an issue and sending a reply. It is aimed at evaluators who want a runnable path across AgentKit (OAuth + tool execution) and an OpenAI-compatible LiteLLM gateway (set LITELLM_BASE_URL from your Scalekit project’s LLM Gateway in the dashboard).
You want to run the happy path locally (Gmail → dashboard proposal → approve → GitHub issue + Gmail reply + Slack update) and understand the moving parts quickly—without treating this repo as a production system.
Rough time and spend (typical):
- First successful run: about 30–45 minutes if you are creating a Scalekit project, three AgentKit connections, and a virtual LiteLLM API key for the first time (most of that is clicking through OAuth consent in Gmail, GitHub, and Slack).
- LiteLLM usage: put a small budget on the order of USD ~$2 on the virtual API key in the dashboard. That is usually enough to explore classification, routing, GitHub issue research, and drafting across a handful of threads (actual burn varies by model choice in
routing.yaml, thread length, and tool-call retries).
| Layer | What it does here |
|---|---|
| Scalekit AgentKit | OAuth and tool calls for Gmail, GitHub, and Slack under one opaque user identifier—your code does not manage refresh tokens or per-provider OAuth apps. |
LiteLLM (your LITELLM_BASE_URL) |
Each pipeline stage calls the gateway with a stage-specific model from routing.yaml—cheap/fast for classification and research, heavier models for drafting—without embedding upstream provider keys in this app. |
The process is polling-based: on an interval, the app pulls new Gmail threads, runs LLM stages + deterministic routing, stores a pending proposal in SQLite, posts to Slack, and serves a localhost dashboard for approve/reject.
Gmail poll
│
├─[classify] model from routing.yaml → { category, severity, keywords }
├─[route] keyword match against routing.yaml
├─[research] model + github_search_issues (tool loop)
├─[tiebreak] model (only when multiple repos tie)
├─[draft] model (stronger model for security/legal categories)
├─[notify] Slack message with proposal summary
└─[dashboard] http://localhost:3000 → Approve: file issue + send reply + update Slack
Reject: discard (no side effects)
Stage → model mapping lives entirely in routing.yaml; the code reads those names at startup.
Use these canonical guides while you configure the dashboard:
| Topic | Documentation |
|---|---|
| AgentKit overview | AgentKit overview |
| Create connections | Configure a connection |
| OAuth scopes | Scopes and permissions |
| GitHub connector | GitHub |
| Gmail connector | Gmail |
| Slack connector | Slack |
| Authorization links | Authorize a user |
| Cookbook (this flow on docs.scalekit.com) | Triage a Gmail inbox with AgentKit and the LiteLLM gateway |
To discover additional stable URLs (or verify paths after docs moves), use the public sitemap: https://docs.scalekit.com/sitemap-0.xml (if that URL returns an error, open docs.scalekit.com and search from there).
- Node.js ≥ 24 (this sample uses built-in
node:sqlite). - A Scalekit account and permission to create a project.
- Google, GitHub, and Slack identities you can use for OAuth (workspace policies allowing those apps).
curl(or any HTTP client) to sanity-check the LiteLLM gateway.
This flow expects an interactive terminal (stdin “press Enter” between connector authorizations). Headless CI is out of scope for this demo.
- Sign up at app.scalekit.com and create a project.
- Under AgentKit → Connections, create three connections: Gmail, GitHub, and Slack.
- Copy the exact Connection name for each (it can differ from the slug, e.g.
gmail-prodinstead ofgmail). You will paste those into.env.
GitHub OAuth scopes (important): when you configure the GitHub connection, ensure the granted OAuth scopes include repo (or the dashboard equivalent your org uses for repository read/write, including private repos you expect to triage). Without sufficient repository scope, issue search, issue create, or updates can fail in ways that look like “random” tool errors. See Scopes and permissions and the GitHub connector if your dashboard labels differ from the classic repo scope name.
- Copy your Environment URL, Client ID, and Client Secret from project settings.
In the Scalekit dashboard, open LLM Gateway and create a virtual API key. Attach a small spend limit if your team uses budgets—for a first pass, ~USD $2 on the key is usually enough to run several end-to-end threads.
Verify the gateway responds:
# Load the same values you put in .env (example: set -a; source .env; set +a)
curl -H "Authorization: Bearer $LITELLM_API_KEY" \
"$LITELLM_BASE_URL/v1/models"You should see model IDs in the JSON. Align the names under routing.yaml → models: with what this endpoint returns.
cp .env.example .env
# Edit .env with your valuesSecrets: keep .env out of git and off screen recordings. Never commit real credentials.
SCALEKIT_ENV_URL=https://your-project.scalekit.cloud
SCALEKIT_CLIENT_ID=skc_...
SCALEKIT_CLIENT_SECRET=sks_...
# Exact Connection names from AgentKit → Connections
GMAIL_CONNECTION_NAME=gmail
GITHUB_CONNECTION_NAME=github
SLACK_CONNECTION_NAME=slack
# Base URL from your project → LLM Gateway (OpenAI-compatible root, no trailing slash)
LITELLM_BASE_URL=https://YOUR_LLM_GATEWAY_HOST
LITELLM_API_KEY=sk-...
On first run the app creates DATA_DIR/identifier.txt with a stable UUID. That value ties Gmail, GitHub, and Slack to the same logical user. Do not delete or edit that file casually—doing so orphans prior connector authorizations for this data directory.
npm install
npm run devFirst run — connector authorization: the CLI walks Gmail → GitHub → Slack in order. For each connector it prints a magic link. Open it in a browser, complete OAuth, return to the terminal, and press Enter when prompted.
The HTTP dashboard does not come up until all three connectors report active. In src/index.ts, setupConnectors(...) finishes before the Express server binds to localhost. Wait for:
All connectors active.
poller started {"intervalMs":5000}
dashboard listening on http://localhost:3000
Only then is http://localhost:3000 guaranteed to be listening.
With npm run dev still running, send any email to the Gmail account you connected—even a short “test” message to yourself. The poller runs on POLL_INTERVAL_MS (default 5 s). Within roughly one interval you should see log lines for processing thread followed by Slack + a pending row in the dashboard.
You do not need a special subject line for the first smoke test; routing is driven by your routing.yaml keywords plus the classifier output.
Open http://localhost:3000. A card shows classification, routed repo, related issues, and drafts.
- Approve: creates the GitHub issue (if enabled), sends the Gmail reply (if enabled), updates Slack.
- Reject: marks the proposal rejected; no GitHub issue and no email send.
Extending the sample: to add repos, connectors, or pipeline stages after you have the baseline running, continue with GUIDE.md.
models:
classify: claude-haiku-4-5
research: claude-haiku-4-5
tiebreak: claude-sonnet-4-6
draft_default: claude-sonnet-4-6
draft_sensitive: claude-opus-4-6 # when category is security or legal
repos:
- name: owner/repo
keywords: [keyword1, keyword2]
labels: [label1, label2]
slack_channel: "#channel-or-user-id"
default:
name: owner/fallback-repo
labels: [needs-triage]
slack_channel: "#triage"| Variable | Description |
|---|---|
SCALEKIT_ENV_URL |
Your Scalekit environment URL |
SCALEKIT_CLIENT_ID |
OAuth client ID |
SCALEKIT_CLIENT_SECRET |
OAuth client secret |
GMAIL_CONNECTION_NAME |
Exact Connection name for Gmail |
GITHUB_CONNECTION_NAME |
Exact Connection name for GitHub |
SLACK_CONNECTION_NAME |
Exact Connection name for Slack |
LITELLM_BASE_URL |
Required. OpenAI-compatible LiteLLM base URL from LLM Gateway in the dashboard (set in .env; do not commit real values) |
LITELLM_API_KEY |
Virtual API key from the dashboard |
POLL_INTERVAL_MS |
Gmail poll interval in ms (default 5000) |
PORT |
Dashboard port (default 3000) |
DATA_DIR |
SQLite directory (default ./data) |
(file identifier.txt) |
Auto-created UUID under DATA_DIR; ties connectors—do not rotate casually |
src/
index.ts Entry — connector auth, poller, web server
config.ts Env + routing.yaml validation
tools/
auth.ts Per-connector authorization helpers
agentkit.ts Thin wrapper around executeTool
lib/
litellm.ts OpenAI-compatible client → gateway
log.ts Pino logger
store/
schema.sql SQLite schema
db.ts Database access
pipeline/
ingest.ts Gmail polling + cursor
classify.ts Classification stage
route.ts Keyword routing
research.ts GitHub issue search loop
tiebreak.ts LLM tiebreak
draft.ts Issue + reply drafting
notify.ts Slack notification
act.ts Approve actions (issue + reply + Slack)
web/
server.ts REST API + static UI
views/ Dashboard (vanilla JS)
AgentKit: client.actions.executeTool(...) runs provider tools with Scalekit-managed tokens. Your process passes a connector name, tool name, tool input, and the same identifier string for Gmail, GitHub, and Slack.
LiteLLM: src/lib/litellm.ts issues chat.completions calls to LITELLM_BASE_URL using LITELLM_API_KEY. Each stage selects a model alias from routing.yaml. For category: security or category: legal, the draft stage uses draft_sensitive instead of draft_default.
- Connection name mismatch:
GMAIL_CONNECTION_NAME/GITHUB_CONNECTION_NAME/SLACK_CONNECTION_NAMEmust equal the Connection name in the dashboard, not a guess likegmailunless that is literally what you created. - GitHub failures: confirm the GitHub connection includes repository scope appropriate for private repos (see
reponote above). - Model not found: re-run the
curl .../v1/modelscheck and updaterouting.yamlmodel strings to match gateway IDs. - Dashboard never loads: wait until logs show All connectors active—the server starts only after all three OAuth flows succeed.
Deeper extension and debugging patterns live in GUIDE.md.
Use a single checklist for both “what the sample does today” and “what you would change before production.”
| Demo behavior today | What you would harden for real workloads |
|---|---|
One opaque identifier per DATA_DIR; single-operator mental model |
Per-tenant identifiers, rotation policies, and auditable mapping to real users |
| SQLite on local disk | Managed database, backups, encryption at rest, retention policy |
| Poll Gmail on a fixed interval | Webhooks or history APIs where available; idempotency and backoff |
| localhost-only Express listener | Authenticated hosted UI, TLS, CSRF/session controls, network segmentation |
Secrets in .env |
Secret manager, short-lived credentials, separate config per environment |
| Broad Slack + email side effects after one click | Role-based approval, dry-run mode, second-person review for external replies |
| English-centric prompts and fixtures | Internationalization, redaction rules, attachment policies |
.envholds live credentials; treat it like production secrets.identifier.txtis a durable join key across connectors for this data directory—treat loss or manual edits as a security and availability incident for that operator.- The dashboard binds to
localhostby default—remote access requires an explicit tunnel or code change; do not expose it unintentionally. - SQLite stores thread metadata and proposal payloads from Gmail—encrypt disk or wipe
DATA_DIRon shared machines.
See GUIDE.md for recipes (new connector, new repo row, swap models, add a pipeline stage).
This repository does not currently include a LICENSE file. Add one before you redistribute or fork publicly.
Run npm test and npm run typecheck to validate changes locally (tests mock AgentKit and LiteLLM; no network).