Introduction
GitHub Projects v2, launched in beta and stabilized in 2024, revolutionizes project management with dynamic boards, custom fields, and native automations. Unlike Projects v1's static columns, v2 provides a powerful GraphQL API for programmatic CRUD, iterations like sprints, and seamless GitHub Actions integrations. For experts, the key is scaling: automate updates from issues/PRs, build data-driven dashboards, and connect external tools. This tutorial walks you through it step by step with complete code to turn your repos into project powerhouses. Ideal for tech leads managing 10+ repos at once. (112 words)
Prerequisites
- GitHub Pro or Enterprise account with Projects v2 access (enable beta if needed).
- Existing repository with issues and PRs.
- Advanced knowledge of GitHub Actions and GraphQL.
- Node.js 20+ to test API scripts.
- GitHub CLI installed (
gh auth login).
Create a Project via GraphQL API
mutation CreateProject {
createProjectV2(input: {
ownerId: "REPO:your-org/your-repo",
title: "Mon Projet Expert 2026",
description: "Projet avancé avec automatisations",
public: true
}) {
projectV2 {
id
number
title
url
}
}
}This GraphQL mutation creates a Project v2 in a specific repo. Replace ownerId with your repo (format 'REPO:org/repo'). Run it via GitHub CLI (gh api graphql -f queryFile=query.graphql) or curl with a PAT token (scopes: project). Pitfall: without public: true, it stays private and inaccessible to automations.
Add Custom Fields
After creation, set up fields via UI or API: iteration (for sprints), single select (priorities), number (effort). This sets the stage for automations.
Add Custom Field via API
mutation AddCustomField {
projectV2FieldCreate(input: {
projectId: "PVT_xxxxx",
name: "Priorité",
dataType: SINGLE_SELECT,
singleSelectOptionValues: ["P0", "P1", "P2", "P3"]
}) {
field {
id
name
dataType
}
}
}Adds a 'Priorité' single-select field. projectId comes from the creation response (format PVT_). Great for dynamic sorting. Note: max 50 options per field, and dataType must match (SINGLE_SELECT, ITERATION, etc.). Test with gh api graphql.
YAML Workflow for Auto-Adding Issues
name: Ajouter Issue au Project
on:
issues:
types: [opened, labeled]
jobs:
add-issue:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/add-to-project@v2
with:
project-url: https://github.com/orgs/your-org/projects/1
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
labeled: triage
label-operator: ORThis workflow automatically adds issues labeled 'triage' to the Project. Use a dedicated PAT (repo:write, project scopes). project-url is the board's URL. Pitfall: missing permissions cause silent failures; check Actions logs.
Link Items to Issues/PRs
Now, map existing issues to the Project and update fields automatically.
Add Item and Update Fields
mutation AddItemAndUpdate {
addProjectV2ItemById(input: {
projectId: "PVT_xxxxx",
contentId: "I_abc123"
}) {
item {
id
}
}
projectV2ItemFieldSingleSelectValueUpdate(input: {
projectId: "PVT_xxxxx",
itemId: "PVTF_xxxxx",
fieldId: "PVTF_xxxxx",
singleSelectOptionId: "P0"
}) {
singleSelectField {
name
}
}
}Adds an issue (contentId: I_... for issue, PR_ for PR) to the project, then updates the 'Priorité' field to 'P0'. Fetch IDs via prior queries. Tip: Chain mutations in batches for better performance. Common error: IDs expire after 30 days.
Advanced Workflow: Auto-Update on PR Merge
name: Update Project on Merge
on:
pull_request:
types: [closed]
jobs:
update-field:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Run field updater
uses: actions/github-script@v7
with:
script: |
const { data: { project } } = await github.rest.projects.get({ owner: context.repo.owner, project_id: 1 });
// Logique custom pour update via GraphQL
env:
PROJECT_ID: ${{ secrets.PROJECT_ID }}Triggers on PR merge to update fields (e.g., set to 'Done'). Extend with github-script for GraphQL calls. Secure with PROJECT_ID secret. Limit: API rate limits (5000/hour with PAT).
GraphQL Query for Custom Dashboard
query ProjectDashboard($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
title
items(first: 20) {
nodes {
content {
... on Issue {
title
labels(first: 5) { nodes { name } }
}
}
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
}
}
}
}Paginated query to export Project data as JSON/CSV. Uses $projectId variable. Ideal for external dashboards (e.g., Node script to Google Sheets). Pitfall: first:100 max per page; use after for cursor pagination.
Best Practices
- Always use dedicated PATs with minimal scopes (project, repo) stored in secrets.
- Paginate GraphQL queries for >100 items; implement cursors.
- Version workflows with feature branches for testing.
- Backup IDs: Store project/field IDs in repo vars/secrets.
- Monitor rate limits via GitHub API status.
Common Errors to Avoid
- Stale IDs: Project v2 IDs rarely change, but verify after org migrations.
- Missing permissions: Actions fail without
issues:write; addpermissions:block. - Uninitialized fields: Updating before addItem causes NULL errors.
- Rate limiting: >100 mutations/day → 429; add throttling delays.
Next Steps
- Official docs: GitHub Projects GraphQL.
- Example repo: Fork github/docs to test.
- Integrate with Slack/Teams via webhooks.
- Check our Learni Dev trainings on GitHub Automation.