Export GitHub issues and pull requests to Google Sheets, including the reason the issues endpoint returns your PRs too and the Link header that drives pagination.
GitHub has no built-in CSV export for issues, so unlike most tools there is no two-minute no-code option to fall back on: the API is the route. It is a pleasant API to work with, with one trap that catches nearly everyone. The issues endpoint returns pull requests as well as issues, because GitHub models a PR as a kind of issue, which quietly doubles every count you produce.
GitHub to Google Sheets for free (Apps Script + REST API)
Create a personal access token under Settings → Developer settings → Personal access tokens. A fine-grained token scoped to the repositories you need with read access to issues and metadata is the safer choice; a classic token with the repo scope also works. Always authenticate even for public repositories: unauthenticated requests are capped at 60 an hour against 5,000 for a token.
Two structural details. Pagination lives in the Link response header rather than the body, so you follow the URL marked rel="next" until it disappears. And the state parameter defaults to open, so any report about completed work needs state=all or state=closed set explicitly.
function importGitHubIssues() { const TOKEN = "github_pat_XXXXXXXXXXXX"; const OWNER = "your-org"; const REPO = "your-repo"; const headers = { Authorization: "Bearer " + TOKEN, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }; const rows = []; // state defaults to "open"; anything historical needs state=all. let url = "https://api.github.com/repos/" + OWNER + "/" + REPO + "/issues?state=all&per_page=100"; while (url) { const response = UrlFetchApp.fetch(url, { headers, muteHttpExceptions: true }); if (response.getResponseCode() !== 200) { throw new Error(response.getResponseCode() + ": " + response.getContentText()); } JSON.parse(response.getContentText()).forEach((item) => { // A pull request IS an issue in GitHub's model, and comes back from // this endpoint. Only PRs carry a pull_request object, so use it to // separate them rather than double-counting issues. if (item.pull_request) return; rows.push([ item.number, item.title, item.state, item.user ? item.user.login : "", (item.assignees || []).map((a) => a.login).join(", "), (item.labels || []).map((l) => l.name).join(", "), item.created_at, item.closed_at || "", item.comments, item.html_url, ]); }); // Pagination is in the Link header, not the JSON body. url = nextLink(response.getHeaders()); if (url) Utilities.sleep(200); } const header = ["#","Title","State","Author","Assignees","Labels", "Created","Closed","Comments","URL"]; const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName("Issues") || ss.insertSheet("Issues"); sheet.clearContents(); sheet.getRange(1, 1, 1, header.length).setValues([header]); if (rows.length) { sheet.getRange(2, 1, rows.length, header.length).setValues(rows); }} // Link: <https://api.github.com/...&page=2>; rel="next", <...>; rel="last"function nextLink(headers) { const link = headers.Link || headers.link; if (!link) return null; const match = /<([^>]+)>;\s*rel="next"/.exec(link); return match ? match[1] : null;}To report on pull requests instead, invert the filter and keep only items with a pull_request key, or use the dedicated /pulls endpoint, which returns merge state and base and head branches that the issues endpoint does not include.
Limits: Apps Script's six-minute cap applies, so a repo with thousands of issues may need splitting with the since parameter. Reviews, review comments and commit details each require additional calls per item, which is where GraphQL starts to pay off.
The Brooked method
Brooked separates issues from pull requests correctly, follows the Link header pagination, and flattens labels, assignees and reviewers into readable columns. Several repositories can import into one spreadsheet on a single schedule, which is what makes cross-repo engineering reporting practical.
Step 1: Install Brooked
Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.
Step 2: Connect GitHub
Choose GitHub in Brooked and authorise, or paste a personal access token. Pick the repositories and whether you want issues, pull requests, commits or releases, then import.
Step 3: Schedule the refresh
Set the import to re-run daily or weekly so engineering metrics rebuild themselves. A scheduled re-read also picks up issues closed or reopened after the fact, which an event-driven feed would not revisit.
Method comparison
| Method | Cost | Setup | Refresh | Two-way | Best for |
|---|---|---|---|---|---|
| GitHub CSV export | Free | n/a | Not available | No | Nothing, GitHub has no built-in CSV export for issues |
| Apps Script + REST API | Free | 20 to 30 min | Time-driven trigger | DIY | A few repos, no budget |
| GitHub Actions writing to Sheets | Free tier minutes | 30 to 60 min | On schedule or on event | No | Teams already living in Actions |
| Brooked | Free tier · Pro $29/user/mo | ~3 min | Scheduled (hourly, daily, weekly) | No | Scheduled engineering reporting |
Common use cases
- Cycle time: measure time from PR opened to merged across repos
- Issue triage: track open bugs by label, age and assignee in one table
- Release notes: pull merged PRs between two dates as a starting draft
- Contributor reporting: activity per author across several repositories
- SLA tracking: find issues open past a threshold and alert on them
Troubleshooting common issues
Related Articles
- How to Set Up Data Alerts in Google Sheets
- How to Connect Pipedrive to Google Sheets
- How to Connect Monday.com to Google Sheets
Frequently asked questions
Why does the GitHub issues endpoint return pull requests?
Because in GitHub's data model a pull request is an issue with extra attributes, so /repos/{owner}/{repo}/issues returns both. Every PR in the response carries a pull_request object that plain issues lack, so filter on the presence of that key. Skipping this step roughly doubles your issue counts on an active repo and is the single most common GitHub API mistake.
Can I export GitHub issues to Google Sheets for free?
Yes, via the API. GitHub has no built-in CSV export for issues, so there is no no-code route here at all. The Apps Script method in this guide uses a free personal access token and a free time-driven trigger. Brooked's free tier covers 100 imports a month if you would rather not maintain a script.
How does GitHub pagination work?
Unlike most APIs, GitHub does not return pagination state in the response body. It sends a Link header containing URLs with rel values such as next and last, and you follow the next link until it is absent. per_page maxes at 100. Code that expects a next_cursor field in the JSON will silently stop after the first page.
What are GitHub's rate limits?
5,000 requests per hour for an authenticated personal access token, versus only 60 per hour unauthenticated, which is why you should always authenticate even for public repos. The search API is limited separately and much more tightly, at around 30 requests per minute, so prefer the list endpoints with filters over search where you can.
Should I use REST or GraphQL?
REST is simpler and fine for straightforward exports of issues, PRs or commits. GraphQL earns its complexity when you need related data in one round trip, such as pull requests with their reviews, review comments and linked issues, where REST would need several calls per PR and burn through your rate limit.
Can I write back to GitHub from Sheets?
Technically yes with a write-scoped token, though it is rarely a good idea. Bulk-editing issue titles, labels or assignees from a spreadsheet bypasses code review and audit expectations that teams usually want around their tracker. Brooked treats GitHub as read-only for reporting.
Connect GitHub to Google Sheets today.
Free tier: 100 imports per month, no credit card required. Or see the full GitHub integration details.
Install Brooked free →

