GitHub Projects has a GraphQL API, and it is the API that covers the whole product: projects, fields, items, field values and the issue relationships around them. Every example on this page is a query or mutation that Gitsu itself sends to GitHub, trimmed where it selects more than the example needs. 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 it keeps no copy of your board: every read and every write goes through this API. 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?
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. A classic personal access token with project is enough to run every example below against your own boards.
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?
The GraphQL schema lists 258 mutations, and 32 of them work on Projects. Several things you can do on github.com have no mutation:
- 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.
The REST side has the opposite gap. GitHub's classic REST projects endpoints were built for the old project boards, and every Projects example on this page is GraphQL. Gitsu's only REST calls are milestone writes: the GraphQL schema has no milestone mutation at all, so creating, editing, closing or deleting a milestone goes through REST.
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.
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?
Views have create, update and delete mutations in the schema. Workflows can only be read and deleted; there is no mutation to create, edit or enable one, so workflows are configured on github.com.
Is there a REST API for GitHub Projects?
The classic REST projects endpoints served the old project boards. GraphQL is the API every Projects example here uses, and the one Gitsu uses for every project read and write. Gitsu only calls REST for milestones, which GraphQL cannot write.
Try it on your own boardsGitsu opens your GitHub Projects boards in the browser. Sign in with GitHub; your data stays in GitHub.
Open the Web App