| name | extended |
| description | Create and manage Linear documents and project milestones |
Overview
This skill extends Linear MCP capabilities by adding write operations for Documents and ProjectMilestones. While Linear MCP provides read-only access to documents and no milestone support, this skill enables full CRUD operations via direct GraphQL API calls.
What this skill adds:
- Document creation, updates, and deletion
- Project milestone management (create, update, delete, list, get)
- Direct GraphQL access for advanced operations
Prerequisites:
- Linear API Key from https://linear.app/settings/api
- Set environment variable:
export LINEAR_API_KEY="lin_api_xxxxx" - Optional:
jqfor JSON formatting
Document Operations
Creating a Document
Basic example:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentCreate($input: DocumentCreateInput!) { documentCreate(input: $input) { success document { id title url slugId createdAt creator { name } } } }",
"variables": {
"input": {
"title": "API Design Document"
}
}
}'
With content and project:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentCreate($input: DocumentCreateInput!) { documentCreate(input: $input) { success document { id title url slugId } } }",
"variables": {
"input": {
"title": "Q4 Roadmap",
"content": "# Q4 Goals\n\n- Launch feature X\n- Improve performance by 30%",
"projectId": "PROJECT_ID_HERE",
"color": "#FF6B6B"
}
}
}'
Available parameters:
title(required): Document titlecontent: Markdown contentprojectId: Attach to projectinitiativeId: Attach to initiativeissueId: Attach to issuecolor: Icon color (hex format)icon: Icon emoji or name (optional, some emojis may not be valid - omit if validation fails)sortOrder: Display order (float)
Updating a Document
Update title and content:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) { documentUpdate(id: $id, input: $input) { success document { id title updatedAt updatedBy { name } } } }",
"variables": {
"id": "DOCUMENT_ID_OR_SLUG",
"input": {
"title": "Updated Title",
"content": "# Updated Content\n\nNew information here."
}
}
}'
Move to trash:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) { documentUpdate(id: $id, input: $input) { success } }",
"variables": {
"id": "DOCUMENT_ID",
"input": {
"trashed": true
}
}
}'
Available update parameters:
title: New titlecontent: New markdown contentcolor: New icon coloricon: New icontrashed: Move to trash (true) or restore (false)projectId: Move to different projectsortOrder: Update display order
Deleting a Document
Permanently delete (archive):
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentDelete($id: String!) { documentDelete(id: $id) { success } }",
"variables": {
"id": "DOCUMENT_ID"
}
}'
Restore archived document:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentUnarchive($id: String!) { documentUnarchive(id: $id) { success entity { id title } } }",
"variables": {
"id": "DOCUMENT_ID"
}
}'
Project Milestone Operations
Creating a Milestone
Basic milestone:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { id name status progress targetDate project { id name } } } }",
"variables": {
"input": {
"projectId": "PROJECT_ID_HERE",
"name": "Beta Release"
}
}
}'
With description and target date:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { id name status progress targetDate } } }",
"variables": {
"input": {
"projectId": "PROJECT_ID_HERE",
"name": "MVP Launch",
"description": "# MVP Goals\n\n- Core features complete\n- 10 beta users onboarded",
"targetDate": "2025-06-30"
}
}
}'
Interactive approach (using AskUserQuestion):
When user doesn't specify a target date, use AskUserQuestion to ask:
// Step 1: Ask user for target date
AskUserQuestion({
questions: [{
question: "What is the target date for this milestone?",
header: "Target Date",
multiSelect: false,
options: [
{
label: "End of this month",
description: "Set target date to the last day of current month"
},
{
label: "End of next month",
description: "Set target date to the last day of next month"
},
{
label: "Custom date",
description: "I'll specify a custom date in YYYY-MM-DD format"
},
{
label: "No target date",
description: "Create milestone without a specific target date"
}
]
}]
})
// Step 2: Based on user's answer, construct the mutation
// If custom date selected, prompt for YYYY-MM-DD format
// If no target date, omit targetDate from input
Available parameters:
projectId(required): Parent project IDname(required): Milestone namedescription: Markdown descriptiontargetDate: Target date (YYYY-MM-DD format)sortOrder: Display order (float)
Status values (auto-calculated):
unstarted: No progress yetnext: Next milestone to work onoverdue: Past target datedone: All issues completed
Updating a Milestone
Update name and target date:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneUpdate($id: String!, $input: ProjectMilestoneUpdateInput!) { projectMilestoneUpdate(id: $id, input: $input) { success projectMilestone { id name status targetDate } } }",
"variables": {
"id": "MILESTONE_ID",
"input": {
"name": "MVP Launch - Extended",
"targetDate": "2025-07-15"
}
}
}'
Available update parameters:
name: New namedescription: New markdown descriptiontargetDate: New target date (YYYY-MM-DD)sortOrder: New display order
Listing Milestones
List all milestones:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query ProjectMilestones($first: Int) { projectMilestones(first: $first) { nodes { id name status progress targetDate project { id name } issues { nodes { id title } } } } }",
"variables": {
"first": 50
}
}'
List milestones for specific project:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query Project($id: String!) { project(id: $id) { id name projectMilestones { nodes { id name status progress targetDate } } } }",
"variables": {
"id": "PROJECT_ID"
}
}'
Getting a Single Milestone
Detailed milestone info:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query ProjectMilestone($id: String!) { projectMilestone(id: $id) { id name description status progress progressHistory currentProgress targetDate createdAt updatedAt project { id name state } issues { nodes { id title state { name type } assignee { name } } } } }",
"variables": {
"id": "MILESTONE_ID"
}
}'
Deleting a Milestone
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneDelete($id: String!) { projectMilestoneDelete(id: $id) { success } }",
"variables": {
"id": "MILESTONE_ID"
}
}'
Moving a Milestone to Another Project
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneMove($id: String!, $input: ProjectMilestoneMoveInput!) { projectMilestoneMove(id: $id, input: $input) { success projectMilestone { id name project { id name } } } }",
"variables": {
"id": "MILESTONE_ID",
"input": {
"projectId": "NEW_PROJECT_ID"
}
}
}'
Usage Guidelines
When to use this skill
Document operations:
- User asks to "create a document" or "write a doc"
- User wants to "update document content"
- User needs to "delete" or "archive" a document
- User wants to "move document to trash" or "restore document"
Milestone operations:
- User asks to "create a milestone" or "add milestone"
- User wants to "set target date for milestone"
- User needs to "update milestone status" or "rename milestone"
- User asks to "list project milestones" or "show milestone progress"
- User wants to "delete milestone" or "move milestone to another project"
IMPORTANT for Milestones:
- Always use AskUserQuestion to ask for targetDate when creating or updating milestones
- Ask the user to provide a target date in YYYY-MM-DD format
- Validate the date format before making the API call
- If user doesn't provide a date, milestone can be created without targetDate (optional)
How to use
Always check for LINEAR_API_KEY:
if [ -z "$LINEAR_API_KEY" ]; then echo "Error: LINEAR_API_KEY not set. Get key from https://linear.app/settings/api" exit 1 fiGet IDs first:
- Use Linear MCP's
list_projectsto get project IDs - Use Linear MCP's
list_issuesto get issue IDs - Use
list_documentsto get document IDs/slugs
- Use Linear MCP's
For milestone operations, use AskUserQuestion:
- When creating a milestone, ask for targetDate using AskUserQuestion tool
- Example question: "What is the target date for this milestone? (YYYY-MM-DD format, or leave empty for no date)"
- Parse the user's response and include in the mutation
- If user provides empty/no date, omit targetDate from the input
Handle JSON carefully:
- Escape newlines in markdown: use
\n - Escape quotes: use
\" - For complex content, consider using heredoc or jq
- Escape newlines in markdown: use
Check responses:
- Always verify
success: truein mutation responses - If
success: false, check theerrorsarray - Show the document/milestone URL when available
- Always verify
Handle icon field carefully:
- The
iconfield is optional for documents - Some emojis may fail validation with "icon is not a valid icon" error
- If icon validation fails, omit the field and retry
- Linear API only accepts certain emojis - no definitive list available
- The
Format output for user:
- Use
jqto pretty-print JSON - Extract key fields like
id,url,status - Provide actionable next steps
- Use
Error Handling
Authentication errors:
{
"errors": [
{
"message": "Authentication required",
"extensions": { "code": "UNAUTHENTICATED" }
}
]
}
→ Check if LINEAR_API_KEY is set and valid
Not found errors:
{
"errors": [
{
"message": "Resource not found",
"extensions": { "code": "NOT_FOUND" }
}
]
}
→ Verify the ID exists using list operations first
Validation errors:
{
"data": {
"documentCreate": {
"success": false
}
},
"errors": [
{
"message": "Title is required",
"path": ["documentCreate", "input", "title"]
}
]
}
→ Check required fields are provided
Rate limiting:
{
"errors": [
{
"message": "Rate limit exceeded",
"extensions": { "code": "RATE_LIMITED" }
}
]
}
→ Wait and retry after a few seconds
Icon validation errors:
{
"errors": [
{
"message": "Argument Validation Error",
"extensions": {
"code": "INVALID_INPUT",
"validationErrors": [
{
"property": "icon",
"constraints": {
"customValidation": "icon is not a valid icon"
}
}
]
}
}
]
}
→ Omit the icon field or try a different emoji/icon name. Linear API only accepts certain emojis.
Examples
Example 1: Create a project document with content
User request: "Create a technical spec document for project abc123 with an overview section"
Response:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentCreate($input: DocumentCreateInput!) { documentCreate(input: $input) { success document { id title url } } }",
"variables": {
"input": {
"title": "Technical Specification",
"content": "# Overview\n\nThis document outlines the technical architecture and implementation details.\n\n## Architecture\n\nTBD\n\n## Implementation\n\nTBD",
"projectId": "abc123"
}
}
}' | jq '.'
Example 2: Create milestone with target date (user specified)
User request: "Add a 'Beta Launch' milestone to project xyz789, target date is March 31, 2025"
Response:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { id name status targetDate project { name } } } }",
"variables": {
"input": {
"projectId": "xyz789",
"name": "Beta Launch",
"targetDate": "2025-03-31"
}
}
}' | jq '.'
Example 2b: Create milestone asking for target date
User request: "Create a milestone 'Phase 1 Complete' for project xyz789"
Step 1 - Use AskUserQuestion to ask for target date:
Ask user: "What is the target date for the 'Phase 1 Complete' milestone?"
Options:
- Specific date (YYYY-MM-DD format)
- No target date
Step 2 - If user provides date (e.g., "2025-06-30"):
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { id name status targetDate project { name } } } }",
"variables": {
"input": {
"projectId": "xyz789",
"name": "Phase 1 Complete",
"targetDate": "2025-06-30"
}
}
}' | jq '.'
Step 2 - If user provides no date:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { id name status project { name } } } }",
"variables": {
"input": {
"projectId": "xyz789",
"name": "Phase 1 Complete"
}
}
}' | jq '.'
Example 3: Update document content
User request: "Update document doc_abc123 to add a new section about testing"
Response:
# First, get current content using Linear MCP's get_document
# Then update with new content:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) { documentUpdate(id: $id, input: $input) { success document { id updatedAt } } }",
"variables": {
"id": "doc_abc123",
"input": {
"content": "[EXISTING_CONTENT]\n\n## Testing\n\n- Unit tests\n- Integration tests\n- E2E tests"
}
}
}' | jq '.'
Example 4: List milestones with progress
User request: "Show me all milestones and their progress"
Response:
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query { projectMilestones(first: 50) { nodes { name status progress targetDate project { name } issues { nodes { id } } } } }"
}' | jq '.data.projectMilestones.nodes[] | "\(.project.name) - \(.name): \(.progress * 100)% complete (target: \(.targetDate // "No date set"))"'
Combining with Linear MCP
This skill works best alongside the official Linear MCP server:
Linear MCP provides (read operations):
list_documents- Get existing documentsget_document- Read document contentlist_projects- Get project IDslist_issues- Get issue IDslist_teams- Get team info
This skill adds (write operations):
documentCreate- Create new documentsdocumentUpdate- Update documentsdocumentDelete- Delete documentsprojectMilestoneCreate- Create milestonesprojectMilestoneUpdate- Update milestonesprojectMilestoneDelete- Delete milestonesprojectMilestonesquery - List milestones (not in MCP)
Typical workflow:
- Use Linear MCP to list projects → Get project ID
- Use this skill to create a document for that project
- Use Linear MCP to verify the document appears in listings
- Use this skill to create milestones for the project
- Use this skill to query milestone progress
Advanced Usage
Using jq for complex operations
Extract just the document URL:
curl -X POST ... | jq -r '.data.documentCreate.document.url'
Format milestone list as table:
curl -X POST ... | jq -r '.data.projectMilestones.nodes[] | [.name, .status, (.progress * 100 | tostring + "%")] | @tsv'
Using heredoc for large content
CONTENT=$(cat <<'EOF'
# Architecture Design
## Overview
System architecture overview here.
## Components
- API Gateway
- Service Mesh
- Data Layer
EOF
)
# Create temp file with Python for proper JSON encoding
TEMP_FILE=$(mktemp)
python3 << PEOF > "$TEMP_FILE"
import json
data = {
"query": """mutation DocumentCreate(\$input: DocumentCreateInput!) {
documentCreate(input: \$input) {
success
document { id url }
}
}""",
"variables": {
"input": {
"title": "Architecture Design",
"content": """$CONTENT"""
}
}
}
print(json.dumps(data, ensure_ascii=False))
PEOF
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d @"$TEMP_FILE" | jq '.'
rm "$TEMP_FILE"
References
For detailed schema information:
- @references/document-schema.md - Complete Document type definitions
- Search patterns:
grep "DocumentCreateInput\|DocumentUpdateInput\|icon\|color" references/document-schema.md
- Search patterns:
- @references/milestone-schema.md - Complete ProjectMilestone type definitions
- Search patterns:
grep "ProjectMilestoneCreateInput\|ProjectMilestoneUpdateInput\|targetDate" references/milestone-schema.md
- Search patterns:
- @references/examples.md - Additional usage examples
- Search patterns:
grep "Example\|mutation\|query" references/examples.md
- Search patterns:
For the original GraphQL schema: