{
	"name": "аллоя — звонок из рабочего процесса",
	"active": false,
	"nodes": [
		{
			"id": "manual",
			"name": "Запустить вручную",
			"type": "n8n-nodes-base.manualTrigger",
			"typeVersion": 1,
			"position": [0, 300],
			"parameters": {}
		},
		{
			"id": "request",
			"name": "Параметры звонка",
			"type": "n8n-nodes-base.set",
			"typeVersion": 3.4,
			"position": [220, 300],
			"parameters": {
				"mode": "raw",
				"jsonOutput": "{\n  \"mode\": \"discover\",\n  \"assistant_id\": \"\",\n  \"phone_number_id\": \"\",\n  \"called_number\": \"\",\n  \"context\": \"\",\n  \"client_reference\": \"\",\n  \"approval_expires_at\": \"\"\n}",
				"options": {}
			}
		},
		{
			"id": "assistants",
			"name": "Ассистенты",
			"type": "@n8n/n8n-nodes-langchain.mcpClient",
			"typeVersion": 1.1,
			"position": [440, 300],
			"retryOnFail": false,
			"parameters": {
				"serverTransport": "httpStreamable",
				"endpointUrl": "https://alloya.ru/mcp",
				"authentication": "bearerAuth",
				"tool": { "__rl": true, "value": "list_assistants", "mode": "id" },
				"inputMode": "json",
				"jsonInput": "{\"limit\":100}",
				"options": { "timeout": 10000 }
			}
		},
		{
			"id": "numbers",
			"name": "Номера",
			"type": "@n8n/n8n-nodes-langchain.mcpClient",
			"typeVersion": 1.1,
			"position": [660, 300],
			"retryOnFail": false,
			"parameters": {
				"serverTransport": "httpStreamable",
				"endpointUrl": "https://alloya.ru/mcp",
				"authentication": "bearerAuth",
				"tool": { "__rl": true, "value": "list_phone_numbers", "mode": "id" },
				"inputMode": "json",
				"jsonInput": "{\"limit\":100}",
				"options": { "timeout": 10000 }
			}
		},
		{
			"id": "review",
			"name": "Проверить запрос",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [880, 300],
			"parameters": {
				"jsCode": "const input = $('Параметры звонка').first().json;\nconst assistants = $('Ассистенты').first().json.structuredContent?.result;\nconst numbers = $input.first().json.structuredContent?.result;\nif (!Array.isArray(assistants) || !Array.isArray(numbers)) throw new Error('Не удалось прочитать каталог аллоя');\nif (input.mode === 'discover') return [{ json: { should_dial: false, assistants, phone_numbers: numbers, catalog_limit: 100 } }];\nif (input.mode !== 'call') throw new Error('Укажите mode: discover или call');\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nfor (const key of ['assistant_id', 'phone_number_id']) {\n  if (typeof input[key] !== 'string' || !uuid.test(input[key])) throw new Error('Выберите ассистента и исходящий номер по их ID');\n}\nif (typeof input.called_number !== 'string' || !/^(?:\\+7\\d{10}|\\+375\\d{9})$/.test(input.called_number)) throw new Error('Номер получателя нужен в формате E.164 для России или Беларуси');\nif (typeof input.context !== 'string' || !input.context.trim() || input.context.length > 1000) throw new Error('Укажите цель звонка: от 1 до 1000 символов');\nif (typeof input.client_reference !== 'string' || !input.client_reference.trim() || input.client_reference.length > 255 || input.client_reference !== input.client_reference.trim()) throw new Error('Укажите постоянный client_reference без пробелов по краям, до 255 символов');\nif (typeof input.approval_expires_at !== 'string' || !/(?:Z|[+-]\\d{2}:\\d{2})$/.test(input.approval_expires_at)) throw new Error('Укажите срок подтверждения с часовым поясом');\nconst expires = Date.parse(input.approval_expires_at);\nif (!Number.isFinite(expires) || expires <= Date.now() || expires > Date.now() + 30 * 60 * 1000) throw new Error('Подтверждение должно действовать сейчас и не дольше 30 минут');\nconst assistant = assistants.find((row) => row.id === input.assistant_id);\nconst source = numbers.find((row) => row.id === input.phone_number_id);\nif (!assistant || !source || source.can_place_calls !== true) throw new Error('Выбранный ассистент или исходящий номер недоступен в этом каталоге');\nreturn [{ json: { should_dial: true, request: { called_number: input.called_number, assistant_id: input.assistant_id, phone_number_id: input.phone_number_id, context: input.context.trim(), client_reference: input.client_reference }, approval_expires_at: input.approval_expires_at, review: { assistant: assistant.name, source: source.phone_number, destination: input.called_number, purpose: input.context.trim(), client_reference: input.client_reference } } }];"
			}
		},
		{
			"id": "authorized",
			"name": "Звонок подтверждён",
			"type": "n8n-nodes-base.if",
			"typeVersion": 2.2,
			"position": [1100, 300],
			"parameters": {
				"conditions": {
					"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
					"conditions": [
						{
							"id": "approved",
							"leftValue": "={{ $json.should_dial }}",
							"rightValue": "",
							"operator": { "type": "boolean", "operation": "true", "singleValue": true }
						}
					],
					"combinator": "and"
				},
				"options": {}
			}
		},
		{
			"id": "dial",
			"name": "Позвонить",
			"type": "@n8n/n8n-nodes-langchain.mcpClient",
			"typeVersion": 1.1,
			"position": [1320, 200],
			"retryOnFail": false,
			"parameters": {
				"serverTransport": "httpStreamable",
				"endpointUrl": "https://alloya.ru/mcp",
				"authentication": "bearerAuth",
				"tool": { "__rl": true, "value": "make_call", "mode": "id" },
				"inputMode": "json",
				"jsonInput": "={{ (() => { if (Date.parse($json.approval_expires_at) <= Date.now()) throw new Error('Подтверждение истекло'); return $json.request; })() }}",
				"options": { "timeout": 10000 }
			}
		},
		{
			"id": "registered",
			"name": "Начать наблюдение",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [1540, 200],
			"parameters": {
				"jsCode": "const receipt = $input.first().json.structuredContent;\nconst request = $('Проверить запрос').first().json.request;\nif (!receipt || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(receipt.call_id) || receipt.client_reference !== request.client_reference) throw new Error('Регистрация звонка не подтверждена. Не создавайте новый запрос: сначала проверьте звонки в аллоя');\nreturn [{ json: { call_id: receipt.call_id, client_reference: request.client_reference, called_number: request.called_number } }];"
			}
		},
		{
			"id": "status",
			"name": "Статус звонка",
			"type": "@n8n/n8n-nodes-langchain.mcpClient",
			"typeVersion": 1.1,
			"position": [1760, 200],
			"retryOnFail": false,
			"parameters": {
				"serverTransport": "httpStreamable",
				"endpointUrl": "https://alloya.ru/mcp",
				"authentication": "bearerAuth",
				"tool": { "__rl": true, "value": "get_call", "mode": "id" },
				"inputMode": "json",
				"jsonInput": "={{ { call_id: $json.call_id, include_transcript: false, include_recording: false } }}",
				"options": { "timeout": 10000 }
			}
		},
		{
			"id": "observe",
			"name": "Результат наблюдения",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [1980, 200],
			"parameters": {
				"jsCode": "const call = $input.first().json.structuredContent?.result;\nconst registered = $('Начать наблюдение').first().json;\nconst statuses = ['scheduled', 'queued', 'processing', 'completed', 'cancelled', 'rejected', 'error'];\nconst outcomes = ['read', 'confirmed', 'accepted', 'declined', 'unknown', 'blocked', 'readback_required'];\nif (!call || call.id !== registered.call_id || call.client_reference !== registered.client_reference || call.called_number !== registered.called_number || !statuses.includes(call.status)) throw new Error('Не удалось подтвердить состояние выбранного звонка. Не повторяйте набор автоматически');\nif (!Array.isArray(call.actions) || !Array.isArray(call.unavailable_actions) || call.actions.some((action) => !outcomes.includes(action.outcome) || !Number.isInteger(action.sequence) || action.sequence < 0)) throw new Error('Некорректные сведения о действиях. Проверьте звонок в аллоя');\nconst attempts = $runIndex + 1;\nconst terminal = ['completed', 'cancelled', 'rejected', 'error'].includes(call.status);\nconst settled = terminal && (call.status !== 'completed' || call.outcome != null);\nconst continue_polling = !settled && attempts < 20;\nreturn [{ json: { call_id: call.id, client_reference: call.client_reference, transport_status: call.status, observation: continue_polling ? 'waiting' : settled ? 'observed_terminal' : 'observation_timeout', continue_polling, attempts, observed_at: new Date().toISOString(), outcome: call.outcome, end_call_reason: call.end_call_reason, summary: call.summary, actions: call.actions, unavailable_actions: call.unavailable_actions } }];"
			}
		},
		{
			"id": "pending",
			"name": "Продолжить наблюдение",
			"type": "n8n-nodes-base.if",
			"typeVersion": 2.2,
			"position": [2200, 200],
			"parameters": {
				"conditions": {
					"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
					"conditions": [
						{
							"id": "continue",
							"leftValue": "={{ $json.continue_polling }}",
							"rightValue": "",
							"operator": { "type": "boolean", "operation": "true", "singleValue": true }
						}
					],
					"combinator": "and"
				},
				"options": {}
			}
		},
		{
			"id": "wait",
			"name": "Подождать 15 секунд",
			"type": "n8n-nodes-base.wait",
			"typeVersion": 1.1,
			"position": [2200, 420],
			"parameters": { "resume": "timeInterval", "amount": 15, "unit": "seconds" },
			"webhookId": "21b52177-8f9a-4b10-a344-82562a6ef58b"
		},
		{
			"id": "guide",
			"name": "Перед запуском",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [0, -160],
			"parameters": {
				"width": 1080,
				"height": 340,
				"content": "## Один подтверждённый звонок\nПроверено с n8n 2.42.6. [Настройка](https://alloya.ru/mcp/n8n). Выберите отдельные Bearer Auth credentials во всех четырёх узлах MCP Client. Ключ не хранится в этом файле.\n\n1. Запустите с `mode: discover`: каталог доступен в «Проверить запрос», звонка не будет. Показаны первые 100 ассистентов и номеров.\n2. В «Параметры звонка» заполните ID ассистента и исходящего номера, номер получателя, цель и постоянный `client_reference`. Проверьте право на звонок и все параметры. Установите `mode: call` и `approval_expires_at` с часовым поясом не дальше чем на 30 минут. **Ручной запуск разрешает платный набор.** Изменили параметры — проверьте их заново.\n3. Один запрос на набор, затем до 20 проверок с интервалом 15 секунд. Тайм-аут или ошибка не отменяют звонок и не разрешают повторный набор. Сначала проверьте существующий звонок; при осознанном повторе запроса сохраните reference и параметры. Удаление звонка удаляет защиту этого reference.\n\n`observed_terminal` — наблюдаемый статус, не подтверждение записи или доставки. Результаты действий и неизвестные исходы проверяйте отдельно. Workflow не отменяет и не повторяет действия. Не используйте execution ID или время запуска как reference. Хранение результатов n8n тоже должно соответствовать требованиям к персональным данным."
			}
		}
	],
	"connections": {
		"Запустить вручную": { "main": [[{ "node": "Параметры звонка", "type": "main", "index": 0 }]] },
		"Параметры звонка": { "main": [[{ "node": "Ассистенты", "type": "main", "index": 0 }]] },
		"Ассистенты": { "main": [[{ "node": "Номера", "type": "main", "index": 0 }]] },
		"Номера": { "main": [[{ "node": "Проверить запрос", "type": "main", "index": 0 }]] },
		"Проверить запрос": { "main": [[{ "node": "Звонок подтверждён", "type": "main", "index": 0 }]] },
		"Звонок подтверждён": { "main": [[{ "node": "Позвонить", "type": "main", "index": 0 }], []] },
		"Позвонить": { "main": [[{ "node": "Начать наблюдение", "type": "main", "index": 0 }]] },
		"Начать наблюдение": { "main": [[{ "node": "Статус звонка", "type": "main", "index": 0 }]] },
		"Статус звонка": { "main": [[{ "node": "Результат наблюдения", "type": "main", "index": 0 }]] },
		"Результат наблюдения": { "main": [[{ "node": "Продолжить наблюдение", "type": "main", "index": 0 }]] },
		"Продолжить наблюдение": { "main": [[{ "node": "Подождать 15 секунд", "type": "main", "index": 0 }], []] },
		"Подождать 15 секунд": { "main": [[{ "node": "Статус звонка", "type": "main", "index": 0 }]] }
	},
	"settings": { "executionOrder": "v1", "executionTimeout": 600, "saveManualExecutions": true },
	"pinData": {},
	"tags": []
}
