If you run an engineering team on Linear, your inbox is full of Linear notification emails — someone assigned you, someone @-mentioned you, someone moved an issue from “In Progress” to “Done”, a cycle started, a cycle ended, a project got a status update, a due date is tomorrow. Each notification arrives in its own message with its own subject line and its own little blob of prose, but the structured signal underneath — which workspace, which project, which issue, who acted, why you were notified — is the same every time. After a quarter the question “what is waiting on me in Linear right now?” turns into a Monday-morning scroll through a hundred archived messages across three inboxes.
This post shows how to turn those emails into typed, validated JSON you can route to a triage queue, a chat bot, or a “what just moved on my board” digest — using a JSON Schema and a single POST to MailFrame’s /v1/parse endpoint. If you want the ready-made schema reference, it lives at Parse Linear Notification Emails; this post is the why-and-how around it.
What’s actually in a Linear notification email
Linear encodes a lot of structure into each notification, but most of it is implicit. The signal you actually want is a small, fixed set of fields every Linear email prints somewhere:
- The notification type —
notification_typefrom the body wording. Linear uses distinct templates forassigned,mention,comment,status_change,due_date_reminder,project_update, andcycle_update. This is the field that decides which lane the notification belongs in. - The workspace and project —
workspace_slug(theacmeinhttps://acme.linear.app) andproject_name/project_slug. The workspace is the routing root; the project lets you slice by initiative. - The issue identity —
issue_identifier(the human-friendlyENG-482) andissue_id(Linear’s internal UUID).issue_identifieris the join key across your board, your chat bot, and your dashboard;issue_idis the stable key Linear itself uses. - The issue title —
issue_title, stripped of the[ENG-482]prefix so it slots cleanly into a Slack message or a digest row. - The actor —
actor_nameandactor_emailof the person who triggered the event. Useful for filtering out your own actions and for routing by teammate. - The priority and state —
priority(urgent | high | medium | low | none) andstate(backlog | unstarted | started | completed | canceled). Linear prints both when they apply;priorityis the field you page on,stateis the field you chart. - The due date —
due_datewhen the email mentions one (most often onassignedanddue_date_remindernotifications). The canonical URL (url) and the first line of the comment body (body_preview) round out the message.
The fields worth extracting are roughly:
| Field | Why you want it |
|---|---|
notification_type | Decide which lane, which queue, which Slack channel |
workspace_slug | Route by team — multiple workspaces, one inbox |
project_name / project_slug | Slice by initiative; build a per-project digest |
issue_identifier | Join key across your board, chat bot, and dashboard |
issue_id | Stable key Linear itself uses; safe for downstream IDs |
issue_title | Drop straight into a notification body |
actor_name / actor_email | Filter out your own actions; route by teammate |
priority | Page on urgent; chart on high+urgent |
state | Compute “what’s in progress” and “what just shipped” without re-querying Linear |
due_date | ”What’s due tomorrow?” — turn the reminder into a calendar event |
url | Deep link to drop into Slack, email, or a ticket |
body_preview | Enough context to decide without opening Linear |
date | Notification timestamp, normalized to ISO 8601 |
A JSON Schema for the extraction
MailFrame is schema-first: you declare the exact shape you want before anything is parsed, and every extraction is validated against that contract before it is returned. (For the reasoning behind that design, see Why We Built MailFrame Schema-First.)
Here is a compact schema for Linear notification emails. The enum constraints on notification_type, priority, and state are not hints — they are hard constraints, so a value outside the list fails validation instead of silently landing in your triage database:
{
"$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" }
}
}
This is the same schema MailFrame ships pre-built under the id linear_notification, so you do not have to register it yourself — but copying it here is a good starting point if you want to tighten or extend it for your own routing logic (for example, drop state from required if your team does not use workflow states, or require actor_email if every notification in your workflow must carry a routable contact).
Send the raw email to /v1/parse
MailFrame’s shipped path is direct and synchronous: you POST the raw RFC 822 MIME text of the email — exactly the bytes you received — along with a schema id, and you get typed JSON back in the same HTTP response. No inbox to configure, no callback to wait on.
You can reference the pre-built schema by id (schema_id) or pass an inline JSON Schema. Here it is with curl, using the built-in linear_notification schema:
curl -X POST 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"
}'
The response wraps the extracted, schema-valid data in a data object alongside the parse status and any validation errors:
{
"id": "parse_a91b3e",
"status": "completed",
"validation_errors": [],
"data": {
"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"
}
}
Routing on the result
Because the result is validated against your schema before it reaches you, your application code can stay small. Check validation_errors first, then branch on notification_type and priority — those two fields together are usually all you need to pick a lane:
interface ParseResult {
id: string;
status: "completed" | "failed";
validation_errors: unknown[];
data: LinearNotification;
}
interface LinearNotification {
notification_type:
| "assigned"
| "mention"
| "comment"
| "status_change"
| "due_date_reminder"
| "project_update"
| "cycle_update";
workspace_slug: string;
project_name?: string;
project_slug?: string;
issue_id?: string;
issue_identifier: string;
issue_title?: string;
actor_name?: string;
actor_email?: string;
priority?: "urgent" | "high" | "medium" | "low" | "none";
state?: "backlog" | "unstarted" | "started" | "completed" | "canceled";
due_date?: string;
url?: string;
body_preview?: string;
date?: string;
}
async function processLinearNotification(rawMime: string): Promise<void> {
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
schema_id: "linear_notification",
raw_mime: rawMime,
}),
});
const result: ParseResult = await res.json();
// Validation ran server-side against your schema — surface failures, don't guess.
if (result.status !== "completed" || result.validation_errors.length > 0) {
await flagForReview(result);
return;
}
const n = result.data;
// Page on urgent regardless of notification type.
if (n.priority === "urgent") {
await pageAssignee(n);
}
// Then route by notification type.
switch (n.notification_type) {
case "assigned":
await notifyAssignedLane(n);
break;
case "mention":
await notifyMentionLane(n);
break;
case "due_date_reminder":
// Promote to a calendar event with the canonical URL.
await createCalendarReminder(n);
break;
case "status_change":
await recordStateTransition(n);
break;
case "comment":
case "project_update":
case "cycle_update":
default:
await archive(n);
}
}
A few practical notes:
- Trust the types, not the strings. Because
notification_type,priority, andstateare enum-constrained in the schema, aswitchover them is exhaustive and safe — an unexpected value would have shown up invalidation_errorsrather than reaching this code. - 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 both keys keeps a busy issue from creating multiple rows for the same logical event in your triage queue, while still letting acommentand astatus_changeon the same issue land as separate rows. - Keep the raw MIME intact. Don’t pre-strip headers before sending — the
Subjectline carries the[ENG-482]prefix and the title, theFromheader carries the workspace display name, and the message footer carries the canonical URL and the actor. Send the message as you received it.
Where this fits in your pipeline
The example above POSTs from wherever you already hold the message — an IMAP poller, an inbound-email webhook, a Lambda reading from S3, a filter forwarding your Linear mail to a queue. That keeps ingestion in your own code and under your own retries, which is the model we generally recommend; the trade-offs are laid out in How to Choose an Email Parsing API and the broader email-to-JSON API overview.
Linear notifications are usually a small share of overall dev-tool mail, so most teams wire this schema up alongside the other pre-built parsers — GitHub notification emails go to a triage queue via Parse GitHub Notification Emails, SaaS renewal emails route into a finance-ops dashboard via Parse Subscription Renewal Emails, and Linear notifications end up in a “what just moved on my board” digest or a per-project Slack channel. Browse the rest of the schema library for the full list of pre-built parsers.
For teams that would rather receive results asynchronously, signed webhook delivery (HMAC-SHA256, with retries and delivery history) is available in early access — see Designing Webhooks That Don’t Break for how to verify and handle those payloads. Forwarding your Linear mail to a unique MailFrame inbox address, instead of POSTing it yourself, is on the roadmap; until it ships, the direct POST shown here is the supported path.
The full endpoint reference — request and response shapes, error handling, and language samples — lives in the API docs. If you want to try the Linear schema against your own notification emails, request early access and we’ll help you wire it up.