Build an AI Agent That Opens Pull Requests for You
Learn to build a practical AI agent that creates feature branches, commits changes, and opens pull requests automatically with safe permissions and review.
Before you start
You are tired of manually creating branches, committing changes, and opening pull requests for every small fix. This tutorial shows you how to build an AI agent that does that for you, safely and with review.
We will use Node.js, the GitHub REST API, and OpenAI's function calling. The agent will run locally, but the same pattern works in CI. You will need a GitHub personal access token and an OpenAI API key.
The final agent will: take a natural language instruction, create a branch, make a commit, push it, and open a PR. It will also check for existing PRs to avoid duplicates and respect a dry-run mode.
mkdir ai-pr-agent && cd ai-pr-agent
npm init -y
npm install openai @octokit/rest dotenv simple-git
- Node.js 18 or later
- GitHub personal access token with repo scope
- OpenAI API key with access to gpt-4o-mini
- A local git repository with a remote on GitHub
Step 1: Set up environment variables
Create a .env file in the project root. This keeps your tokens out of the code. Add the file to .gitignore if you are committing this project.
The agent will read these variables at runtime. The GitHub token needs the repo scope to create branches and PRs. The OpenAI key is used for generating commit messages and PR descriptions.
GITHUB_TOKEN=ghp_your_token_here
OPENAI_API_KEY=sk-your-key-here
REPO_OWNER=your-github-username
REPO_NAME=your-repo-name
Step 2: Build the agent core
We will create an agent that uses OpenAI function calling. The model decides which tools to call based on the user's request. We define three tools: createBranch, commitAndPush, and openPullRequest.
The agent loop is simple: send the user message and the tool definitions to OpenAI. If the response includes tool calls, execute them and feed the results back. Repeat until the model is done.
const { OpenAI } = require('openai');
const { Octokit } = require('@octokit/rest');
const dotenv = require('dotenv');
dotenv.config();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const tools = [
{
type: 'function',
function: {
name: 'createBranch',
description: 'Create a new branch in the repository',
parameters: {
type: 'object',
properties: {
branchName: { type: 'string', description: 'Name of the branch to create' },
baseBranch: { type: 'string', description: 'Base branch to branch from', default: 'main' }
},
required: ['branchName']
}
}
},
{
type: 'function',
function: {
name: 'commitAndPush',
description: 'Commit changes and push to the remote branch',
parameters: {
type: 'object',
properties: {
branchName: { type: 'string', description: 'Branch to push to' },
commitMessage: { type: 'string', description: 'Commit message' }
},
required: ['branchName', 'commitMessage']
}
}
},
{
type: 'function',
function: {
name: 'openPullRequest',
description: 'Open a pull request from a branch to the base branch',
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: 'PR title' },
body: { type: 'string', description: 'PR description' },
head: { type: 'string', description: 'Source branch' },
base: { type: 'string', description: 'Target branch', default: 'main' }
},
required: ['title', 'head']
}
}
}
];
Step 3: Implement the tool functions
Now we implement the actual functions that the model will call. Each function uses the Octokit SDK or simple-git to perform the action. We also add a check in createBranch to avoid duplicates.
For commitAndPush, we use simple-git to stage all changes, commit with the message, and push to the branch. Make sure you are in the repository directory when running the agent.
const simpleGit = require('simple-git');
const git = simpleGit();
async function createBranch({ branchName, baseBranch = 'main' }) {
// Check if branch already exists
const { data: branches } = await octokit.repos.listBranches({
owner: process.env.REPO_OWNER,
repo: process.env.REPO_NAME
});
if (branches.some(b => b.name === branchName)) {
return `Branch ${branchName} already exists`;
}
await git.fetch();
await git.checkout([baseBranch]);
await git.checkoutLocalBranch(branchName);
return `Created branch ${branchName} from ${baseBranch}`;
}
async function commitAndPush({ branchName, commitMessage }) {
await git.add(['.']);
await git.commit(commitMessage);
await git.push('origin', branchName);
return `Committed and pushed to ${branchName}`;
}
async function openPullRequest({ title, body, head, base = 'main' }) {
const { data: pr } = await octokit.pulls.create({
owner: process.env.REPO_OWNER,
repo: process.env.REPO_NAME,
title,
body,
head,
base
});
return `Opened PR #${pr.number}: ${pr.html_url}`;
}
Step 4: Run the agent loop
The main loop reads the user's instruction, sends it to OpenAI with the tool definitions, and handles tool calls. We keep a message history so the model can see the results of previous tool calls.
The loop continues until the model stops requesting tool calls. We also include a safety check: if the user says dry-run, we do not actually execute the tools, just print what would happen.
async function runAgent(userInput) {
const messages = [{ role: 'user', content: userInput }];
const dryRun = userInput.toLowerCase().includes('dry run');
for (let i = 0; i < 5; i++) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages,
tools,
tool_choice: 'auto'
});
const message = response.choices[0].message;
messages.push(message);
if (!message.tool_calls) {
console.log('Final answer:', message.content);
break;
}
for (const toolCall of message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
console.log(`Tool call: ${toolCall.function.name}(${JSON.stringify(args)})`);
let result;
if (dryRun) {
result = `[dry-run] Would call ${toolCall.function.name}`;
} else {
switch (toolCall.function.name) {
case 'createBranch':
result = await createBranch(args);
break;
case 'commitAndPush':
result = await commitAndPush(args);
break;
case 'openPullRequest':
result = await openPullRequest(args);
break;
default:
result = 'Unknown tool';
}
}
messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result });
console.log('Tool result:', result);
}
}
}
// Example usage
runAgent('Create a branch called fix-typo, commit the current changes with message "Fix typo in README", and open a PR titled "Fix typo" with body "Fixes a small typo"');
Step 5: Test the agent locally
Run the agent with a simple instruction. Make sure you are in a git repository with some uncommitted changes. The agent will create a branch, commit, push, and open a PR.
If you want to test without affecting the repo, use the dry-run mode. The agent will print the tool calls without executing them.
node index.js
# Output should show tool calls and final answer
- Run node index.js to start the agent
- Use dry-run first to verify the flow
- Check that the branch and PR appear on GitHub
- If the agent fails, check the error messages in the console
Verify it worked
After running, go to your GitHub repository and confirm that a new branch exists, the commit is there, and a pull request is open. The PR should have the title and body generated by the model.
You can also check the local git log to see the commit. The agent should have switched back to the original branch after pushing, but that is not implemented here; you can add it later.
git log --oneline -1
# Should show your commit message
Adding safety checks
In a real workflow, you want to prevent the agent from pushing to main or opening PRs without review. Add a check that refuses to create a branch if the base branch is not main, or require a confirmation step.
You can also restrict the agent to only modify certain files. The example below shows how to add a simple confirmation prompt before executing any tool.
// Add this inside the tool call loop
const confirm = await askUser(`Execute ${toolCall.function.name}? (y/n)`);
if (confirm !== 'y') {
result = 'Skipped by user';
} else {
// execute the tool
}
What I would do
For a production setup, I would add a human-in-the-loop step before opening the PR. I would also use a more robust agent framework like LangChain or Vercel AI SDK, but the core logic remains the same.
Here is a recommended .env.example and a minimal run script you can copy. This is the setup I would use for a team bot that listens to Slack commands.
GITHUB_TOKEN=ghp_xxx
OPENAI_API_KEY=sk-xxx
REPO_OWNER=my-org
REPO_NAME=my-repo
ALLOWED_BRANCHES=main,develop
// run.js
require('dotenv').config();
const { runAgent } = require('./agent');
const input = process.argv.slice(2).join(' ');
if (!input) {
console.error('Usage: node run.js "your instruction"');
process.exit(1);
}
runAgent(input);
Troubleshooting
If the agent fails to push, check that your local branch is up to date and that you have permission to push. If the model does not call the expected tools, rephrase your instruction.
Common issues: missing environment variables, wrong repo name, or not being in the correct directory. The error messages from Octokit and simple-git usually point to the problem.
- Check that GITHUB_TOKEN has repo scope
- Ensure the repository path is correct and you have committed changes
- If the model returns an empty response, increase the max tokens or simplify the instruction
- If you get a 403 from GitHub, your token may be expired
FAQ
Answers to the questions that come up most often on this topic.
- Q: Can I use this with GitHub Actions? A: Yes, you can run the agent as a scheduled job or on issue comments, but you need to set up a token with write permissions.
- Q: How do I prevent the agent from pushing to main? A: Add a check in the createBranch function to reject branch names like main or master.
- Q: Can I use a different LLM? A: Yes, you can replace OpenAI with any model that supports function calling, such as Anthropic's Claude or a local model.
- Q: How do I handle multiple changes in one PR? A: The agent can create multiple commits on the same branch before opening the PR.
Next action
Run the agent with a dry-run instruction to see the flow. Then make a real change and let it open a PR for you. Remember to review the PR before merging.
The full source code is available in the article. Modify it to fit your workflow.
node run.js "dry run: create branch test, commit changes, open PR"
Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on ai-agent for?
- Working developers who need a practical take on build an ai agent that opens pull requests for you — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published September 17, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.