Tutorial15 min read2026-04-14Updated 2026-09-10

Build Your Own AI Coding Agent with FlowBoard's API

Step-by-step tutorial for building a custom AI coding agent that reads tasks from FlowBoard, writes code, and creates pull requests automatically.

Karim Gaad
Karim Gaad

Founder of FlowBoard · Full-stack developer & serial entrepreneur

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.

Ready to Try FlowBoard?

Start managing your projects with continuous flow. No sprints, no bloat - just ship.

Get Started Free