Linear is the issue tracker half the engineering world has migrated to, and it
emails you for nearly every event that touches an issue assigned to you —
someone assigned you, someone @-mentioned you, someone changed status, a cycle
started or ended, a due date is approaching, a project was updated. The signal
you actually want (which workspace, which project, which issue, who acted, and
why you were notified) is spread across the Subject line, the From header,
and the message footer. MailFrame extracts those fields into a typed,
schema-validated JSON object you can route to a triage queue, a chat bot, or
your own “what just moved on my board” dashboard.
It works on the body of Linear.app issue, project, cycle, and @-mention
notification emails — typically sent from notifications@linear.app or
replies@linear.app from your workspace’s display name (e.g.
Acme Engineering <notifications@linear.app>). Match on the message body and
the Linear-specific wording (“assigned you”, “mentioned you”, “due”,
“moved to”, “cycle”, “project update”) rather than the From address alone,
because workspace-specific display names vary. The workspace and project
slugs, issue identifier (ENG-482), issue title, actor, priority, state,
due date, and canonical URL all normalize into one consistent shape
regardless of which notification type or workspace sent the message.
PDF, image, and calendar-attachment input are on the roadmap; inbox
forwarding is planned.
Fields MailFrame extracts
| Field | Type | Example | Notes |
|---|---|---|---|
notification_type | enum | assigned | One of assigned, mention, comment, status_change, due_date_reminder, project_update, cycle_update |
workspace_slug | string | acme | The Linear workspace slug — the subdomain portion of https://acme.linear.app |
project_name | string | Webhook Reliability | The project the issue belongs to, when the email mentions it |
project_slug | string | webhook-reliability | URL-safe slug for the project |
issue_id | string | 8f2c1a93-4b7e-4d1a-9c0e-2f8b6d4e1a7c | Linear’s internal UUID for the issue |
issue_identifier | string | ENG-482 | The human-friendly issue key — team prefix plus sequential number |
issue_title | string | Add retry backoff to webhook sender | Issue title, stripped of the [ENG-482] prefix |
actor_name | string | Alex Rivera | Display name of the user who triggered the event |
actor_email | string | alex.rivera@acme.com | Email of the actor, when printed in the footer |
priority | enum | high | One of urgent, high, medium, low, none |
state | enum | started | One of backlog, unstarted, started, completed, canceled |
due_date | string | 2026-07-21 | The issue’s due date, ISO 8601, when present in the email |
url | string | https://linear.app/acme/issue/ENG-482 | Canonical link to the issue |
body_preview | string | Jordan, can you add exponential backoff to the retry loop? | First line of the comment or event body |
date | string | 2026-07-02 | Notification date normalized to ISO 8601 |
Sample input
A typical Linear assignment notification looks like this:
From: Acme Engineering <notifications@linear.app>
Subject: [ENG-482] Add retry backoff to webhook sender
Date: Thu, 02 Jul 2026 09:14:00 -0700
To: jordan.rivera@acme.com
Message-ID: <linear.issue.8f2c1a93-4b7e-4d1a-9c0e-2f8b6d4e1a7c@linear.app>
Alex Rivera assigned this issue to you.
Project: Webhook Reliability
Priority: High
Due date: Jul 21, 2026
The retry loop in webhook_sender.rb doubles the delay on each attempt but
never caps it, so a flaky endpoint could stall the whole worker.
View issue:
https://linear.app/acme/issue/ENG-482
—
You're receiving this because you were assigned to ENG-482.
The same shape is produced for mentions, comments, status changes, due-date
reminders, project updates, and cycle updates — notification_type
distinguishes them, while priority, state, and due_date are populated
only when the email actually mentions them. A mention carries the
@-mentioned user, a status_change carries the new state, a
due_date_reminder always carries due_date, and a project_update or
cycle_update may not carry an issue_identifier at all (those rows land
in validation_errors and you can choose to widen the schema or skip them).
Structured JSON output
{
"notification_type": "assigned",
"workspace_slug": "acme",
"project_name": "Webhook Reliability",
"project_slug": "webhook-reliability",
"issue_id": "8f2c1a93-4b7e-4d1a-9c0e-2f8b6d4e1a7c",
"issue_identifier": "ENG-482",
"issue_title": "Add retry backoff to webhook sender",
"actor_name": "Alex Rivera",
"actor_email": "alex.rivera@acme.com",
"priority": "high",
"due_date": "2026-07-21",
"url": "https://linear.app/acme/issue/ENG-482",
"body_preview": "The retry loop in webhook_sender.rb doubles the delay on each attempt but never caps it, so a flaky endpoint could stall the whole worker.",
"date": "2026-07-02"
}
JSON Schema definition
Every field is validated against the schema before MailFrame returns it.
You can copy this as a starting point and tighten it for your own use
case — for example, require actor_email if you want every notification
to carry a routable contact, or restrict notification_type to
{"assigned", "mention"} if those are the only two you act on:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "linear_notification",
"type": "object",
"required": [
"notification_type",
"workspace_slug",
"issue_identifier"
],
"properties": {
"notification_type": {
"type": "string",
"enum": ["assigned", "mention", "comment", "status_change", "due_date_reminder", "project_update", "cycle_update"]
},
"workspace_slug": { "type": "string", "minLength": 1 },
"project_name": { "type": "string" },
"project_slug": { "type": "string" },
"issue_id": { "type": "string" },
"issue_identifier": { "type": "string", "pattern": "^[A-Z][A-Z0-9]*-[0-9]+$" },
"issue_title": { "type": "string" },
"actor_name": { "type": "string" },
"actor_email": { "type": "string", "format": "email" },
"priority": {
"type": "string",
"enum": ["urgent", "high", "medium", "low", "none"]
},
"state": {
"type": "string",
"enum": ["backlog", "unstarted", "started", "completed", "canceled"]
},
"due_date": { "type": "string", "format": "date" },
"url": { "type": "string", "format": "uri" },
"body_preview": { "type": "string" },
"date": { "type": "string", "format": "date" }
}
}
Parse via the API
POST the raw email (MIME or plain text) to /v1/parse with the schema you
want to extract against:
curl https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer ${MAILFRAME_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "linear_notification",
"raw_mime": "From: Acme Engineering <notifications@linear.app>\r\nTo: jordan.rivera@acme.com\r\nSubject: [ENG-482] Add retry backoff to webhook sender\r\nDate: Thu, 02 Jul 2026 09:14:00 -0700\r\n\r\nAlex Rivera assigned this issue to you.\r\n\r\nProject: Webhook Reliability\r\nPriority: High\r\nDue date: Jul 21, 2026\r\n\r\nThe retry loop in webhook_sender.rb doubles the delay on each attempt but never caps it, so a flaky endpoint could stall the whole worker.\r\n\r\nView issue:\r\nhttps://linear.app/acme/issue/ENG-482"
}'
MailFrame returns the typed JSON in the same HTTP response. Prefer asynchronous
delivery? Signed webhook delivery — where MailFrame POSTs the extraction result to
your endpoint with an HMAC-SHA256 signature in the MailFrame-Signature header and
exponential-backoff retries — is available during early access.
Operational notes
- Dedup on
issue_identifier+notification_type. A single issue produces one email per event (an assignment, then a comment, then a status change, then a due-date reminder). Grouping on(issue_identifier, notification_type)keeps a busy issue from creating multiple rows for the same logical event in your triage queue, while still letting a comment and a status_change on the same issue land as separate rows. - Page on
priority == "urgent". Treaturgentas a high-signal routing flag — page the assignee, post to a #pager-duty channel, or open a Linear “Urgent” view — instead of filing it alongside the standard-priority queue. due_dateis the assignment-level date, not the cycle date. Linear prints the issue’s due date when the email mentions one (most often onassignedanddue_date_remindernotifications). For cycle-wide dates, derive them from your own cycle configuration rather than from the email body.- Body-only today. MailFrame reads the fields printed in the email body. PDF, image, and calendar-attachment input are on the roadmap; inbox forwarding — pointing a Gmail or Outlook filter for
notifications@linear.appat a unique inbox address MailFrame assigns you — is planned as well. Until those ship, POST the raw email to/v1/parseas shown above.
Parsing other dev-tool notifications? See the GitHub notification schema and the subscription renewal schema, browse the full schema library, or read the API docs and pricing for the free-tier limits.