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

# File Linear Issues from Email (CLI)

Support and bug reports land in a shared inbox, but the work gets tracked in Linear. The manual hop is copy-paste: read the email, open Linear, retype the title and body. This guide closes the loop from the terminal — pull messages as JSON with the CLI, then POST a single GraphQL issueCreate mutation to the Linear API with your team ID. No paid connector, no per-issue fee.

Written by [Nick Barraclough](https://cli.nylas.com/authors/nick-barraclough) Product Manager

Updated June 9, 2026

> **TL;DR:** Pull messages with `nylas email search --json`, then create one Linear issue per message by POSTing a GraphQL `issueCreate` mutation to `https://api.linear.app/graphql` with a `teamId`. The CLI handles the inbox across six providers; one mutation handles the write. The catch most people miss is where the `teamId` comes from — resolved below in two lines of GraphQL.

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 you file a Linear issue from an email?

You file a Linear issue from an email by pulling the message as JSON and sending one GraphQL `issueCreate` mutation. The Nylas CLI returns structured messages with `nylas email search --json`, and a single POST to `https://api.linear.app/graphql` creates the issue. Linear has no REST endpoint — every write goes through one GraphQL URL.

Two setup steps come first. Generate a personal API key in Linear under Settings, API, then export it as `LINEAR_API_KEY`. Linear authenticates with the raw key in the `Authorization` header — no `Bearer` prefix for personal keys, per the [Linear authentication docs](https://developers.linear.app/docs/graphql/working-with-the-graphql-api/authentication). Below, the CLI pulls the latest bug reports into a file so the next step has clean JSON to read.

```bash
# Pull bug-report email from the last day into a JSON file
nylas email search "bug" --after 2026-06-08 --json --limit 50 > items.json
```

## Where does the Linear team ID come from?

The `teamId` is a UUID that scopes an issue to one Linear team, and `issueCreate` rejects the mutation without it. You can read it from the team URL, but the reliable path is a `teams` query against the same GraphQL endpoint. A workspace with 3 teams returns 3 nodes, each with an `id` and a human key like ENG.

Query the endpoint once and cache the result. The `teams` connection returns up to 50 nodes per page by default, which covers most workspaces in a single request. Match on the key you recognize — ENG, SUP, OPS — rather than the UUID, since the key is stable and readable while the UUID is not. The command below pipes the response through `jq` to print key-to-ID pairs.

```bash
curl -s -X POST https://api.linear.app/graphql \
  -H "Authorization: $LINEAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ teams { nodes { id key name } } }"}' \
  | jq -r '.data.teams.nodes[] | "\(.key)\t\(.id)"'
```

## How do you map an email to an issueCreate mutation?

Map the email subject to the issue `title` and the body to `description`, both passed inside the `issueCreate` input alongside the `teamId`. The mutation returns a `success` boolean and the new issue's identifier, so one round trip both creates and confirms. Linear's description field accepts Markdown.

Build the request with GraphQL variables instead of string-interpolating the email text, so a subject containing quotes or a backslash can't break the query. The pattern below reads each message from the JSON, extracts `subject` and `snippet` with `jq`, and sends the same mutation per message. A blank subject falls back to a placeholder so no issue is created untitled. The [Linear mutations reference](https://developers.linear.app/docs/graphql/mutations) documents the full `IssueCreateInput`.

```bash
TEAM_ID="your-team-uuid"
MUTATION='mutation($t:String!,$d:String,$team:String!){ issueCreate(input:{title:$t, description:$d, teamId:$team}){ success issue { identifier url } } }'

jq -c '.[]' items.json | while read -r msg; do
  title=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  body=$(echo "$msg"  | jq -r '.snippet // ""')
  jq -n --arg q "$MUTATION" --arg t "$title" --arg d "$body" --arg team "$TEAM_ID" \
    '{query:$q, variables:{t:$t, d:$d, team:$team}}' \
  | curl -s -X POST https://api.linear.app/graphql \
      -H "Authorization: $LINEAR_API_KEY" \
      -H "Content-Type: application/json" \
      --data @- \
  | jq -r '.data.issueCreate.issue.identifier'
done
```

## How do you avoid filing the same email twice?

Avoid duplicates by scoping the search to a narrow window and recording which messages you have filed. Run the pipeline daily against `--after` set to yesterday, so only the last 24 hours of mail is ever processed. The CLI returns a stable message `id` per message, which is the key you track against a local ledger.

Keep a flat file of processed IDs and skip any message already in it. This survives reruns, partial failures, and an overlapping search window without creating a second Linear issue. For a 50-message daily batch, the ledger check adds under 1 second. Append each ID only after `issueCreate` returns `success: true`, so a failed call retries on the next run.

```bash
touch filed.txt
jq -c '.[]' items.json | while read -r msg; do
  id=$(echo "$msg" | jq -r '.id')
  grep -qx "$id" filed.txt && continue   # already filed, skip
  # ... run the issueCreate mutation here ...
  echo "$id" >> filed.txt
done
```

## Why drive this from a webhook instead of a cron poll?

A webhook files the issue the moment the email arrives, while a cron poll waits for the next scheduled run. For a support team, the gap between a daily poll and a webhook is up to 24 hours of delay before a bug report becomes a tracked Linear issue. The CLI exposes a `message.created` trigger that fires on each new message.

Run a local webhook server with `nylas webhook server`, which opens a tunnel and prints events as they arrive. The same `issueCreate` mapping runs per event — only the trigger changes from a poll to a push. The Linear API key stays separate from the mailbox grant the CLI manages, so you rotate them independently. The command below subscribes to new-message events.

```bash
# Subscribe to new-message events, then map each to an issueCreate call
nylas webhook create --url https://your-app.example.com/hooks --triggers message.created
```

## Next steps

- [Fireflies vs Nylas Notetaker: Meeting Bots](https://cli.nylas.com/guides/fireflies-vs-nylas-notetaker) — Fireflies is a meeting-notes app with a GraphQL API for reading…
- [File Jira issues from email](https://cli.nylas.com/guides/email-to-jira-issues) — the same pattern against the Jira REST API
- [Email to Trello cards](https://cli.nylas.com/guides/email-to-trello-cards) — create cards instead of issues
- [Send email to a Notion database](https://cli.nylas.com/guides/email-to-notion) — file messages as Notion pages
- [Email to Airtable](https://cli.nylas.com/guides/email-to-airtable) — the same pattern into Airtable
- [Extract email data with jq](https://cli.nylas.com/guides/extract-email-data-jq) — the JSON-shaping toolkit
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
- [Linear GraphQL API overview](https://developers.linear.app/docs/graphql/working-with-the-graphql-api) — the endpoint, pagination, and rate limits
- [jq manual](https://jqlang.github.io/jq/manual/) — the JSON filter reference used throughout
- [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) — the internet message format behind every email field
