End-to-end instructions for deploying ai-workflow to your own Vercel account. Read the README first for architectural context.
- Prerequisites
- Provision external accounts
- Clone the repo and link to Vercel
- Install the Neon Postgres marketplace integration
- Configure environment variables
- Deploy to Vercel
- Register the Jira webhook
- Register the GitHub webhook (post-PR gate)
- Register the Slack slash command
- Smoke test the deployment
- CI / GitHub Actions
- Optional integrations
- Troubleshooting
Local toolchain:
| Tool | Version | Install |
|---|---|---|
| Node.js | 20+ | https://nodejs.org |
| pnpm | 10+ | npm i -g pnpm |
| Vercel CLI | latest | npm i -g vercel@latest |
| Git | 2.40+ | https://git-scm.com |
Accounts you must own:
- Vercel — Pro plan recommended (Cron Jobs, Sandbox, Workflow are paid features on Hobby).
- Atlassian Jira Cloud — admin access on the project to manage columns, transitions, and webhooks.
- GitHub or GitLab — admin on the target repository (PR + branch creation).
- Slack workspace — admin to install a custom app and register slash commands.
- Anthropic or OpenAI — API key for the agent runtime.
- Neon Postgres — installed via Vercel Marketplace in step 4.
Do these in any order — you'll paste the resulting values into Vercel in step 5.
ai-workflow authenticates to Jira as an Atlassian service account — a machine identity managed in the organization admin, with no human login. Tokens are Bearer-style and routed through api.atlassian.com/ex/jira/{cloudId}. Don't use a personal API token from a real user account: rotation, audit, and least-privilege all break down when the bot shares identity with a human.
Create the service account (requires Atlassian org admin):
- Go to https://admin.atlassian.com → pick your organization → Directory → Service accounts.
- Create service account → name it (e.g.
ai-workflow) → grant product access to Jira only.
Generate a scoped API token:
-
Back in admin.atlassian.com → Directory → Service accounts, open the account you just created → API tokens → Create credentials. Give it a label (e.g.
ai-workflow-prod) and pick these two classic scopes:Scope Covers read:jira-workGET /issue/{id}(summary, description, comments, labels, status, project, attachments),GET /issue/{id}/transitions,GET /search/jql, attachment downloadwrite:jira-workPOST /issue/{id}/comment,POST /issue/{id}/transitions(move ticket) -
Copy the token immediately (it's shown once) →
JIRA_API_TOKEN.
Scope ≠ project permissions. The token's scopes (
read:jira-work,write:jira-work) gate which API categories the token can call — butwrite:jira-workcovers destructive endpoints likeDELETE /issue/{id}too. The second gate is the project's Permission Scheme in Jira itself: the service account is a regular Jira user, and Jira filters every request by that user's permissions on the project.Concretely: leave the service account in
jira-users(default) and make sure your project's Permission Scheme does not grantDelete Issues,Delete All Comments,Delete All Attachments,Administer Projects, orManage Sprintstojira-usersorAny logged in user. Atlassian'sDefault Permission Schemeships this way out of the box — verify at<site>/secure/admin/ViewPermissionSchemes.jspa→ findDefault Permission Scheme→ click Permissions. ADELETEfrom the bot will then return403 You do not have permission to delete issues in this project.even though the token scope allows the call. If you need stricter isolation (e.g. hide the bot from other projects), create a dedicated group + dedicated scheme — otherwise the default should be sufficient.
Capture the rest of the config:
-
Note your Atlassian instance URL (e.g.
https://your-domain.atlassian.net) →JIRA_BASE_URL. -
Open the project ai-workflow will operate on. Note its key (e.g.
AWT) →JIRA_PROJECT_KEY. -
On the project board, identify the three columns ai-workflow uses. Create them if they don't exist:
COLUMN_AI— tickets assigned to the agent (default:AI)COLUMN_AI_REVIEW— completed tickets pending human review (default:AI Review)COLUMN_BACKLOG— tickets bounced back for clarification (default:Backlog)
-
Optional but recommended: capture stable Jira transition IDs for workflow moves:
JIRA_BACKLOG_TRANSITION_ID— transition back toCOLUMN_BACKLOGJIRA_AI_REVIEW_TRANSITION_ID— transition toCOLUMN_AI_REVIEW
These avoid relying on localized transition display names. You can fetch IDs from
GET /rest/api/3/issue/<KEY>/transitionswhile the ticket is in the source status. -
Generate a webhook secret to authenticate Jira → Vercel deliveries:
openssl rand -hex 32
Save as
JIRA_WEBHOOK_SECRET. You'll register the webhook itself in step 7.
Without a webhook, dispatch falls back to the 1-minute cron poll — workable for testing, sluggish in production.
GitHub (GitHub App — required):
ai-workflow authenticates to GitHub via a GitHub App. The App scopes the bot to a single installation, commits as <app-slug>[bot], and lets you rotate the private key without touching a human account. See docs/GITHUB-APP-SETUP.md for the full step-by-step walkthrough — the short version:
-
Go to https://github.com/settings/apps → New GitHub App.
-
Set Webhook → Active to on, set the URL to
https://<your-deployment>/webhooks/github(placeholder ok pre-deploy), and generate a secret (openssl rand -hex 32) →GITHUB_WEBHOOK_SECRET. The post-PR gate verifiesX-Hub-Signature-256on every delivery. -
Under Repository permissions, grant exactly:
Permission Access Why Contents Read & write Clone the repo, push commits Pull requests Read & write Create PRs, fetch PR data Issues Read & write PR review comments live on the issues API Checks Read & write Read CI check results + create post-PR gate check runs Actions Read-only Read workflow run status Metadata Read-only Mandatory, auto-included Leave every other permission at No access.
-
Under Subscribe to events, enable Pull request (drives the post-PR gate on
opened/synchronize/reopened), Check run (drives thetrigger_pr_checks_failedworkflow trigger), and Pull request review (drives thetrigger_pr_reviewworkflow trigger). The last two need no extra permission grant:ChecksandPull requestsare already Read & write from step 3. Leave everything else unchecked. Seedocs/GITHUB-APP-SETUP.md§5 for the full rationale per event. -
Choose Only on this account for installation scope, create the app, then Install App on the target repo's owner and select that one repo. If you change permissions later, every installed repo will need a one-click re-acceptance from a repo admin.
-
From the app settings page, capture:
- App ID →
GITHUB_APP_ID - Generate a private key → download the
.pem. Base64-encode the file contents (base64 -i app.pem | tr -d '\n') →GITHUB_APP_PRIVATE_KEY. - From the Installations list, the numeric installation ID →
GITHUB_INSTALLATION_ID.
- App ID →
-
Note the target repo's
ownerandname→GITHUB_OWNER,GITHUB_REPO. -
Note the base branch (usually
main) →GITHUB_BASE_BRANCH.
The legacy
GITHUB_TOKENPAT path was removed —VCS_KIND=githubnow requires the App vars above.env.tsenforces this at boot, includingGITHUB_WEBHOOK_SECRET.
GitLab:
For GitLab.com single-project setup, see docs/GITLAB-SETUP.md. The short version:
- Create a Project Access Token when available, or a dedicated bot/service-account PAT if project tokens are unavailable. Grant
apiandwrite_repositoryscopes →GITLAB_TOKEN. - Give the token identity enough project access to create branches, open MRs, push commits, and create commit statuses. Maintainer is simplest. Prefer leaving
blazebot/*unprotected; if protected, the token identity must be allowed to push and force-push that pattern. - Set the namespace/project path, for example
my-group/my-repo→GITLAB_PROJECT_ID. Numeric project IDs are not supported because sandbox clone/push needs a path. - Generate a random webhook secret →
GITLAB_WEBHOOK_SECRET. - Note the base branch (usually
main) →GITLAB_BASE_BRANCH.
The Slack app powers two things: notifications (run start, success, failure messages posted to a channel) and the /ai-workflow slash command (registered later in step 8).
Create the app:
-
Go to https://api.slack.com/apps → Create New App → From scratch. Name it (e.g.
ai-workflow) and pick the workspace. -
Under OAuth & Permissions → Bot Token Scopes, add exactly:
Scope Why chat:writePost notifications to the channel commandsRegister and respond to the /ai-workflowslash commandDon't add
chat:write.publicunless you want the bot to post in channels it isn't a member of — keeping it out forces the explicit invite below, which is what you want. -
Click Install to Workspace and approve. Copy the Bot User OAuth Token (
xoxb-...) →CHAT_SDK_SLACK_TOKEN. -
Under Basic Information → App Credentials, copy Signing Secret →
SLACK_SIGNING_SECRET. This authenticates incoming slash-command requests.
Wire up notifications:
- Pick (or create) the channel where ai-workflow should post — e.g.
#ai-workflowor your team's engineering channel. Public is simplest; private works as long as you invite the bot. - In Slack, open the channel → Channel details → Integrations → Add apps → select the ai-workflow app you just installed. (Or run
/invite @ai-workflowin the channel.) Without this the bot's posts will fail withnot_in_channel. - Right-click the channel → View channel details → copy the channel ID at the bottom (looks like
C0123456789) →CHAT_SDK_CHANNEL_ID. - Optional: choose a display name →
CHAT_SDK_BOT_NAME(defaultblazebot). This is what users see as the message author. - Optional: restrict who can invoke the slash command by setting
SLACK_ALLOWED_USER_IDSto a comma-separated list of Slack user IDs (U0123…). When unset, anyone in the workspace can run it.
If you skip the Slack section entirely (
CHAT_SDK_SLACK_TOKENandCHAT_SDK_CHANNEL_IDunset), runs proceed silently — Jira and PRs still update, just no chat notifications.
The slash command itself is registered in step 8 (after you have a deployment URL). For the deeper walkthrough, see .claude/skills/init-slack/.
Pick one — controlled by AGENT_KIND.
Claude (default):
- Configure either a standard Console API key or a Claude Code OAuth token as
ANTHROPIC_API_KEY. The same credential is used for execution and pinned-CLI Harness Profile discovery. - Optionally pin a model:
CLAUDE_MODEL=claude-opus-4-8(default).
Codex:
AGENT_KIND=codexCODEX_API_KEY=sk-...(orCODEX_CHATGPT_OAUTH_TOKEN)- Optionally
CODEX_MODEL=gpt-5-codex.
To edit Harness Profiles for both providers, configure ANTHROPIC_API_KEY and
one Codex credential even when AGENT_KIND selects only one execution default.
The scheduled worker refresh populates both exact-version catalogs.
This repo is a pnpm workspace monorepo (see the Repository Layout section of the README). pnpm install from the root installs every app at once:
git clone <your-fork-or-this-repo>.git
cd ai-workflow
pnpm installThe two deployable apps — apps/worker (the bot) and apps/dashboard (the cockpit) — deploy as separate Vercel projects, each linked from its own directory. Link the worker now; the dashboard is optional and covered under Optional integrations → Dashboard.
cd apps/worker
vercel link
cd ../..vercel link walks you through selecting the team and either creating a new project or linking to an existing one. Running it from apps/worker produces apps/worker/.vercel/project.json and pins the project's Root Directory to apps/worker, so Vercel builds the worker (and picks up its vercel.json cron) rather than the repo root. The .vercel/ dir is gitignored — keep it out of source control.
If you instead create the project through the Vercel dashboard, set Project → Settings → Build & Development → Root Directory to
apps/workermanually.
ai-workflow uses Neon Postgres as its run registry and post-PR-gate store (atomic claim/release for concurrent runs, dedupe, locking). Tables are created automatically — migrations run during every deploy's build step.
- Open https://vercel.com/marketplace/neon and click Install.
- Connect it to the ai-workflow Vercel project.
- Critical: enable a separate branch per environment (development /
preview / production) when configuring the integration. Each environment's
DATABASE_URLmust point at its own Neon branch. The build fails with anenv_markererror if two environments share one branch — that guard protects the production run registry from preview deployments.
Verify:
vercel env ls | grep DATABASE_URLYou should see DATABASE_URL present for each environment. (vercel env ls
shows values as Encrypted, so it can't confirm branch isolation — use the
pull-and-compare check in .claude/skills/init-neon/ to verify each
environment points at its own Neon branch.)
Two paths — pick the one that matches your workflow.
Open Project → Settings → Environment Variables and add every required variable from the table below. Set scope to Production, Preview, Development unless noted otherwise.
The worker's vars belong to the worker's Vercel project, so run these from apps/worker (where you linked in step 3):
cd apps/worker
cp .env.example .env
# fill in values, then:
vercel env add JIRA_BASE_URL production
vercel env add JIRA_API_TOKEN production
# ... repeat| Variable | Purpose |
|---|---|
JIRA_BASE_URL, JIRA_API_TOKEN, JIRA_PROJECT_KEY |
Jira credentials (scoped service-account Bearer token) |
COLUMN_AI, COLUMN_AI_REVIEW, COLUMN_BACKLOG |
Jira status/display names for polling, webhooks, and fallback transition lookup |
JIRA_BACKLOG_TRANSITION_ID, JIRA_AI_REVIEW_TRANSITION_ID |
Optional stable transition IDs for Jira moves; recommended when Jira localizes transition names |
VCS_KIND |
Optional. Provider credentials are additive: configure GitHub, GitLab, or both in one deployment (a run can then mix repositories from both providers). Set VCS_KIND only to pin the legacy single-repo helpers to one provider; leave it unset in dual-provider deployments. |
GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_INSTALLATION_ID, GITHUB_OWNER, GITHUB_REPO |
If GitHub is configured (GitHub App auth) |
GITHUB_WEBHOOK_SECRET |
If GitHub is configured — signs pull_request webhook deliveries for the post-PR gate. Required in every environment (Production, Preview, Development) because the webhook fires on preview deployments too. Generate: openssl rand -hex 32. |
GITLAB_TOKEN, GITLAB_PROJECT_ID, GITLAB_BASE_BRANCH, GITLAB_WEBHOOK_SECRET |
If GitLab is configured — GitLab.com token with api + write_repository, namespace/project path, target branch, and merge request webhook secret. Generate: openssl rand -hex 32. |
ANTHROPIC_API_KEY |
Claude execution and Harness Profile capability discovery; accepts a standard API key or Claude Code OAuth token |
CODEX_API_KEY (or CODEX_CHATGPT_OAUTH_TOKEN) |
Codex execution or Harness Profile capability discovery |
DATABASE_URL |
Auto-injected by Neon integration |
BETTER_AUTH_SECRET |
Signing/encryption key for Better Auth (dashboard human login). At least 32 chars. Generate: openssl rand -base64 32. |
BETTER_AUTH_URL |
The worker's own base URL (no trailing slash) — Better Auth's baseURL. |
DASHBOARD_ORIGIN |
The dashboard deployment's origin, added to Better Auth trustedOrigins. |
DASHBOARD_AUTH_EMAIL |
Email of the single predefined dashboard admin (seeded at build; no registration UI). |
DASHBOARD_AUTH_PASSWORD |
Password for that admin. Changing it re-hashes on the next deploy. |
This is enough for password-only dashboard login. SSO and Resend are optional worker-side additions.
| Variable | Default | Purpose |
|---|---|---|
GITHUB_BASE_BRANCH |
main |
PR target branch |
CHAT_SDK_SLACK_TOKEN, CHAT_SDK_CHANNEL_ID |
unset | Slack bot. When unset, runs proceed silently (no notifications). |
CHAT_SDK_BOT_NAME |
blazebot |
Slack display name |
SLACK_SIGNING_SECRET |
unset | Required only if you register the /ai-workflow slash command. When unset, /webhooks/slack rejects all requests. |
SLACK_ALLOWED_USER_IDS |
empty (anyone) | Comma-separated user IDs allowed to run slash commands |
CRON_SECRET |
unset | Generate: openssl rand -hex 32. Without it, /cron/poll accepts unauthenticated callers — strongly recommended in production. |
JIRA_WEBHOOK_SECRET |
unset | Generate: openssl rand -hex 32. Without it, dispatch is cron-bound (1-min latency). |
CLAUDE_MODEL |
claude-opus-4-8 |
Anthropic model |
CODEX_MODEL |
gpt-5-codex |
Codex model |
MAX_CONCURRENT_AGENTS |
3 |
Parallel sandbox cap |
JOB_TIMEOUT_MS |
1800000 (30 min) |
Per-run timeout |
POLL_INTERVAL_MS |
300000 (5 min) |
Internal poll cadence |
COMMIT_AUTHOR, COMMIT_EMAIL |
unset on GitHub → auto-derived from the App (commits author as <app-slug>[bot]); GitLab falls back to ai-workflow-blazity / ai-workflow@blazity.com |
Optional override; set both or neither |
DASHBOARD_ORG_NAME, DASHBOARD_ORG_SLUG |
AI Workflow, ai-workflow |
Fixed dashboard organization display name and slug. Override before first auth bootstrap only. |
SSO_ISSUER, SSO_ALLOWED_DOMAIN, SSO_CLIENT_ID, SSO_CLIENT_SECRET |
unset | Optional SSO config. Set all four together, or leave all four unset for password-only login. |
RESEND_API_KEY, RESEND_FROM_EMAIL |
unset | Optional email delivery config. RESEND_API_KEY requires RESEND_FROM_EMAIL. |
RESEND_WEBHOOK_SECRET |
unset | Optional Resend webhook signing secret. Requires RESEND_API_KEY. |
AGENT_ALLOWED_REPOS |
empty (unrestricted, see below) | Comma-separated owner/repo allowlist of every repository the agent may read, branch, or open a PR on (case-insensitive, exactly one slash per entry). Fails open when empty. |
DASHBOARD_TRUSTED_ORIGINS |
empty (only DASHBOARD_ORIGIN is trusted) |
Extra origins trusted for dashboard login on top of DASHBOARD_ORIGIN, e.g. a preview deployment's stable alias. Comma-separated; each entry must be a full origin URL (scheme included) or startup validation fails. DASHBOARD_ORIGIN remains the canonical origin for links and SSO redirects. |
GITHUB_BOT_LOGIN, GITLAB_BOT_LOGIN |
unset (commented-review triggers for that provider are unavailable) | Provider-specific login of the bot's own VCS account. Required for every selected, configured provider when trigger_pr_review.on includes commented, so the bot cannot recursively trigger a run from its own review. For a GitHub App this is usually <app-slug>[bot]. |
VCS_BOT_LOGIN |
unset | Legacy fallback for a commented-review bot identity, accepted only when exactly one VCS provider is configured. Mixed GitHub/GitLab deployments require provider-specific logins. |
ENABLE_REVIEW_PHASE |
false |
Adds a self-review step (a review_agent block) to the built-in workflow templates, so the agent reviews and fixes its own diff before push. Only shapes new default definitions: once a definition is saved via the dashboard, that definition's own review_agent block presence controls this instead. |
ENABLE_LEAK_REVIEW |
false |
Screens the unpushed diff for secrets and sensitive data before Finalize pushes the branch, via a built-in leak_review block. Like the review flag, it only shapes built-in templates: once a definition is saved via the dashboard, that definition's own leak_review block presence controls this instead. |
ENABLE_REPO_MEMORY |
false |
Distills per-repository facts and lessons at the end of a successful run, injects them into agent prompts, seeds them from the manifest, and reads repository-authored .ai/memory documents. Unlike the two flags above, this one gates execution directly at every read and write, so turning it off stops the feature immediately without a deploy and leaves stored documents untouched and unread. |
ENABLE_ORG_MEMORY_PROMOTION |
false |
Promotes facts shared by two or more repositories of one owner into an org-scoped document. Has no effect unless ENABLE_REPO_MEMORY is also on. |
ENABLE_REPO_ROUTING_MEMORY |
false |
Remembers which repository a human resolved a ticket to (keyed by ticket label), so the "which repository?" question is asked once instead of every time. Has no effect unless ENABLE_REPO_MEMORY is also on. |
env.ts cross-validates at startup — missing required vars or wrong combinations (e.g. VCS_KIND=github without GITHUB_OWNER) crash the process with a precise error.
AGENT_ALLOWED_REPOS is the hard guard on which repositories the agent may ever touch. It both filters repository discovery and hard-guards branch/PR creation, so an off-list repo fails the run with Refusing to branch <repo>: not in AGENT_ALLOWED_REPOS rather than being silently skipped.
When it is empty or unset, there is no restriction: the agent may act on any repository your VCS App installation can reach. That is the intended default for the multi-repo product, but it is a fail-open default, so decide deliberately rather than inheriting it:
- Set it to pin runs to an explicit list. Strongly recommended for a first deploy, a demo, or any environment whose App installation is scoped to more than the repos you want touched. Example:
AGENT_ALLOWED_REPOS=your-org/your-repo,your-org/another-repo. - Leave it empty only when you genuinely intend every installed repository to be in scope. Restrict the App installation itself as well: the allowlist is defense-in-depth, not a substitute for installation scope.
Both failure modes are logged rather than silent: an empty effective allowlist logs a one-time warn on startup, and any entry that is not a valid owner/repo path logs an error naming it. Malformed entries are ignored individually, so a single typo cannot silently widen the allowlist to "all" as long as one valid entry remains.
Unlike most variables here, AGENT_ALLOWED_REPOS is read from process.env directly (in apps/worker/src/lib/repo-allowlist.ts) rather than through the validated env.ts singleton, so it is deliberately absent from env.ts and a malformed value will not crash startup.
Pre-PR checks (per-repo commands run before push/PR creation) are configured in the dashboard: Pre-PR checks in the cockpit sidebar. Admins and owners can edit; changes are versioned with one-click restore.
Run these from apps/worker (the linked worker project). The dashboard deploys separately — see Optional integrations → Dashboard.
vercelConfirm the preview URL works:
curl https://<preview-url>/healthvercel --prodOr push to your production branch if you've connected the Vercel Git integration — production deployments fire automatically.
- HTTP routes from
apps/worker/src/routes/— health, cron, webhooks, slash commands, and the read-only/api/v1/*API. - Vercel Workflow definitions — workflow state is managed by Vercel in production (no Postgres needed).
- Cron job from
vercel.json(* * * * *→/cron/poll) — activates automatically. Vercel injects theCRON_SECRETauth header.
Without this, ai-workflow only learns about ticket changes via the 1-minute cron poll.
- Go to Jira → System Settings → WebHooks (admin only) or use the Atlassian REST API.
- Create a webhook:
- URL:
https://<your-vercel-domain>/webhooks/jira - Secret: the
JIRA_WEBHOOK_SECRETvalue from step 5. Jira signs each delivery with HMAC-SHA256 in theX-Hub-Signatureheader; the handler atapps/worker/src/routes/webhooks/jira.post.tsverifies it withtimingSafeEqual. - Events:
jira:issue_updated(required). Addjira:issue_createdandcomment_createdif you want creates and comments to dispatch instantly. Answering clarification questions does not requirecomment_created: the answers are picked up when the ticket is moved back to the AI column (comment first, then move), and the cron poller is the backstop if that webhook is missed. - JQL filter (optional):
project = AWTto limit deliveries to the relevant project.
- URL:
- Save.
Verify by moving a test ticket into the AI column and watching the Vercel runtime logs.
The post-PR gate runs configurable checks against every PR/MR on the target repo. GitHub surfaces results as Check Runs; GitLab.com surfaces them as commit statuses on the MR head SHA. The provider webhook is what triggers it.
If you followed docs/GITHUB-APP-SETUP.md in step 2.2, the App is already configured with the right webhook URL, secret, permissions, and event subscription. This section is the post-deploy verification — and the place to fix things if any of the above were skipped.
- Update the webhook URL if you used a placeholder during App creation. In the App settings (
https://github.com/settings/apps/<your-app>or via the org's developer settings), set Webhook URL tohttps://<your-vercel-domain>/webhooks/github. - Confirm the App has the right permissions and event subscription:
- Repository permissions → Checks: Read & write, Pull requests: Read & write
- Subscribe to events → Pull request, Check run, Pull request review (all checked). See
docs/GITHUB-APP-SETUP.md§5. Without Check run and Pull request review, thetrigger_pr_checks_failedandtrigger_pr_reviewworkflow triggers never fire.
- Re-accept on every installed repo if you changed permissions or events after the initial install. A repo admin opens
https://github.com/organizations/<ORG>/settings/installations/<INSTALLATION_ID>and clicks "Review request" → "Accept". Until accepted, the new permissions and events are inert and the gate webhook stays silent. - Confirm
GITHUB_WEBHOOK_SECRETis set in Vercel (step 5) and matches the value pasted into the App's webhook config. A mismatch returns 401 on every delivery — visible in the App's Advanced → Recent Deliveries tab. - Tune
post-pr-gate.yamlatapps/worker/post-pr-gate.yamlif the defaults don't fit (the build copies it fromnitro.options.rootDir, which isapps/worker/, not the monorepo root; a file placed at the monorepo root is silently ignored and the built-in default keeps applying). The default config runs onblazebot/*branches only, skips drafts, and runs a singlecode-hygienestep as advisory (onFailure: continue). Steps are defined inapps/worker/src/post-pr-gate/steps/. (The deployment skills directory ships by a twin mechanism but follows the opposite location rule: see §12 Agent skills shipped with this deployment.)
For GitLab.com, configure the project webhook instead: see docs/GITLAB-SETUP.md. The webhook URL is https://<your-vercel-domain>/webhooks/gitlab, the Secret token field must match GITLAB_WEBHOOK_SECRET, and Merge request events, Pipeline events (the Pipeline Hook), and Comments (the Note Hook) are required. Pipeline events drive trigger_pr_checks_failed; Comments can drive only the commented variant of trigger_pr_review. GitLab Request Changes, with or without a summary, is unsupported until GitLab emits a reliable event that distinguishes that transition. Do not use GitLab's newer Signing token flow until the worker implements signing-token verification.
For GitHub, verify by opening a manual PR titled feat: smoke check against the target repo (any blazebot/* branch — or set botPrsOnly: false in post-pr-gate.yaml to test from any branch). Within a few seconds you should see a blazebot / code-hygiene check run appear on the PR's head SHA and resolve.
For GitLab.com, verify by opening or updating a blazebot/* merge request and checking that the MR head commit shows a blazebot / ... commit status. See the smoke checklist in docs/GITLAB-SETUP.md.
- In your Slack app config, go to Slash Commands → Create New Command.
- Configure:
- Command:
/ai-workflow - Request URL:
https://<your-vercel-domain>/webhooks/slack - Short description:
Manage ai-workflow runs - Usage hint:
list | status <KEY> | cancel <KEY>
- Command:
- Save and reinstall the app to your workspace if Slack prompts you.
- Confirm
SLACK_SIGNING_SECRETis set in Vercel (step 5) —/webhooks/slackrejects requests with bad signatures.
Test in Slack:
/ai-workflow list
If you set SLACK_ALLOWED_USER_IDS, only those Slack user IDs can invoke the command — useful for limiting to your engineering team.
See
.claude/skills/init-slack/references/slash-commands.mdfor the full walkthrough.
curl https://<your-vercel-domain>/health
# → {"status":"ok","timestamp":"..."}curl https://<your-vercel-domain>/cron/poll
# → 401 Unauthorized
curl -H "Authorization: Bearer $CRON_SECRET" https://<your-vercel-domain>/cron/poll
# → 200 with the poll result- Create a test Jira ticket with a clear acceptance criterion (e.g. "add a
/pingroute returningpong"). - Move it to the AI column.
- Within ~1 minute (cron) or instantly (webhook), watch:
- Vercel logs — workflow starts, sandbox provisions.
- Jira ticket — moves to AI Review (success) or Backlog (clarification needed).
- Target repo — new branch
blazebot/<ticket-key>and an open PR. - Slack channel — notification fires.
Answering a clarification request. When a run needs input it posts the numbered questions (with suggested answers and a dashboard link) as a Jira comment, includes them in the Slack notification, and parks the ticket in Backlog. Answer it either in the dashboard (the run resumes immediately, as before) or by replying in a Jira comment and then moving the ticket back to the AI column: the move is the commit gesture (a comment on its own triggers nothing, and the bot's own comments are ignored), and the resumed run reads the comments as the answer. The paused run stays resumable for 7 days; after that the next move to AI starts the ticket over fresh.
If anything stalls, jump to troubleshooting.
Four workflows ship in .github/workflows/:
-
ci.yml— runs on pull requests againstmain/devand onmerge_groupevents. Thecijob runs typecheck + unit tests with no secrets. The merge-queue path additionally runse2e-orchestration → e2e-capacity → e2e-agentagainst the samee2eGitHub environment. -
e2e.yml— manualworkflow_dispatchwith two inputs:tier:orchestration|capacity|agent|all(defaultall).agent:claude|codex— passed asE2E_AGENT_KIND, only consumed by theagenttier.
Tiers and timeouts:
- orchestration — dispatch / cron / webhook (60 min).
- capacity — concurrency, claim/release, reconciler (30 min, gated on orchestration).
- agent — full ticket → PR run against real Jira + GitHub (120 min, gated on capacity).
-
prepare-artur-release.yml— manually generates reviewable, non-technical Artur release notes and can open a docs-only pull request. -
sync-artur-release.yml— after an approved release-note PR is merged, synchronizes the complete pinned application snapshot into a pull request inBlazity/ai-workflow-arthur. It preserves only that repository's.github/directory andrenovate.json; it does not call Vercel.
The E2E jobs need the production env
vars exposed as GitHub Actions secrets in the e2e environment (Repo Settings
→ Environments → e2e → Secrets). They additionally require E2E_BASE_URL,
E2E_GITHUB_APP_ID, E2E_GITHUB_APP_PRIVATE_KEY (base64-encoded PEM),
E2E_GITHUB_INSTALLATION_ID, E2E_GITHUB_OWNER, E2E_GITHUB_REPO, and
VERCEL_AUTOMATION_BYPASS_SECRET.
Release-note preparation uses a separate artur-release-preparation
environment restricted to protected main. Put the release GitHub App
credentials, optional ANTHROPIC_API_KEY, and one-time
ARTUR_INITIAL_BASE_SHA there rather than in unrestricted repository secrets.
The App is installed only on ai-workflow and ai-workflow-arthur; workflows
mint repository-scoped tokens. Production deployment and publication belong
to the Artur repository. See the
Artur release runbook.
apps/dashboard is a separate Next.js app that renders the worker's runs, KPIs, and eval health. It's read-only and entirely optional — the bot runs without it. It holds no integration credentials; it just calls the worker's gated /api/v1/* API server-side. Deploy it as its own Vercel project:
-
Link the dashboard from its directory (sets Root Directory to
apps/dashboard):cd apps/dashboard vercel link # create/select a SECOND project (e.g. ai-workflow-dashboard)
-
Set its env var (
apps/dashboard/.env.exampledocuments it):Variable Value WORKER_BASE_URLThe deployed worker's base URL, no trailing slash (e.g. https://<your-worker>.vercel.app).vercel env add WORKER_BASE_URL production
-
Deploy:
vercel --prod
The dashboard holds no worker secret. Human login is handled by the worker (Better Auth); the dashboard stores the worker-issued session token in a first-party httpOnly cookie and replays it server-side. Set DASHBOARD_ORIGIN on the worker to this dashboard's URL so Better Auth trusts it. Password-only mode needs no SSO vars; sign in at /login with DASHBOARD_AUTH_EMAIL / DASHBOARD_AUTH_PASSWORD. Optional SSO, Resend, and fixed organization vars belong on the worker project, not the dashboard project.
Skills are the repository-specific knowledge an agent loads on top of its prompt. Besides importing them from GitHub, a deployment can carry its own: put a skills/ directory at the root of the repository this deployment is built from, and every tenant gets its skills without any cross-organization GitHub App access, GitLab-only tenants included.
Location, and how it differs from the YAML rule. post-pr-gate.yaml is copied from nitro.options.rootDir, which is apps/worker/, and a copy at the monorepo root is ignored (§8 step 5). The skills directory is the opposite: it is copied from the monorepo root, because it belongs to the repository a tenant deploys, not to the worker app. In local development the worker runs with apps/worker as its working directory, so the runtime looks there first and then falls back to the repository root; a skills/ directory at the root therefore works under nitro dev as well as in a deployment.
Layout. One level of nesting, one directory per skill, each holding a SKILL.md:
skills/
review-checklist/
SKILL.md
references/api.md
release-notes/
SKILL.md
SKILL.md opens with YAML front matter, and the rest of the file is the Markdown the agent reads:
---
name: review-checklist
description: House rules the reviewer applies to every pull request.
---
# Review checklist
...A copyable example lives in docs/example-skill/SKILL.md. It sits under docs/ deliberately: a copy under skills/ would be discovered as a real skill of this deployment.
Contract. A directory that breaks any of these is skipped, with the reason shown in the dashboard and, since the same reader runs during the build, printed by the build:
| Rule | Limit |
|---|---|
| Nesting | exactly one level; a SKILL.md one level deeper is reported, not found |
SKILL.md |
a regular file, not a symlink |
name |
lowercase letters, digits and hyphens, 1 to 64 characters, starting and ending alphanumeric, unique across the directory |
description |
1 to 1024 characters, no leading or trailing whitespace |
| Files per skill | 500 |
| Bytes per file | 1 MiB |
| Bytes per skill | 5 MiB |
Bytes across skills/ |
25 MiB, because the directory is copied into every function bundle |
| Symlinks | rejected anywhere inside a skill |
| File modes | not carried: the function bundle does not preserve them, so every file is imported as a plain file and a deployment skill cannot ship an executable |
Using them. In the dashboard, open a harness profile, go to Skills and choose Add skills → This deployment. An imported skill is stored as an immutable artifact and the profile pins it by content hash, so a later deployment changes nothing on its own: after a redeploy that edits a skill, use Refresh on that skill and publish the profile again. The dashboard says whether the refresh moved the pin or found the same contents.
Build behaviour. pnpm build in apps/worker validates the directory before anything else runs and fails the build when any entry cannot ship, listing each path and reason. A deployment with no skills/ directory is fine and says so in one line.
Set both:
GENAI_ENGINE_API_KEY=...
GENAI_ENGINE_TRACE_ENDPOINT=https://your-arthur-host/api/v1/tracesThis enables per-run tracing and the optional arthur_injection_check block. The tracer is built into every sandbox via pnpm build:arthur-tracer during deploy.
Provider credentials are additive. To run GitLab only, provide GITLAB_TOKEN, GITLAB_PROJECT_ID, and GITLAB_WEBHOOK_SECRET (optionally VCS_KIND=gitlab to pin the legacy single-repo helpers); GITHUB_* vars may be removed. To run BOTH providers in one deployment, keep the GitHub App vars and add the GitLab vars side by side, leave VCS_KIND unset, and set per-provider bot logins (GITHUB_BOT_LOGIN, GITLAB_BOT_LOGIN) instead of the legacy VCS_BOT_LOGIN. A dual-provider deployment lists repositories from both providers in one catalog, and a single run can read and modify a mix of GitHub and GitLab repositories, publishing a PR or MR per changed repository. For GitLab.com setup, see docs/GITLAB-SETUP.md.
A generic authenticated webhook trigger lets an external system (Zendesk, Sentry, or anything that can sign an HTTP POST) start a workflow. It is off by default: the trigger_webhook block only appears in the editor palette once an encryption key is set.
| Variable | Value |
|---|---|
WEBHOOK_TRIGGER_ENCRYPTION_KEY |
64 hex characters (32 bytes). Generate with openssl rand -hex 32. |
The key is required for the feature but optional at boot: the worker starts without it, but the webhook trigger stays unavailable in the palette until it is set. It encrypts every endpoint's minted secret at rest, so treat it like any other production secret and set it on every environment that serves deliveries.
Registering a sender. Deploy the workflow, open its trigger_webhook block in the dashboard inspector, and copy the endpoint URL it shows (POST /webhooks/custom/<endpointId>). The block mints a per-endpoint secret and the inspector reveals it once. How the sender presents that secret depends on the auth scheme configured on the block:
hmac_sha256(default): sign the raw request body with the secret using HMAC-SHA256 and send the result as lowercase hex in the configured header (defaultX-Workflow-Signature). Sentry works by pointing this block at itsSentry-Hook-Signatureheader: that value is the same hex HMAC of the body, so only the header name changes.shared_token: send the secret verbatim in the configured header. This suits systems that only support a static custom header, such as a Zendesk webhook custom header.- Optional timestamp mode (replay protection): when enabled, sign
${unix_seconds}.${body}instead of the body alone and send the Unix timestamp in the timestamp header. A delivery outside the tolerance window is rejected as stale.
Senders that dictate their own secret (Sentry). Some systems sign with a secret they generate, not one this endpoint mints. A Sentry Internal Integration is the common case. Create the integration with a webhook pointed at the endpoint URL, set this block's Authentication to HMAC SHA-256 with the header name Sentry-Hook-Signature, then use Set secret in the inspector to paste Sentry's Client Secret. That makes our HMAC check use the same secret Sentry signs with, so the signatures match. Set secret replaces the current secret immediately, with no dual-accept window. Point the block's mapping dot-paths at Sentry's own payload shape; a path missing from a given event resolves to an empty string, so a partial payload never fails the delivery.
Rotating a secret. Rotate from the inspector panel. The previous secret keeps working for a 24 hour dual-accept window, so update the sender with the new secret within 24 hours; after that only the new secret is accepted.
Rekeying (WEBHOOK_TRIGGER_ENCRYPTION_KEY changed). The key encrypts endpoint secrets at rest, so changing it makes every existing secret undecryptable. Those deliveries then fail with decrypt_failed (recorded in the endpoint's rejection counters, and distinct from a bad signature, which is invalid_signature). To recover, rotate or revive each endpoint so its secret is re-minted under the new key, then update every sender with the freshly minted secret.
Security warning. The shipped
webhook-ticket-triagetemplate feeds external input (a support ticket body) straight through to an automatically opened pull request with no human gate. HMAC authenticates the channel, not the content: anyone who can file a ticket into a connected Zendesk or Sentry controls the agent's prompt, and therefore the PR it opens. Before pointing a real sender at this template, add a human-approval gate before theopen_prblock, or treat the workflow as triage-and-notify only.
| Symptom | Likely cause | Fix |
|---|---|---|
Startup crash: Invalid environment variables |
Missing required var or wrong cross-field combination | Read the error — env.ts lists exactly what's missing. |
/cron/poll returns 401 from Vercel Cron |
CRON_SECRET mismatch |
Ensure the var is set in Production environment. Redeploy after changing. |
| Tickets in AI column never get picked up | Cron disabled / webhook misregistered | Check Vercel → Project → Cron Jobs is enabled. Curl /cron/poll with the secret to test manually. |
| Workflow starts but sandbox fails to provision | Missing Vercel OIDC / Sandbox quota | On Vercel, OIDC is automatic. Check the project has Sandbox enabled (Pro plan). For local dev, set VERCEL_TOKEN/VERCEL_TEAM_ID/VERCEL_PROJECT_ID. |
Run registry: DATABASE_URL undefined |
Neon integration not connected to this project, or env var scoped to the wrong environments | Reinstall the Neon integration / check it's connected to this project. |
| Agent runs but PR isn't created | GitHub App missing Pull requests: Read & write or Contents: Read & write, App not installed on target repo, or wrong owner/repo | In the App settings, re-check Repository permissions and the Installations list. Verify GITHUB_OWNER/GITHUB_REPO point at the target repo, not this repo. |
| Post-PR gate never runs on opened PRs | App webhook inactive, Pull request event not subscribed, missing Checks: Read & write, or permission/event change not re-accepted on the installed repo |
App settings → Webhook: Active + URL set to /webhooks/github. Subscribe to Pull request. Bump Checks to read & write. Then have a repo admin re-accept the install at https://github.com/organizations/<ORG>/settings/installations/<INSTALLATION_ID>. Check Advanced → Recent Deliveries for 2xx responses. |
| Post-PR gate webhook returns 401 in Recent Deliveries | GITHUB_WEBHOOK_SECRET missing on the deployment or different from the value pasted into the App |
Set the var on every environment (production + preview + development) — preview deployments receive the webhook too. Redeploy after changing. Test by re-sending a delivery from the App's Recent Deliveries tab. |
| Slack messages don't arrive | Bot not in channel, or wrong CHAT_SDK_CHANNEL_ID |
Invite bot to the channel. Re-copy the channel ID. |
Slash command returns dispatch_failed |
Signing secret wrong, or app not reinstalled | Verify SLACK_SIGNING_SECRET. Reinstall the Slack app after adding the slash command. |
| Two pollers race on the same ticket | Stale claim sentinel | The reconciler clears claims older than 5 minutes on every poll — wait one cycle, or run pnpm exec tsx scripts/clear-run-registry.ts <ticket> from apps/worker (after vercel env pull .env.local). |
| Sandbox times out | Job too large for JOB_TIMEOUT_MS |
Increase to 60–90 minutes for complex tickets, or split the work. |
- Vercel runtime logs:
vercel logs <deployment-url>or Project → Logs. - Workflow runs: Project → Workflows in the Vercel dashboard — shows step-by-step state, failures, retries.
- Local logs: Pino prints structured JSON. Pipe through
pnpm dlx pino-pretty.
- Architecture and workflow internals → README.md
- Spec → docs/SPEC.md
- User stories → docs/user-stories.md
- Per-integration walkthroughs →
.claude/skills/init-*/(Jira, Slack, Neon, VCS, agent runtime)