Source: https://cli.nylas.com/guides/email-to-github-issues

# Create GitHub Issues from Email (CLI)

Bug reports, support tickets, and feature requests still land in an inbox, but the work happens in GitHub. The usual bridge is a paid automation that charges per run. This guide pulls the email as JSON with the CLI, shapes the title and body with jq, and POSTs to the GitHub REST API /issues endpoint with a personal access token. You get a deduped, label-aware email-to-issue pipeline you control and can run on a cron.

Written by [Aaron de Mello](https://cli.nylas.com/authors/aaron-de-mello) Senior Engineering Manager

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Pull matching mail with `nylas email search --json`, shape a title and body with `jq`, then POST to the GitHub REST API `/repos/{owner}/{repo}/issues` endpoint with a fine-grained token. The CLI handles the inbox across six providers; GitHub handles the issue. One line you add at the end stops the same email from opening a duplicate issue on the next run.

Command references used in this guide: [`nylas email search`](https://cli.nylas.com/docs/commands/email-search), [`nylas email list`](https://cli.nylas.com/docs/commands/email-list), and [`nylas email read`](https://cli.nylas.com/docs/commands/email-read).

## How do I create a GitHub issue from an email?

Create a GitHub issue from an email by pulling the message as JSON and POSTing its subject and body to the GitHub REST API. The CLI returns structured messages with `nylas email search --json`, and a single POST to `/repos/{owner}/{repo}/issues` opens one issue per message. The create-issue contract is documented in the [GitHub REST API reference](https://docs.github.com/en/rest/issues/issues#create-an-issue).

Two prerequisites come first: a fine-grained personal access token with the repository's Issues permission set to Read and write, and the CLI authenticated to the mailbox. The token is separate from the mailbox grant the tool manages, so you rotate each independently. GitHub requires the `Accept: application/vnd.github+json` header and an `X-GitHub-Api-Version` header pinned to a dated version such as `2022-11-28`, which keeps the request stable across roughly annual API changes.

```bash
# Pull the messages you want to file as issues
nylas email search "*" --subject "bug" --after 2026-06-08 --json --limit 50 > items.json
```

## What GitHub token and scope do I need?

You need a fine-grained personal access token scoped to the repository, granting the Issues permission at the Read and write level. Fine-grained tokens are repository-scoped and expire on a date you set, up to 366 days, which is safer than a classic token carrying the full `repo` scope across every repository you can touch. GitHub documents both token types in its authentication reference.

Mint the token under Settings, then Developer settings, then Fine-grained tokens, and select only the repository the pipeline writes to. The minimum scope for opening issues is the Issues permission set to Read and write; that single grant also lets you add labels and assignees. Pass the token in the `Authorization` header on every request, as described in the [GitHub authentication docs](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api). Store it as an environment variable, never in the script.

```bash
export GH_TOKEN="github_pat_..."   # fine-grained token, Issues: read+write
export GH_REPO="acme/support"      # owner/repo

# Verify the token can reach the repo before running the loop
curl -s -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/$GH_REPO | jq -r '.full_name'
```

## How do I map an email to issue title and body?

Map the email subject to the issue title and the email snippet or body to the issue body, then send both in a JSON payload. The GitHub create-issue endpoint accepts `title`, `body`, `labels`, and `assignees` fields. Build the payload with `jq` so quoting and newlines in the subject stay valid JSON, since a raw subject with a quote would otherwise break the request.

Using `jq` to construct the object is null-safe: a message with no subject falls back to a placeholder instead of opening a blank-titled issue. The loop below reads each message, builds a payload, and POSTs it. GitHub returns HTTP `201 Created` with the new issue's `number` and `html_url`, so you can echo the link for every issue opened.

```bash
jq -c '.[]' items.json | while read -r msg; do
  title=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  from=$(echo "$msg"  | jq -r '.from[0].email // "unknown"')
  snippet=$(echo "$msg" | jq -r '.snippet // ""')

  payload=$(jq -n --arg t "$title" --arg b "From: $from"$'\n\n'"$snippet" \
    --arg lbl "email" \
    '{title: $t, body: $b, labels: [$lbl]}')

  curl -s -X POST https://api.github.com/repos/$GH_REPO/issues \
    -H "Authorization: Bearer $GH_TOKEN" \
    -H "Accept: application/vnd.github+json" \
    -H "X-GitHub-Api-Version: 2022-11-28" \
    -d "$payload" | jq -r '"opened #\(.number): \(.html_url)"'
done
```

## How do I stop duplicate issues on a schedule?

Stop duplicates by scoping the search to a narrow window and keeping a ledger of message IDs you have already filed. Each Nylas message carries a stable `id`, so append every processed ID to a file and skip any ID already present. A daily cron scoped to `newer_than:1d` only ever touches the last 24 hours of mail, and the ledger guards the overlap when a run reprocesses a boundary message.

The check is two lines: `grep` the ledger before POSTing, and append the ID after a successful `201`. GitHub also enforces a secondary rate limit of roughly 80 content-creating requests per minute, so a batch of more than 80 new issues should sleep between calls. For near-real-time intake, drive the same POST from a [message.created webhook](https://cli.nylas.com/guides/parse-inbound-email-webhooks) instead of a poll; the mapping code is identical and only the trigger changes.

```bash
SEEN="$HOME/.gh-issue-ledger"
touch "$SEEN"

jq -c '.[]' items.json | while read -r msg; do
  id=$(echo "$msg" | jq -r '.id')
  grep -qxF "$id" "$SEEN" && continue   # already filed, skip

  title=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  payload=$(jq -n --arg t "$title" --arg b "Filed from email $id" \
    '{title: $t, body: $b, labels: ["email"]}')

  code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
    https://api.github.com/repos/$GH_REPO/issues \
    -H "Authorization: Bearer $GH_TOKEN" \
    -H "Accept: application/vnd.github+json" \
    -d "$payload")

  [ "$code" = "201" ] && echo "$id" >> "$SEEN"
done
```

## Why use the CLI instead of a paid email connector?

Use the CLI because it gives you the raw email as JSON and leaves the GitHub write under your control, with no per-record fee. Hosted email-to-issue connectors typically meter on task count and run on a vendor schedule. The pipeline here runs on your own cron, costs nothing beyond a GitHub token, and pulls the same inbox across Gmail, Outlook, and four other providers through one OAuth grant.

Ownership matters when the mapping gets specific. You can route mail from `support@` to one repository and `security@` to another, set labels from the sender domain, or assign issues by parsing the subject. None of that needs a new connector, only another `jq` filter. The email payload itself follows the address and header rules of [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322), which the tool parses into the structured fields the loop reads. Provider-side behavior is described from documented provider behavior, not from a verified end-to-end test on each backend — verify locally before deploying provider-specific routing.

## Next steps

- [Email to Jira issues](https://cli.nylas.com/guides/email-to-jira-issues) — the same pattern into a Jira project
- [Email to Linear issues](https://cli.nylas.com/guides/email-to-linear-issues) — file issues into Linear instead
- [GitHub Actions email notifications](https://cli.nylas.com/guides/github-actions-email-notifications) — send mail back out from CI
- [Extract email data with jq](https://cli.nylas.com/guides/extract-email-data-jq) — the JSON-shaping toolkit this loop relies on
- [Parse inbound email webhooks](https://cli.nylas.com/guides/parse-inbound-email-webhooks) — open issues in real time
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
