A workflow-driven AI coding automation service that turns Jira tickets into merge-ready pull requests. ai workflow polls your issue tracker for tickets assigned to AI, implements features end-to-end inside isolated Vercel Sandboxes, and delivers PRs for human approval — no manual intervention required.
Designed to work with Vercel infrastructure: bring your own API keys (Jira, GitHub, Slack, Anthropic) and deploy onto Vercel — Functions for the HTTP server, Workflows for durable orchestration, and Sandboxes for isolated agent execution.
- You move a Jira ticket to the "AI" column on your board
- ai workflow dispatches the ticket — instantly via the Jira webhook, or within ~1 min via the Vercel Cron poller as a fallback
- A durable Vercel Workflow runs the agent in phases (research → implementation) inside a single Vercel Sandbox per ticket
- The sandbox pushes commits directly to the feature branch, the ticket moves to "AI Review", and your team gets a Slack notification
If the ticket already has an open PR (review feedback), the same workflow re-runs and feeds the PR comments + conflict status into the agent's context. If the agent can't proceed without human input, it posts clarification questions on the ticket and moves it to Backlog.
flowchart TD
A["Jira ticket moved to AI column"] --> B{"Dispatch"}
B -- "webhook (instant)" --> D["agentWorkflow"]
B -- "cron poll (~1 min)" --> D
D --> E["fetchPRContext (existing PR?)"]
E --> F["createFeatureBranch (only if no PR)"]
F --> G["provisionSandbox + register sandbox"]
G --> P1["Phase 1: Research / Plan"]
P1 --> P1R{Research result?}
P1R -- "clarification_needed" --> CL["Post questions → Backlog → notify"]
P1R -- "failed / timeout" --> FB["Move to Backlog → notify failed"]
P1R -- "completed" --> P2["Phase 2: Implementation"]
P2 --> P2R{Impl result?}
P2R -- "clarification_needed" --> CL
P2R -- "failed / timeout" --> FB
P2R -- "implemented" --> PUSH["pushFromSandbox (git push --force from inside sandbox)"]
PUSH --> PUSHR{Push ok?}
PUSHR -- "no" --> FIX["fixAndRetryPush (lightweight fix agent)"]
FIX --> PUSHR
PUSHR -- "yes" --> PR["createPullRequest / findPR"]
PR --> MV["Move to AI Review → notify pr_ready"]
TD["teardownSandbox (always runs in finally)"]
MV -.-> TD
CL -.-> TD
FB -.-> TD
R["Reconciler (every poll)"] -.-> R1["Stale claims (>5 min)"]
R -.-> R2["Finished runs"]
R -.-> R3["Orphaned runs (ticket left AI column)"]
R -.-> R4["Stale failed-ticket markers"]
| Component | Technology | Purpose |
|---|---|---|
| Server | Nitropack | HTTP server framework (Vercel Functions) |
| Orchestration | Vercel Workflows | Durable execution — survives crashes and deploys |
| Agent Execution | Vercel Sandbox | Isolated per-ticket environments |
| AI Agent | Claude Code or OpenAI Codex CLI | Coding agent (selectable via AGENT_KIND) |
| Issue Tracker | Jira REST API | Ticket lifecycle management |
| VCS | GitHub (Octokit) or GitLab (@gitbeaker/rest) | Branches, PRs/MRs, comments |
| Messaging | Chat SDK + Slack | Team notifications + /ai-workflow slash commands |
| Run Registry | Upstash Redis (via Vercel Marketplace integration) | Atomic claim/release for concurrent runs |
| Tracing (optional) | Arthur AI Engine | Per-run prompt/tool tracing inside the sandbox |
| Validation | Zod | Schema validation for config and agent output |
| Logging | Pino | Structured JSON logs |
| Testing | Vitest | Unit and E2E tests |
For installation, environment variables, and deployment instructions, see SETUP.md.
There is a single durable workflow — agentWorkflow in src/workflows/agent.ts — that handles both fresh tickets and review-fix re-runs. The branching happens at context-assembly time, not at the workflow level: if an open PR for blazebot/{ticket-key} already exists, its comments, check results, and conflict status are folded into the agent's input.
| Step | What happens |
|---|---|
fetchAndValidateTicket |
Fetches the ticket from Jira; aborts if it's no longer in the AI column |
fetchPRContext |
Looks up an open PR for blazebot/{ticket-key}; returns comments, check results, conflict status (or null for fresh tickets) |
createFeatureBranch |
Only when there's no existing PR — creates/resets blazebot/{ticket-key} from the base branch |
fetchAttachments |
Downloads ticket attachments (size/count limited by ATTACHMENT_* env vars) |
ensureArthurTaskForTicket |
Optional — creates an Arthur trace task when GENAI_ENGINE_* is configured |
resolveAgentKindOverride |
Per-ticket override via labels (e.g. agent:codex); falls back to AGENT_KIND |
provisionSandbox |
Provisions a Vercel Sandbox, installs the agent CLI + skills, configures auth + Arthur tracer |
registerTicketSandbox |
Pins the sandbox id to the ticket in Redis so cleanup paths can stop it by id |
writeAttachments |
Writes downloaded attachments under /tmp/attachments/ inside the sandbox |
| Phase 1 — Research/Plan | setCommitGuardStep(false) → planPhaseStep("research") → writeAndStartPhase → pollUntilDone (20 min) → collectPhase → parseResearchStep. Result is completed, clarification_needed, or failed |
| Phase 2 — Implementation | setCommitGuardStep(true) → planPhaseStep("impl", AGENT_SCHEMA) → writeAndStartPhase → pollUntilDone (35 min) → collectPhase → parseAgentOutputStep |
pushFromSandbox |
Injects the VCS token into the sandbox's git remote (after the agent process is dead) and runs git push --force from inside the sandbox |
fixAndRetryPush |
Fallback: if the push is rejected (e.g. pre-receive hook), spawns a lightweight fix agent in the same sandbox, then retries the push once |
createPullRequest / findPRForBranch |
Opens a new PR (no prior PR) or re-fetches the existing PR (review-fix path) |
moveTicket → notifyTicket("pr_ready") |
Moves the ticket to "AI Review" and sends the Slack notification with the usage report |
unregisterRun |
Removes the ticket from the Redis run registry |
teardownSandbox |
Always runs in finally — destroys the sandbox regardless of outcome |
If either phase returns clarification_needed, the workflow posts numbered questions as a Jira comment, moves the ticket to Backlog, and emits a needs_clarification Slack event. If a phase fails or times out, the ticket is moved to Backlog with a failed event.
A third "Review" phase is implemented in
agent.tsbut gated behindENABLE_REVIEW_PHASE(defaultfalse). When enabled, it runs after Phase 2 — the agent self-reviews its diff and fixes issues before push (15 min poll cap,REVIEW_SCHEMAfor structured output).
Each agent run gets a fresh, isolated Vercel Sandbox — a Firecracker microVM with no access to production infrastructure or other tickets.
| Input | How it's provided |
|---|---|
| Repository source code | Cloned via git source at the feature branch (shallow depth=1); unshallowed before push if needed |
| Auth env vars | ANTHROPIC_API_KEY (Claude) or CODEX_API_KEY / CODEX_CHATGPT_OAUTH_TOKEN (Codex) — written to /tmp/agent-env.sh (mode 0600) and sourced by each phase script |
| Model | CLAUDE_MODEL or CODEX_MODEL baked into the phase wrapper script |
| Per-phase input | /tmp/research-requirements.md and /tmp/impl-requirements.md — assembled by assembleResearchPlanContext / assembleImplementationContext |
| Attachments | Written to /tmp/attachments/<filename> |
| Git identity | git config user.name / user.email from COMMIT_AUTHOR / COMMIT_EMAIL (or auto-derived from the GitHub App when unset) |
| Agent CLI | @anthropic-ai/claude-code (Claude) or @openai/codex (Codex), installed globally |
| Skills | Installed via npx skills add ... -g --agent claude-code codex --copy to both ~/.claude/skills/ and ~/.agents/skills/. Currently only frontend-design is in GLOBAL_SKILLS |
| Arthur tracer (optional) | Python tracer + ~/.claude/arthur_config.json + hook entries in ~/.claude/settings.json |
The sandbox runs on Node.js 24 with a configurable timeout (JOB_TIMEOUT_MS, default 30 minutes). On Vercel, OIDC authenticates the sandbox automatically. For local dev, explicit VERCEL_TOKEN / VERCEL_TEAM_ID / VERCEL_PROJECT_ID are needed.
Each phase has its own wrapper script (/tmp/{phase}-wrapper.sh) that sources /tmp/agent-env.sh and pipes the phase input into the agent CLI:
- Claude (
buildPhaseScriptinsrc/sandbox/agents/claude.ts):cat /tmp/{phase}-requirements.md | claude \ --print --model '<model>' --dangerously-skip-permissions --output-format json \ [--json-schema '<AGENT_SCHEMA>'] \ > /tmp/{phase}-stdout.txt 2>/tmp/{phase}-stderr.txt - Codex (
buildPhaseScriptinsrc/sandbox/agents/codex.ts) usescodex exec --model … --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --jsonwith--output-schemafor structured output.
The script ends by writing a sentinel file (/tmp/{phase}-done). The workflow polls every 30 seconds via checkPhaseDone and suspends between polls — durable across redeploys.
The implementation phase enforces the structured contract:
{
"result": "implemented | clarification_needed | failed",
"summary": "What was done",
"questions": ["Question 1", "Question 2"],
"error": "What went wrong"
}A commit-guard stop hook (toggled per phase via setCommitGuardStep) blocks the agent from exiting with uncommitted changes. Phase 1 has it disabled (research only — no commits expected); phase 2 enables it so the implementation phase can't return result: "implemented" while leaving the working tree dirty.
ai workflow pushes from inside the sandbox, but only after the agent process has exited. The flow in src/sandbox/poll-agent.ts:
- Verify commits exist — compare the saved
/tmp/.pre-agent-shato the currentHEAD. If unchanged, the workflow fails the run with "Agent reported success but made no commits." - Inject the token —
git remote set-url origin <auth-url>. The agent process is already dead at this point and never sees the token. - Unshallow if needed — shallow clones miss shared ancestry with
main, which breaks PR creation. - Push —
git push --force origin HEAD:refs/heads/{branch}(force-push is safe;blazebot/*branches have no concurrent pushers).
If the push is rejected (e.g. by a remote pre-receive hook), fixAndRetryPush strips the token, spawns a smaller fix agent in the same sandbox with the push error as context, lets it commit fixes, then re-injects the token and retries the push once.
For fresh tickets, the workflow opens a PR via the VCS adapter (octokit.pulls.create() for GitHub, @gitbeaker/rest for GitLab):
- Head:
blazebot/{ticket-key} - Base:
GITHUB_BASE_BRANCH/GITLAB_BASE_BRANCH(defaultmain) - Title: the ticket title
For tickets that already had a PR (the review-fix path), no new PR is created — the existing PR is updated by the force-push and re-fetched via findPRForBranch.
The sandbox is always destroyed after each run (in a finally block), whether the agent succeeded, failed, or timed out. Every run starts and ends with a clean slate.
ai workflow uses an atomic claim pattern via Upstash Redis to prevent duplicate runs:
- When a ticket is dispatched, a
claiming:{timestamp}sentinel is set atomically (hsetnx) - Only one poller instance can win the claim — others see it's taken
- After the workflow starts, the sentinel is replaced with the real workflow run ID and the sandbox id is pinned to the ticket
- On every poll cycle, the reconciler (
src/lib/reconcile.ts) cleans up:- Stale claims older than 5 minutes (kills any orphaned sandbox + clears the sentinel)
- Finished runs still tracked in the registry (status
completed/failed/cancelled) - Orphaned runs for tickets that left the AI column — cancels the workflow and stops the sandbox
- Stale failed-ticket markers (cleared once the ticket leaves the AI column)
- A 30-second grace window guards against Jira's JQL index lag during column transitions
MIT