GitHub Projects (Boards)
GitHub Projects is GitHub’s built-in planning tool for organizing issues, pull requests, and standalone tasks into boards, tables, and roadmaps. Unlike branches or commits, a project lives entirely in GitHub’s own database — it never touches your repository’s git history. If Issues are where you track individual problems, Projects is where you see the whole plan at once: what’s in progress, what’s blocked, and what ships next. This lesson covers the modern Projects experience (sometimes called “Projects v2”), how it relates to Issues and pull requests, and how to manage it from the command line with the gh CLI.
Overview / How it works
A GitHub project is a container of items — issues, pull requests, and draft issues (lightweight to-dos that exist only inside the project until you convert them into a real issue) — arranged using one or more views. There are three view types:
- Table view — a spreadsheet: one row per item, one column per field. Good for bulk editing, sorting, and filtering.
- Board view — a kanban-style board where cards are grouped into columns by the value of a single single-select field, almost always one named
Status(for example Todo, In Progress, Done). Dragging a card to another column just changes that field’s value on the underlying issue or pull request — nothing happens in git. - Roadmap view — a timeline (Gantt-style) that plots items along a date range, driven by start-date and target-date fields.
Every project also has fields: built-in ones like Status, Assignees, Labels, and Milestone (mirrored from the underlying issue or pull request), plus custom fields you define yourself — text, number, date, single-select, or iteration (a repeating sprint-like time box). Fields live on the project, not on the issue, so the same issue can have a different Priority value in two different projects it belongs to.
Projects are owned at the user or organization level, not the repository level, and a single project can pull in issues and pull requests from many repositories at once — even repositories owned by different people, as long as you have access to each one. This is a deliberate change from the older “classic” Projects feature (repository- or org-scoped, now deprecated and being phased out), which offered fixed columns and no custom fields. If you run into documentation describing cards tied 1:1 to a single repo with no custom fields, it’s almost certainly describing classic Projects — this lesson covers the current version.
Under the hood, a project and its items are ordinary GitHub data reachable through the GraphQL API (object types like ProjectV2, ProjectV2Item, and field-value records that store what each item is worth for each field). Adding an issue to a project links the issue’s node to the project — it does not create a commit, branch, tag, or any object in your repository’s object database. You can delete a project entirely and every commit, blob, and tree in your repositories stays untouched, because a project is metadata about work, not a snapshot of code.
Automation is built in: a project can be configured so that closing an issue automatically sets its Status to Done, or so that any issue added to the project defaults to Todo. For anything more custom — like auto-adding every new issue from a repository, or every pull request carrying a specific label — you wire it up with a GitHub Actions workflow that calls the project’s GraphQL API (commonly through the community action actions/add-to-project) or with the gh project CLI commands shown below.
Syntax
There is no git subcommand for Projects — it’s a GitHub product feature, managed through the web UI, the GraphQL API, or gh (GitHub’s official command-line tool, separate from git itself). The general form of the relevant gh commands:
gh project SUBCOMMAND [flags]
| Subcommand | Purpose |
|---|---|
gh project create |
Create a new project for a user or organization |
gh project list |
List projects owned by a user or organization |
gh project view |
Show a project’s details, optionally opening it in the browser |
gh project item-add |
Add an existing issue or pull request to a project by URL |
gh project item-list |
List the items currently in a project |
gh project item-edit |
Change a field’s value on a project item |
gh project field-list |
List a project’s fields (Status, custom fields, and so on) |
Common flags across these subcommands:
--owner <login>— the user or organization that owns the project (use@mefor your own account)--title <text>— the project’s display name, used withcreate--format json— machine-readable output, useful for scripting--web— open the result in your browser instead of printing it to the terminal
Examples
Example 1: create a project
gh project create --owner "octocat" --title "Website Relaunch"
Output:
Creating project
https://github.com/orgs/octocat/projects/7
This creates a brand-new project owned by the octocat organization and prints its URL. The number 7 in the URL is the project’s number within that organization — you’ll pass it to later gh project commands. At this point the project is empty: no fields beyond the defaults, no items, and no automation configured yet.
Example 2: add items and list them
gh project item-add 7 --owner "octocat" \
--url https://github.com/octocat/website/issues/42
gh project item-list 7 --owner "octocat"
Output:
Added item
ID
PVTI_lADOAxxxxxxxxx
Title Status Assignees Labels Repository
Fix broken footer links Todo bug octocat/website
Redesign homepage hero section In Progress mona enhancement octocat/website
The first command adds an existing issue (by URL) to project number 7 as a new item; GitHub prints the internal item ID it created for that link. The second command lists everything currently in the project, including an issue that was added earlier through the web UI and already has a Status of In Progress. Note that item-add only accepts existing issues or pull requests by URL — for a task that doesn’t have an issue yet, you’d use gh project item-create to add a draft issue instead.
Example 3: auto-add new issues with a workflow
name: Add new issues to project
on:
issues:
types: [opened]
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/octocat/projects/7
github-token: ${{ secrets.PROJECT_TOKEN }}
Saved as .github/workflows/add-to-project.yml in a repository, this workflow runs every time a new issue is opened and adds it to project 7 automatically — no one has to remember to do it by hand. The token needs the project scope; a fine-grained personal access token or a GitHub App token stored as a repository secret (never the built-in GITHUB_TOKEN, which can’t write to org-level projects) is the usual choice.
How it works step by step
Whether you drag a card on the board or run gh project item-edit, the same underlying process happens:
- The client (browser or
gh) sends a GraphQL mutation identifying the project, the item, the field being changed, and the new value. - GitHub’s backend writes that value into the field-value record for that specific item in that specific project — a row in GitHub’s own database, not anything in a git repository.
- If the field is
Statusand the project has a matching built-in workflow enabled (for example “When Status changes to Done, close the issue”), GitHub fires that automation, which may in turn open or close the linked issue or pull request through the normal Issues API. - The board view re-renders the card in its new column purely by re-querying items grouped by the current
Statusvalue — there’s no separate “board state” stored anywhere; the board is just a live view over the field data.
At no point in this sequence does Git write a commit, move a branch pointer, or touch the index — Projects and git history are two entirely separate systems that happen to reference the same issues and pull requests.
Common Mistakes
Assuming a card moves itself when work happens elsewhere. Closing an issue with gh issue close 42 does not move its card to “Done” unless the project has the built-in Item closed workflow turned on (Project → ⋯ menu → Workflows). Without it, the card stays wherever it was left, and the board silently drifts out of sync with reality. Turn on the relevant built-in workflows for every project you rely on for status tracking.
Referencing a draft issue before converting it. Draft issues exist only inside the project — they have no repository, no issue number, and nothing for git or GitHub’s issue-linking syntax to find.
git commit -m "Fixes #57"
If #57 is still a draft issue rather than a real issue in the repository you’re committing to, Git happily records the commit, but GitHub’s automatic issue-closing will not find any issue #57 to close — the numbers only exist for real issues and pull requests. Convert the draft to a real issue (the “Convert to issue” action on the card) before referencing its number anywhere outside the project.
Mixing up project visibility with repository visibility. A project has its own visibility setting, independent of the repositories whose issues it contains. It’s easy to create a project as private by default while the underlying repository is public, which quietly hides your roadmap from contributors who can otherwise read every issue it tracks. Check the project’s own Settings → Visibility if teammates report they can see the issues but not the board.
Best Practices
- Keep one field named
Statusas the single source of truth for where work stands, and use it consistently across every project in an organization so people don’t relearn conventions per team. - Prefer a few well-defined custom fields (
Priority,Size,Iteration) over many overlapping ones — a project with fifteen fields is harder to triage than one with four. - Use multiple views filtered by team, label, or iteration rather than multiple separate projects, so everyone still works from one shared set of items and fields.
- Turn on the built-in
Item addedandItem closedworkflows so the board reflects reality without manual upkeep. - Convert draft issues to real issues as soon as they’re worth linking from a commit, pull request, or another issue — draft issues have no number anything outside the project can reference.
- Script bulk changes with
gh project item-edit --format jsoninstead of dragging dozens of cards by hand when re-triaging a large backlog. - Write a short description in the project itself explaining what each custom field means, especially single-select options — “Blocked” means something different on every team unless it’s written down.
Practice Exercises
- Create a project for one of your own repositories, add three issues to it (creating them first if you don’t have any), add a
Statusfield withTodo,In Progress, andDoneoptions if it isn’t there by default, and arrange a board view grouped by that field. Confirm that dragging a card between columns changes the issue’s field value without creating any new commit in the repository. - Add a custom single-select field named
Prioritywith optionsLow,Medium, andHigh, then usegh project field-listandgh project item-editto set a priority on each item from the terminal instead of the web UI. Hint:item-editneeds the project item’s ID (fromitem-list) and the field’s ID (fromfield-list). - Write a GitHub Actions workflow that automatically adds any pull request labeled
needs-reviewto your project, then open a test pull request with that label and confirm the item appears without you touching the project directly.
Summary
- GitHub Projects organizes issues, pull requests, and draft issues into boards, tables, and roadmaps; it lives entirely in GitHub’s database, never in your git history.
- Board columns are just groupings by a single-select field’s value (usually
Status); moving a card edits that field, nothing more. - Projects are owned at the user or org level and can span multiple repositories, unlike the deprecated repo-scoped “classic” Projects.
- Manage projects from the terminal with
gh project create,item-add,item-list,item-edit, andfield-list. - Built-in workflows (Item added, Item closed) and GitHub Actions (
actions/add-to-project) keep boards in sync without manual dragging. - Draft issues and project visibility are project-only concepts — convert drafts before referencing them elsewhere, and check project visibility separately from repository visibility.
