Kommo + Linear: automatic creation of development tasks from won deals
Linear is a task tracker for engineering teams that uses a GraphQL API instead of REST. This is a fundamental difference from most other tools: a single endpoint, all operations via mutations. The Kommo integration solves a concrete problem — when a deal closes, the relevant Linear issue is created automatically with data from the CRM, without manually transferring context between the sales team and development.
The problem: sales and development work in different systems
Without integration:
— Won in Kommo -> manager writes in Slack “please create a task for client X”
— Developer opens Linear, manually creates an issue, copies context from the CRM
— A week later the client clarifies details — the manager does not know the status in Linear
— The Kommo deal does not reflect implementation progress: no current data available for the next call
With integration:
— Won in Kommo -> Linear issue created within 3 minutes with full data from the deal
— Issue status change in Linear -> Note in the Kommo card
— Manager sees progress in CRM without opening the tracker
What is synchronized
Kommo -> Linear:
— Deal name -> issue title
— Description / spec from a custom field -> description (supports Markdown)
— Won stage -> issue status “In Progress” or “Backlog” depending on team process
— Contact email and amount -> added to description for context
— Responsible manager -> assigneeId via email -> Linear userId mapping
— Deadline from custom field -> dueDate in ISO 8601
— Kommo deal ID -> stored in description or a separate label for back-tracing
Linear -> Kommo:
— Issue completed (status “Done”) -> Note in the Kommo deal
— Issue assigned to a new person -> Note with the name for the manager
— Task identifier (e.g. ENG-42) -> custom field in the Kommo card
Architecture
Kommo Webhook: deal moved to Won
↓ Backend
1. GET /api/v4/leads/{id} + contacts
-> name, description from custom fields, email, amount
2. Linear GraphQL: mutation issueCreate
-> teamId + title + description + projectId + assigneeId + priority
-> receive issue.id, issue.identifier (e.g. ENG-42), issue.url
3. Kommo: PATCH /leads/{id}
-> custom field linear_issue_id = ENG-42
4. Kommo: POST /leads/{id}/notes
-> "Linear task created: ENG-42. Link: {url}"
Linear Webhook: issue.action = update, state.name = Done
↓ Backend
1. From payload: identifier, assignee, state
2. Find kommo_deal_id by linear_issue_id in storage (Redis / DB)
3. Kommo: POST /leads/{deal_id}/notes
-> "Task ENG-42 closed in Linear. Assignee: {assignee}"
Linear GraphQL API: key requests
Endpoint: https://api.linear.app/graphql. Authentication: Authorization: Bearer <api_key>. All requests are POST with body {"query": "..."} or {"query": "...", "variables": {...}}.
Get list of teams (teamId is required):
import requests
LINEAR_URL = 'https://api.linear.app/graphql'
HEADERS = {
'Authorization': f'Bearer {LINEAR_API_KEY}',
'Content-Type': 'application/json'
}
def get_teams() -> list:
query = """
query {
teams {
nodes {
id
name
key
}
}
}
"""
resp = requests.post(LINEAR_URL, json={'query': query}, headers=HEADERS)
return resp.json()['data']['teams']['nodes']
# [{'id': 'abc-123', 'name': 'Engineering', 'key': 'ENG'}, ...]
Create an issue from deal data:
def create_issue_from_deal(
team_id: str,
project_id: str,
title: str,
description: str,
assignee_id: str = None,
priority: int = 2 # 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
) -> dict:
mutation = """
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue {
id
identifier
url
}
}
}
"""
variables = {
'input': {
'title': title,
'description': description,
'teamId': team_id,
'projectId': project_id,
'priority': priority
}
}
if assignee_id:
variables['input']['assigneeId'] = assignee_id
resp = requests.post(
LINEAR_URL,
json={'query': mutation, 'variables': variables},
headers=HEADERS
)
result = resp.json()['data']['issueCreate']
if result['success']:
return result['issue'] # {'id': '...', 'identifier': 'ENG-42', 'url': '...'}
raise RuntimeError(f'Linear issue creation failed: {resp.text}')
Get team projects:
def get_team_projects(team_id: str) -> list:
query = """
query TeamProjects($teamId: String!) {
team(id: $teamId) {
projects {
nodes {
id
name
state
}
}
}
}
"""
resp = requests.post(
LINEAR_URL,
json={'query': query, 'variables': {'teamId': team_id}},
headers=HEADERS
)
return resp.json()['data']['team']['projects']['nodes']
Handle Linear webhook:
from flask import Flask, request
import hmac, hashlib
app = Flask(__name__)
@app.route('/webhooks/linear', methods=['POST'])
def linear_webhook():
# Signature verification (recommended for production)
signature = request.headers.get('X-Linear-Signature', '')
secret = LINEAR_WEBHOOK_SECRET.encode()
expected = hmac.new(secret, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return 'Invalid signature', 401
payload = request.json
action = payload.get('action') # 'create' | 'update' | 'remove'
entity_type = payload.get('type') # 'Issue' | 'Comment' | 'Project'
data = payload.get('data', {})
if entity_type == 'Issue' and action == 'update':
state = data.get('state', {})
if state.get('name') == 'Done':
identifier = data.get('identifier') # 'ENG-42'
assignee = data.get('assignee', {}).get('name', 'unknown')
deal_id = get_deal_id_by_linear_issue(identifier)
if deal_id:
create_kommo_note(
deal_id,
f'Task {identifier} closed in Linear. Assignee: {assignee}.'
)
return '', 200
Linear webhook setup: Settings -> API -> Webhooks -> Create webhook. Specify the URL, select event types (Issue), and the team. The webhook secret is used to verify the X-Linear-Signature HMAC-SHA256.
Field mapping: Kommo -> Linear
| Kommo | Linear | Notes |
|---|---|---|
| Deal name | title | Direct mapping |
| Custom field “Spec” | description | Markdown is supported |
| Deal amount | In description | Linear has no native “amount” field |
| Responsible manager | assigneeId | Requires email -> Linear userId mapping |
| Deadline from custom field | dueDate | ISO 8601, optional field |
| Deal label/type | priority | 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low |
User mapping is a separate configuration step. Linear has no “external ID” field, so you need to maintain a table of {kommo_user_email -> linear_user_id}. Retrieve users:
query = """query { users { nodes { id name email } } }"""
Real-world case
B2B SaaS company (EU market, 30–40 new clients per quarter, separate sales and development teams):
- Before: Won in Kommo -> manager sent an email to development with a description. Average time from Won to a Linear issue — 1–2 days. Issues were often created without a description or with incorrect priorities.
- After: Won -> Linear issue within 3 minutes with the full spec from the “Spec” field, the correct project, and High priority. The manager sees the Note “Task ENG-42 created” directly in Kommo.
- Additional effect: when the task is completed, the manager automatically receives a signal to make a service call to the client — no separate reminders or Slack messages needed.
If the team uses ClickUp — it has a REST API, not GraphQL: a different stack, the same logic. For non-technical teams with broader workflows — see Monday.com.
Who this is relevant for
- Development team works in Linear, sales in Kommo
- Need automatic transfer of deal context to a task without manual copying
- A Won client requires onboarding or technical implementation — the process must start immediately
- Important to see Linear task progress directly from the Kommo card without switching tools
Frequently asked questions
Linear API — REST or GraphQL?
GraphQL. Single endpoint https://api.linear.app/graphql, all operations — POST with a GraphQL query body. This distinguishes Linear from most SaaS trackers: Jira, Asana, and ClickUp use REST.
How do I get teamId and projectId for the API?
Via a GraphQL query: query { teams { nodes { id name } } } — returns a list of teams with UUIDs. Similarly for projects: team(id: "...") { projects { nodes { id name } } }. These IDs are stored in the integration config and do not change.
Does Linear support priorities in issueCreate?
Yes. The priority field accepts: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low. When integrating with Kommo, it is convenient to map a deal label or type to a task priority — for example, “Enterprise client” -> Urgent.
How do I verify a webhook from Linear?
Linear signs payloads via HMAC-SHA256. The header X-Linear-Signature contains the hex digest of the request body signed with the webhook secret. Verification: hmac.new(secret, request.data, sha256).hexdigest() == signature. Optional but mandatory for production.
Is OAuth required or is an API key sufficient?
A personal API key is sufficient for server-side integration: Settings -> API -> Personal API keys. OAuth 2.0 is only needed if you are building a public app for multiple Linear accounts. For a single-workspace integration — an API key is simpler and more reliable.
Summary
- Linear GraphQL API: Bearer token, single endpoint, mutations
issueCreate/issueUpdate issueCreateaccepts teamId, projectId, assigneeId, priority, dueDate — data from the Kommo deal- Webhook payload: action + type + data — filter by
type=Issue,state.name=Done - Kommo -> Linear user mapping by email — a separate configuration step
- Typical development timeline — 1–2 weeks
If your development team works in Linear and sales in Kommo — describe the setup: which pipeline stages create tasks, and to which project and team. Exceltic.dev will configure the mapping and two-way event handling.