Skip to content
Tier 2 Schema

Parse Linear Notification Emails

Turn Linear issue, project, cycle, and @-mention notification emails into typed JSON — workspace, project, issue ID, issue title, actor, action, due date, and canonical URL. Schema-first.

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

FieldTypeExampleNotes
notification_typeenumassignedOne of assigned, mention, comment, status_change, due_date_reminder, project_update, cycle_update
workspace_slugstringacmeThe Linear workspace slug — the subdomain portion of https://acme.linear.app
project_namestringWebhook ReliabilityThe project the issue belongs to, when the email mentions it
project_slugstringwebhook-reliabilityURL-safe slug for the project
issue_idstring8f2c1a93-4b7e-4d1a-9c0e-2f8b6d4e1a7cLinear’s internal UUID for the issue
issue_identifierstringENG-482The human-friendly issue key — team prefix plus sequential number
issue_titlestringAdd retry backoff to webhook senderIssue title, stripped of the [ENG-482] prefix
actor_namestringAlex RiveraDisplay name of the user who triggered the event
actor_emailstringalex.rivera@acme.comEmail of the actor, when printed in the footer
priorityenumhighOne of urgent, high, medium, low, none
stateenumstartedOne of backlog, unstarted, started, completed, canceled
due_datestring2026-07-21The issue’s due date, ISO 8601, when present in the email
urlstringhttps://linear.app/acme/issue/ENG-482Canonical link to the issue
body_previewstringJordan, can you add exponential backoff to the retry loop?First line of the comment or event body
datestring2026-07-02Notification 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". Treat urgent as 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_date is the assignment-level date, not the cycle date. Linear prints the issue’s due date when the email mentions one (most often on assigned and due_date_reminder notifications). 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.app at a unique inbox address MailFrame assigns you — is planned as well. Until those ship, POST the raw email to /v1/parse as 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.

Other schemas

Ship this schema in production

Define your fields once, then POST raw email to /v1/parse. MailFrame returns typed JSON in the HTTP response or via signed webhook delivery with retries, attempt history, dead-letter, and replay. (Inbox forwarding is on the roadmap.)

Request early access