DEV Community

Karim GAAD
Karim GAAD

Posted on Originally published at flowboard.dev

Build Your Own AI Coding Agent with FlowBoard's API

Introduction

In this tutorial, we'll build a Node.js agent that runs on a cron schedule, picks tasks from your FlowBoard workspace, uses Claude to write code, and creates GitHub pull requests -- all automatically. By the end, you'll have a working agent you can run nightly to chip away at your backlog.

If you use Cursor, their Automations feature can trigger agents from external events. But building your own gives you full control over the workflow.

The agent is roughly 80 lines of code. It connects three APIs -FlowBoard for task management, Anthropic for code generation, and GitHub for pull requests -into a single autonomous loop. Every piece is practical and copy-pasteable.

Prerequisites

  • FlowBoard account with a workspace and API key (Settings → API → Generate Key)
  • GitHub Personal Access Token with repo scope
  • Claude API key from Anthropic Console
  • Node.js 20+ installed
  • A Git repository cloned locally that the agent will work on

Project Setup

Create a new project and install the dependencies:

The Git and PRs panel on a FlowBoard task, holding the branch name an agent uses to link its commits back to the ticket

mkdir flowboard-agent && cd flowboard-agent npm init -y npm install @anthropic-ai/sdk octokit node-fetch node-cron dotenv Create a .env file with your credentials:

FLOWBOARD_API_URL=https://europe-west3-flowwboard.cloudfunctions.net/api FLOWBOARD_API_KEY=your-flowboard-api-key FLOWBOARD_WORKSPACE_ID=your-workspace-id ANTHROPIC_API_KEY=your-anthropic-api-key GITHUB_TOKEN=your-github-token GITHUB_OWNER=your-github-username GITHUB_REPO=your-repo-name REPO_PATH=/path/to/your/local/repo Connecting to FlowBoard The first step is fetching open tasks from your FlowBoard workspace. We'll query for unassigned tasks sorted by priority, limiting to 5 at a time to keep each run manageable.

import 'dotenv/config'; import fetch from 'node-fetch'; const API = process.env.FLOWBOARD_API_URL; const API_KEY = process.env.FLOWBOARD_API_KEY; const WORKSPACE = process.env.FLOWBOARD_WORKSPACE_ID; async function getOpenTasks() { const response = await fetch( \\${API}/getTasks?workspaceId=\${WORKSPACE}&status=open&assignee=unassigned&sort=priority&limit=5, { headers: { 'Authorization': \Bearer \${API_KEY}} } ); const data = await response.json(); return data.tasks; } This returns an array of tasks with their full details -title, description, acceptance criteria, bug context fields, file hints, and priority score.

Claiming a Task

Before working on a task, the agent claims it by setting the status to "in_progress" and assigning itself. This prevents other agents or humans from picking the same task.

async function claimTask(taskId) { const response = await fetch(\\${API}/updateTask, { method: 'PATCH', headers: { 'Authorization': \Bearer \${API_KEY}, 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, workspaceId: WORKSPACE, status: 'in_progress', assignee: 'ai-agent' }) }); return response.ok; } Understanding the Task The agent needs to parse the task into a structured prompt for Claude. We extract the title, description, bug context fields, file hints, and acceptance criteria to build a comprehensive prompt.

function buildPrompt(task) { let prompt = \You are an expert developer. Fix the following task in the codebase. ## Task: \${task.title} ## Description \${task.description} ; if (task.bugContext) { prompt += \ ## Bug Context - Reproduction Steps: \${task.bugContext.reproductionSteps || 'N/A'} - Error Message: \${task.bugContext.errorMessage || 'N/A'} - Environment: \${task.bugContext.environment || 'N/A'} - Affected Component: \${task.bugContext.affectedComponent || 'N/A'} ; } if (task.fileHints?.length) { prompt += \ ## File Hints \${task.fileHints.map(f => - \${f}\).join('\n')} ; } prompt += \ ## Instructions 1. Read the relevant files 2. Write a targeted fix that satisfies all acceptance criteria 3. Return ONLY the file changes as a JSON array: [{ "path": "file/path.ts", "content": "full file content" }] 4. Do not change files that don't need changes ; return prompt; } Writing Code with Claude We use the Anthropic SDK to send the task context to Claude and get back the code changes. The prompt includes the repository structure and the specific task context.

import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); async function generateFix(task, repoStructure) { const prompt = buildPrompt(task); const message = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 4096, messages: [{ role: 'user', content: \## Repository Structure \${repoStructure} \${prompt}}] }); // Parse the JSON response containing file changes const content = message.content[0].text; const jsonMatch = content.match(/\\[\\s*\\{[\\s\\S]*\\}\\s*\\]/); if (!jsonMatch) throw new Error('No valid JSON in response'); return JSON.parse(jsonMatch[0]); } The key to good results is the prompt. Including the repo structure, file hints, and acceptance criteria gives Claude the context it needs to write targeted, accurate fixes.

Creating a GitHub PR

Once we have the code changes, we use Octokit to create a branch, commit the files, and open a pull request. The PR description includes a link back to the FlowBoard task.

import { Octokit } from 'octokit'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); const owner = process.env.GITHUB_OWNER; const repo = process.env.GITHUB_REPO; async function createPullRequest(task, fileChanges) { // 1. Get the default branch SHA const { data: ref } = await octokit.rest.git.getRef({ owner, repo, ref: 'heads/main' }); const baseSha = ref.object.sha; // 2. Create a new branch const branchName = \fix/\${task.number}\${task.title.toLowerCase().replace(/\s+/g, '').slice(0, 40)}_ai-agent; await octokit.rest.git.createRef({ owner, repo, ref: \refs/heads/\${branchName}, sha: baseSha }); // 3. Create blobs and tree for each changed file const blobs = await Promise.all( fileChanges.map(file => octokit.rest.git.createBlob({ owner, repo, content: file.content, encoding: 'utf-8' }).then(({ data }) => ({ path: file.path, mode: '100644', type: 'blob', sha: data.sha })) ) ); const { data: tree } = await octokit.rest.git.createTree({ owner, repo, base_tree: baseSha, tree: blobs }); // 4. Create commit const { data: commit } = await octokit.rest.git.createCommit({ owner, repo, message: \fix: \${task.title} (FlowBoard #\${task.number}), tree: tree.sha, parents: [baseSha] }); await octokit.rest.git.updateRef({ owner, repo, ref: \heads/\${branchName}, sha: commit.sha }); // 5. Create pull request const { data: pr } = await octokit.rest.pulls.create({ owner, repo, title: \fix: \${task.title}, head: branchName, base: 'main', body: \## FlowBoard Task #\${task.number} \${task.description} --- Automated by FlowBoard AI Agent}); return pr.html_url; } Updating FlowBoard After creating the PR, the agent updates the FlowBoard task to "In Review" and adds a comment with the PR link. This keeps your board in sync without any manual intervention.

async function updateTaskWithPR(taskId, prUrl) { // Move task to "In Review" await fetch(\\${API}/updateTask, { method: 'PATCH', headers: { 'Authorization': \Bearer \${API_KEY}, 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, workspaceId: WORKSPACE, status: 'in_review' }) }); // Add comment with PR link await fetch(\\${API}/addComment, { method: 'POST', headers: { 'Authorization': \Bearer \${API_KEY}, 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, workspaceId: WORKSPACE, comment: \AI Agent created a pull request: \${prUrl}}) }); } Running on a Schedule You can run the agent on a schedule using node-cron for a local setup or GitHub Actions for a cloud-based approach.

Option 1: node-cron (local)

import cron from 'node-cron'; // Run every night at 1 AM cron.schedule('0 1 * * *', () => { console.log('Starting nightly agent run...'); runAgent(); }); Option 2: GitHub Actions (cloud) name: FlowBoard AI Agent on: schedule: - cron: '0 1 * * *' # Every night at 1 AM UTC workflow_dispatch: # Manual trigger jobs: run-agent: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: node agent.js env: FLOWBOARD_API_URL: \${{ secrets.FLOWBOARD_API_URL }} FLOWBOARD_API_KEY: \${{ secrets.FLOWBOARD_API_KEY }} FLOWBOARD_WORKSPACE_ID: \${{ secrets.FLOWBOARD_WORKSPACE_ID }} ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} GITHUB_TOKEN: \${{ secrets.GH_PAT }} The Complete Agent Script Here's the full agent loop that ties everything together. It fetches open tasks, claims them one by one, generates fixes, creates PRs, and updates FlowBoard.

import 'dotenv/config'; import fetch from 'node-fetch'; import Anthropic from '@anthropic-ai/sdk'; import { Octokit } from 'octokit'; import { execSync } from 'child_process'; const API = process.env.FLOWBOARD_API_URL; const API_KEY = process.env.FLOWBOARD_API_KEY; const WORKSPACE = process.env.FLOWBOARD_WORKSPACE_ID; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); const owner = process.env.GITHUB_OWNER; const repo = process.env.GITHUB_REPO; async function runAgent() { console.log('Fetching open tasks...'); const tasks = await getOpenTasks(); if (!tasks.length) { console.log('No open tasks found. Exiting.'); return; } console.log(\Found \${tasks.length} tasks. Processing...); // Get repo structure for context const repoStructure = execSync( \find \${process.env.REPO_PATH}/src -type f -name "*.ts" | head -100, { encoding: 'utf-8' } ); for (const task of tasks) { try { console.log(\\nProcessing: #\${task.number} - \${task.title}); // Claim the task await claimTask(task.id); console.log(' Claimed task'); // Generate fix with Claude const fileChanges = await generateFix(task, repoStructure); console.log(\ Generated changes for \${fileChanges.length} files); // Create GitHub PR const prUrl = await createPullRequest(task, fileChanges); console.log(\ Created PR: \${prUrl}); // Update FlowBoard await updateTaskWithPR(task.id, prUrl); console.log(' Updated FlowBoard task to In Review'); } catch (error) { console.error(\ Failed: \${error.message}); // Release the task back to open if we claimed it await fetch(\\${API}/updateTask, { method: 'PATCH', headers: { 'Authorization': \Bearer \${API_KEY}, 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: task.id, workspaceId: WORKSPACE, status: 'open', assignee: null }) }); } } console.log('\\nAgent run complete.'); } runAgent(); Next Steps You now have a working AI coding agent. Here are some improvements to consider as you iterate on the system:

  1. Add test running: Before creating a PR, run the project's test suite against the changes. If tests fail, feed the failures back to Claude for a second attempt.
  2. Handle blocked tasks: If the agent encounters a task with insufficient context, mark it as "Blocked" with a comment explaining what's missing instead of letting it fail silently.
  3. Add Slack notifications: Send a summary to Slack after each run -how many tasks were processed, PRs created, and any failures. Use FlowBoard's Slack integration for this.
  4. Filter by labels: Add an "agent-eligible" label to tasks and filter for it in the API query. This gives you explicit control over what the agent works on.
  5. Use the MCP server: For a simpler setup that integrates directly with Claude Code and Cursor, use the FlowBoard MCP Server instead of raw API calls.
  6. Add retry logic: If Claude's first attempt doesn't produce valid code, feed the error back and ask for a correction before giving up.
  7. Connect to Cursor Automations: For event-driven triggering -Cursor can kick off agent runs based on external events like new tasks or status changes.
  8. Track agent costs per task: Log token usage and API costs per task so you can measure ROI and identify tasks that are too expensive to automate.

Conclusion

Building an AI coding agent isn't science fiction -it's a few API calls stitched together. FlowBoard provides the task context and status management, Claude provides the code intelligence, and GitHub provides the delivery mechanism. The agent we built here is intentionally simple: fetch, claim, generate, push, update. That's the entire loop.

Start by running it against 3-5 well-defined bug reports. Review the PRs carefully, iterate on your task descriptions, and gradually expand the scope. Within a week, you'll have a reliable system that clears routine tasks from your backlog every night.

Top comments (0)