Guide

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 Senior Engineering Manager

Reviewed by Qasim Muhammad

VerifiedCLI 3.1.17 · Gmail, Outlook · last tested June 9, 2026

Command references used in this guide: nylas email search, nylas email list, and nylas 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.

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.

# 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. Store it as an environment variable, never in the script.

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.

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 instead of a poll; the mapping code is identical and only the trigger changes.

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, 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