GitHub Projects has a GraphQL API for projects, fields, items, field values and issue relationships. The examples on this page are queries or mutations extracted from Gitsu's client, with selections trimmed for the task shown. They are source-backed examples, not a record of a live test of each block against your project. Each block names the file it came from, so you can read the full version in context.
Gitsu is a desktop and web client for GitHub Projects, and GitHub is the system of record for its board data. Gitsu caches reads locally and sends board writes through GraphQL. That makes its client a useful reference for the parts GitHub's documentation leaves to you, such as pagination cost, clearing a value and which id a mutation wants. If you would rather see the result than the requests, getting started with the Gitsu Web App shows the same boards in the browser.
How do I authenticate against the Projects API?
Use the downloadable read-only query for a small connectivity check. It reads a project owned by the signed-in user, its first 20 field declarations and first three item IDs/types, with pagination information. It creates and changes nothing.
Save it as gitsu-project-read.graphql, sign in through GitHub CLI, and replace this example number with your own project number:
PROJECT_NUMBER=1
gh api graphql -F number="$PROJECT_NUMBER" \
-f query="$(cat gitsu-project-read.graphql)" \
--jq '.data.viewer.projectV2 | {number, fieldCount: (.fields.nodes | length), itemCount: (.items.nodes | length), pageInfo: .items.pageInfo}'
The response contains number, fieldCount, itemCount and pageInfo. itemCount here is the sample size, at most three, not the project's total. hasNextPage: true means the sample is incomplete. An organisation-owned project needs organization(login: $login) in place of viewer and a login variable. A null project is not an empty board: check owner, number and access.
This query was exercised against an accessible project on 7 October 2026 and returned project/field IDs, item IDs/types and pagination fields. Private identifiers and field names are not published here. The read-only result does not certify the mutation blocks below. Their handling is checked by source and fixture tests; no live write was made for this guide.
Send a bearer token to https://api.github.com/graphql in an Authorization: Bearer <token> header. GitHub documents two OAuth scopes for Projects: read:project for reading and project for reading and writing. Use project for project writes, and include the repository permissions needed for the issue operations below. A classic token needs repo to access private repository data. Follow GitHub's GraphQL authentication guide for your token type; project access alone does not grant access to every repository represented on a board.
Gitsu's sign-in asks for project repo read:org read:user, the same string in its desktop device flow and its web redirect flow. project covers the board, repo covers the issues and pull requests on it, read:org lets it list organization boards, and read:user reads your profile. It does not ask for notifications, so it cannot change whether you watch a repository.
Rate limits are counted in points per hour, and a query's cost depends on how many nodes it asks for. You can select rateLimit { limit remaining resetAt } on any query to see your budget. You cannot select it on a mutation: GitHub declares rateLimit on Query only, and asking for it inside a mutation fails the whole document with Field 'rateLimit' does not exist on type 'Mutation', so nothing is written. Gitsu reads the x-ratelimit-* response headers after a write instead.
How do I find a project's node id?
Every Projects query and mutation takes the project's global node id, a string starting PVT_, not the number in its URL. List the projects your token can see and read the id off each one. This is the query Gitsu runs to fill its project picker, with the owner fragments trimmed.
Source: src/github/queries/projects.ts (projectsQuery)
query ViewerProjects($first: Int!, $after: String) {
viewer {
projectsV2(
first: $first
after: $after
minPermissionLevel: READ
orderBy: { field: NUMBER, direction: ASC }
) {
nodes {
id
number
title
url
}
pageInfo {
hasNextPage
endCursor
}
}
}
rateLimit {
limit
remaining
resetAt
}
}
The same selection works under organization(login: $login) for an organization's boards. Keep the orderBy: a connection paged without a stable sort can repeat or skip nodes between pages.
Once you have the id, read the project's fields. Writes need field ids, and single select and iteration writes also need option or iteration ids, which only this query returns.
Source: src/github/queries/project.ts (projectQuery, iteration and multi-select fragments trimmed)
query Project($id: ID!, $first: Int!) {
node(id: $id) {
... on ProjectV2 {
id
title
fields(first: $first) {
nodes {
... on ProjectV2Field {
id
name
dataType
}
... on ProjectV2SingleSelectField {
id
name
dataType
options {
id
name
}
}
}
}
}
}
}
GitHub's own field dialog offers six custom field types: text, number, date, single select, multi select and iteration. For an iteration field, read both configuration.iterations and configuration.completedIterations; the first holds only current and future iterations, so a filter naming last sprint needs the second.
How do I read items and their field values?
Page through items 100 at a time, which is the most GitHub allows for first, passing pageInfo.endCursor back as $after until hasNextPage is false. Each item has fieldValues, a union with one member per field type, and you select the members you want with inline fragments.
Source: src/github/queries/items.ts (itemFieldValuesQuery; $archivedStates, dataType, the multi-select fragment and rateLimit trimmed)
query ProjectItemFieldValues($id: ID!, $first: Int!, $after: String) {
node(id: $id) {
... on ProjectV2 {
items(first: $first, after: $after) {
nodes {
id
fieldValues(first: 50) {
nodes {
__typename
... on ProjectV2ItemFieldTextValue {
field {
... on ProjectV2FieldCommon {
id
}
}
text
}
... on ProjectV2ItemFieldNumberValue {
field {
... on ProjectV2FieldCommon {
id
}
}
number
}
... on ProjectV2ItemFieldDateValue {
field {
... on ProjectV2FieldCommon {
id
}
}
date
}
... on ProjectV2ItemFieldSingleSelectValue {
field {
... on ProjectV2FieldCommon {
id
}
}
optionId
name
}
... on ProjectV2ItemFieldIterationValue {
field {
... on ProjectV2FieldCommon {
id
}
}
iterationId
title
startDate
}
}
pageInfo {
hasNextPage
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
Nested connections such as fieldValues cannot be paged with the same cursor, so pick a size that covers your project and check their hasNextPage. Gitsu asks for 50 values per item and reports a truncated page rather than dropping values silently.
Cost is the thing to watch. A page is roughly 100 items times the nodes nested in each, so Gitsu splits the load in two: one walk for each item's issue content (title, assignees, labels, state) and a second walk for the project's own field values. The board can draw from the first while the second is still in flight, and both walk the same connection in the same order so the ids line up.
items excludes archived items unless you pass archivedStates. Built-in columns such as labels, assignees and milestone are read from the issue itself (content { ... on Issue { labels { ... } } }), not from fieldValues.
How do I update a field value?
Call updateProjectV2ItemFieldValue with the project id, the project item id and the field id, plus a value object with exactly one key. The item id is the ProjectV2Item id from the items query, not the issue's id.
Source: src/github/mutations.ts (setFieldValueMutation)
mutation SetFieldValue($input: UpdateProjectV2ItemFieldValueInput!) {
updateProjectV2ItemFieldValue(input: $input) {
projectV2Item {
id
}
}
}
The value key depends on the field type. Gitsu's client picks one per type:
| Field type | value key | What it takes |
|---|---|---|
| Text | text | A string |
| Number | number | A number |
| Date | date | An ISO date, such as 2026-09-24 |
| Single select | singleSelectOptionId | An option id from the fields query |
| Multi select | multiSelectOptionIds | A list of option ids |
| Iteration | iterationId | An iteration id |
Sending two keys is an error. There is also no key that means empty, so clearing a cell is a different mutation that takes the same three ids.
Source: src/github/mutations.ts (clearFieldValueMutation)
mutation ClearFieldValue($input: ClearProjectV2ItemFieldValueInput!) {
clearProjectV2ItemFieldValue(input: $input) {
projectV2Item {
id
}
}
}
Labels, assignees and milestone are not project fields, and updateProjectV2ItemFieldValue cannot write them. Change those on the issue, with addLabelsToLabelable, addAssigneesToAssignable or updateIssue.
How do I add an issue to a project?
Call addProjectV2ItemById with the project id and the issue's node id as contentId. It returns the new project item. Gitsu also selects the new item's field values, because a project workflow such as "Item added to project" may set Status the moment the item lands, and reading it here saves a second request.
Source: src/github/mutations.ts (addItemToProjectMutation, fragments trimmed to one)
mutation AddItemToProject($input: AddProjectV2ItemByIdInput!) {
addProjectV2ItemById(input: $input) {
item {
id
fieldValues(first: 100) {
nodes {
__typename
... on ProjectV2ItemFieldSingleSelectValue {
field {
... on ProjectV2FieldCommon {
id
}
}
optionId
name
}
}
}
}
}
}
To create a new issue and put it on a board, run createIssue first, then this mutation with the returned issue id. Draft issues have their own addProjectV2DraftIssue mutation; Gitsu does not create drafts.
How do I link sub-issues?
Sub-issue links live on the parent issue, not on the project item. addSubIssue takes the parent's issue node id as issueId and the child's as subIssueId. removeSubIssue takes the same pair and only removes the link; the child issue is not deleted.
Source: src/github/mutations.ts (addSubIssueMutation)
mutation AddSubIssue($input: AddSubIssueInput!) {
addSubIssue(input: $input) {
subIssue {
id
}
}
}
To read the hierarchy, select parent, subIssuesSummary { completed total } and subIssues(first: N) on an Issue. reprioritizeSubIssue reorders a child within its parent's list, with afterId naming the sibling it should follow. Blocking relationships work the same way through addBlockedBy and removeBlockedBy; Issue.blocking is the computed inverse and has no mutation of its own.
Which id does each mutation want, and how do errors come back?
Most failed Projects writes come from passing the wrong id. There are three kinds, and they are not interchangeable:
| Id | Where you get it | Used by |
|---|---|---|
Project (PVT_…) | projectsV2 or node(id:) on the project | Every project mutation, as projectId |
| Project item | items { nodes { id } } | Field value writes, archive, delete, as itemId |
| Issue or pull request | content { ... on Issue { id } } | addProjectV2ItemById as contentId, sub-issues, labels |
A GraphQL response can carry an errors array beside a data object, so a successful HTTP status does not mean the query worked. Read errors on every response. Gitsu treats a response whose only errors are NOT_FOUND as a partial success when it asked for several nodes by id at once, since the null nodes are simply the missing ones. A RATE_LIMITED error means the hourly budget is spent; wait for the reset time rather than retrying.
What can't the API do?
Gitsu's August 2026 GraphQL audit recorded the following gaps. This is a dated schema snapshot, so consult GitHub's current mutation reference before using an absence here to decide what you can build:
- Workflows can be read and deleted, but not created, edited or turned on and off. Configure them in the project's Workflows page.
- Insights charts have no field on
ProjectV2and no mutation. - Export view data (CSV) is only in the github.com view menu.
- Sort order cannot be written to a view:
UpdateProjectV2ViewInputhas no sort field, though you can read a view'ssortByFields.
GitHub also has a REST API for modern Projects, with endpoints for projects, fields, draft items, project items and views. These are distinct from the old classic project boards. GraphQL remains the API used in this guide and in Gitsu's project client. Gitsu uses REST for creating, editing, closing and deleting milestones. Choose the API that exposes the operation you need, and check its authentication and API-version requirements rather than assuming all project work requires GraphQL.
What does this look like in Gitsu?
Gitsu is these same calls behind a fast interface. Its board patches the screen the moment you change a value, sends the mutation above, and rolls the change back with a message if GitHub refuses it. The guide to GitHub project board examples shows five board setups built on these fields, and the Gitsu keyboard shortcuts page lists the keys that trigger each write.
How we checked
The GraphQL examples are extracted from Gitsu's GitHub client and trimmed for the task shown. The guide also records limits found in its August 2026 schema audit; that snapshot is not a current count of GitHub's API capabilities.
Frequently asked questions
Which token scope does the Projects GraphQL API need?
GitHub documents read:project for reading projects and project for reading and writing them. Gitsu asks for project repo read:org read:user, because it also edits the issues on the board and lists organization boards.
How do I set a Status column through the API?
Status is a single select field. Read the field id and its option ids from the project fields, then call updateProjectV2ItemFieldValue with singleSelectOptionId set to the option id, using the project item id, not the issue id.
How do I clear a field value?
Call clearProjectV2ItemFieldValue with the project, item and field ids. updateProjectV2ItemFieldValue has no empty value, so sending null or an empty string does not clear a cell.
Can I create project views or workflows through the API?
Gitsu's August 2026 GraphQL audit found create, update and delete mutations for views, but only reads and deletion for workflows. Configure workflows on github.com and check GitHub's current API reference before treating that dated snapshot as a current API limit.
Is there a REST API for GitHub Projects?
Yes. GitHub documents REST endpoints for modern Projects, including fields, items and views. This guide teaches GraphQL because that is what Gitsu uses for its project operations; Gitsu also uses REST for milestone writes.
Try it on your own boardsGitsu opens your GitHub Projects boards in the browser. Sign in with GitHub and work on the same issues your team already uses.
Open the web app