Plane - open-source альтернатива Jira и Linear для управления проектами. Self-hosted или облако (plane.so). В отличие от Jira - нет вендор-локина и лицензионных расходов на $20-45/пользователя. Для команд, которые ведут продажи в Kommo и доставку в Plane, типичная проблема: сделка закрывается, но проект в Plane создаётся вручную - задержка 1-3 дня, потеря контекста сделки. Кастомная интеграция решает: при переходе сделки в Closed Won автоматически создаётся проект в Plane с набором стартовых задач.
Plane REST API использует заголовок X-Api-Key. Ключевые объекты: Workspace (организация), Project, Issue (задача), Cycle (спринт). Для создания проекта из шаблона в Plane нет нативного API “создать из шаблона” - задачи создаются по списку через POST /api/v1/workspaces/{slug}/projects/{id}/issues/.
Plane Workspace Slug - уникальный идентификатор организации в URL (например my-company из app.plane.so/my-company/). Используется во всех API-вызовах.
Архитектура
Kommo: сделка -> Closed Won
-> Kommo webhook: leads.status.changed (status_id = won)
-> Ваш сервер
Ваш сервер
-> Plane API: создать Project (название = deal.name + клиент)
-> Plane API: создать Issues по шаблону (онбординг чеклист)
-> Plane API: назначить ответственного (matching менеджер -> Plane member)
-> Kommo API: сохранить plane_project_url в custom field сделки
Реализация
import requests, os, json as json_lib
from flask import Flask, request, jsonify
app = Flask(__name__)
PLANE_API_KEY = os.environ["PLANE_API_KEY"]
PLANE_BASE_URL = os.environ.get("PLANE_BASE_URL", "https://api.plane.so")
PLANE_WORKSPACE = os.environ["PLANE_WORKSPACE_SLUG"]
PLANE_TEMPLATE_PROJECT_ID = os.environ["PLANE_TEMPLATE_PROJECT_ID"]
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
KOMMO_WON_STATUS = int(os.environ["KOMMO_WON_STATUS_ID"])
KOMMO_CF_PLANE = int(os.environ["KOMMO_CF_PLANE_URL"])
PLANE_HDR = {"X-Api-Key": PLANE_API_KEY, "Content-Type": "application/json"}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
ONBOARDING_ISSUES = [
{"name": "Kickoff встреча с клиентом", "priority": "urgent"},
{"name": "Настройка доступов и окружения", "priority": "high"},
{"name": "Подготовить технический бриф", "priority": "high"},
{"name": "Согласовать таймлайн и milestone", "priority": "medium"},
{"name": "Завести проект в документации", "priority": "medium"},
{"name": "Выслать клиенту Welcome письмо", "priority": "low"},
]
def get_lead(lead_id: int) -> dict:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts,custom_fields_values"},
)
return r.json()
def create_plane_project(name: str, description: str) -> dict:
payload = {
"name": name[:100], # Plane лимит: 100 символов
"identifier": name[:5].upper().replace(" ", ""), # project key
"description": description,
"network": 2, # 0=Secret, 2=Public
}
r = requests.post(
f"{PLANE_BASE_URL}/api/v1/workspaces/{PLANE_WORKSPACE}/projects/",
headers=PLANE_HDR,
json=payload,
)
r.raise_for_status()
return r.json()
def create_plane_issue(project_id: str, name: str, priority: str, description: str = "") -> str:
# priority: "urgent", "high", "medium", "low", "none"
priority_map = {"urgent": 0, "high": 1, "medium": 2, "low": 3, "none": 4}
payload = {
"name": name,
"description": description,
"priority": priority_map.get(priority, 2),
"state": None, # будет назначен default state
}
r = requests.post(
f"{PLANE_BASE_URL}/api/v1/workspaces/{PLANE_WORKSPACE}/projects/{project_id}/issues/",
headers=PLANE_HDR,
json=payload,
)
if r.status_code == 201:
return r.json().get("id", "")
return ""
def update_kommo_lead(lead_id: int, plane_url: str):
requests.patch(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
json={"custom_fields_values": [{
"field_id": KOMMO_CF_PLANE,
"values": [{"value": plane_url}],
}]},
)
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": lead_id,
"entity_type": "leads",
"note_type": "common",
"params": {"text": f"Plane: проект создан - {plane_url}"},
}],
)
@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
data = request.json or {}
for lead_data in data.get("leads", {}).get("status", []):
lead_id = lead_data.get("id")
new_status = lead_data.get("status_id")
is_won = lead_data.get("old_status_id") != 142 # 142 = won in most Kommo accounts
if new_status != KOMMO_WON_STATUS:
continue
lead = get_lead(lead_id)
deal_name = lead.get("name", f"Сделка #{lead_id}")
deal_value = lead.get("price", 0) or 0
contacts = lead.get("_embedded", {}).get("contacts", [])
company = ""
if contacts:
company = contacts[0].get("name", "")
project_name = f"{deal_name} - {company}" if company else deal_name
description = f"Kommo Deal #{lead_id}. Сумма: ${deal_value}. Источник: Kommo CRM."
project = create_plane_project(project_name, description)
project_id = project.get("id", "")
if not project_id:
continue
for issue_tpl in ONBOARDING_ISSUES:
create_plane_issue(project_id, issue_tpl["name"], issue_tpl["priority"])
# URL проекта в Plane
plane_url = f"https://app.plane.so/{PLANE_WORKSPACE}/projects/{project_id}/issues/"
update_kommo_lead(lead_id, plane_url)
return jsonify({"status": "ok"}), 200
Self-hosted Plane: изменение BASE_URL
Для self-hosted Plane установить PLANE_BASE_URL=https://plane.yourcompany.com. API path остаётся идентичным. Self-hosted версия бесплатна для неограниченного числа пользователей - это ключевое преимущество для команд 10+.
Plane Cycles (спринты) из сделки
Если нужно создать не просто задачи, но и первый спринт:
def create_plane_cycle(project_id: str, name: str) -> str:
import datetime
today = datetime.date.today()
end = today + datetime.timedelta(days=14)
r = requests.post(
f"{PLANE_BASE_URL}/api/v1/workspaces/{PLANE_WORKSPACE}/projects/{project_id}/cycles/",
headers=PLANE_HDR,
json={
"name": name,
"start_date": today.isoformat(),
"end_date": end.isoformat(),
},
)
return r.json().get("id", "") if r.status_code == 201 else ""
Вызвать после создания проекта, затем добавить issues в цикл через POST .../cycles/{cycle_id}/cycle-issues/.
Реальный кейс
Агентство разработки, 5 разработчиков + 2 менеджера продаж. Kommo для продаж, Plane (self-hosted) для проектов. До интеграции: задержка 2-3 дня между закрытием сделки и стартом проекта - разработчики ждут постановки задач. После: при переходе в Closed Won в течение 30 секунд создаётся проект с 6 онбординг-задачами. Команда видит новый проект немедленно.
Для кого актуально
Агентства, IT-команды, консалтинг - компании, где продажи в CRM и исполнение в PM-инструменте. Plane особенно подходит командам, которые хотят избежать Jira-лицензий и имеют возможность self-hosting.
Аналогичные интеграции описаны для Kommo + Notion и Kommo + Linear.
Часто задаваемые вопросы
Как получить Plane API Key?
Plane (облако): Settings -> API Tokens -> Create Token. Self-hosted: Settings -> API Tokens (тот же путь в интерфейсе). Токен создаётся на уровне пользователя - используйте сервисный аккаунт, не личный.
Есть ли лимиты API в Plane?
В облачной версии: 60 запросов в минуту на API Key. Для интеграции с Kommo (создание проекта + 6 задач = ~8 запросов на сделку) - лимит несущественен даже при 50 сделках в день. Self-hosted версия без ограничений.
Как назначить ответственного в Plane issue?
При создании issue добавить "assignees": ["{plane_member_id}"]. Member ID получить через GET /api/v1/workspaces/{slug}/members/. Заранее создайте маппинг: kommo_user_id -> plane_member_id - и назначайте ответственного по responsible_user из сделки Kommo.
Итог
Kommo + Plane - автоматическое создание проекта при Closed Won:
X-Api-Keyheader,POST /api/v1/workspaces/{slug}/projects/- Создание Issues из шаблонного списка с priority
- Сохранить plane_url в Kommo custom field
- Self-hosted: сменить
PLANE_BASE_URL, API path идентичен - Plane бесплатен self-hosted для команд любого размера
Если нужна интеграция Kommo с Plane или другим open-source PM - опишите задачу команде Exceltic.dev.