diff --git a/docs/superpowers/plans/2026-08-11-confirmacao-agenda-klingo.md b/docs/superpowers/plans/2026-08-11-confirmacao-agenda-klingo.md new file mode 100644 index 0000000..5f5e684 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-confirmacao-agenda-klingo.md @@ -0,0 +1,1189 @@ +# Confirmação de Agenda via WhatsApp (Klingo Telefonia) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task (não subagent-driven-development — as tasks são chamadas de API do n8n via MCP, não commits de código; não há diff de git pra revisar por task). Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Construir o MCP Server "Klingo Telefonia" (6 tools) + o workflow proativo de lembrete de confirmação (cron diário) + um workflow de exemplo de AI Agent reativo que confirma presença via WhatsApp, todos estruturalmente validados (0 erros), sem cliente Klingo real disponível para teste de ponta a ponta. + +**Architecture:** Mesmo padrão de MCP Server nativo n8n já usado em Z-PRO/Klingo(agendamento)/SysLife/ClinicAll/Mensageria: `MCP Server Trigger` + N × `Call n8n Sub-Workflow Tool`, cada um apontando pra um sub-workflow `Execute Workflow Trigger` → `Config` → montagem de request → `HTTP Request` → classificação de resultado. O workflow de lembrete chama as tools via `Execute Workflow` direto (fluxo determinístico, sem IA). O workflow de exemplo reativo usa `AI Agent` + `Postgres Chat Memory` + tools do MCP Klingo Telefonia, reaproveitando o MCP de Mensageria (Z-PRO) já validado para o disparo em si. + +**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), MCP tools do n8n (`n8n_create_workflow`, `n8n_update_partial_workflow`, `n8n_validate_workflow`, `n8n_get_workflow`), Klingo Telefonia API (`https://api-externa.klingo.app/api/telefonia`, auth header `X-APP-TOKEN`), MCP Server - Mensageria ZPRO (`WsL4RlSD0hZ34gpB`, já em produção). + +## Global Constraints + +- Nenhum campo de request/response é inventado — usar só os campos confirmados no spec OpenAPI (ver Task 1). Onde o spec não documenta algo (schema de `/lista_ultimo_por_cpf` e `/bloqueios`, obrigatoriedade de `callid`), registrar como pendência explícita no código (comentário/descrição do node) e no runbook — nunca assumir silenciosamente. +- Nenhuma credencial é criada pela automação — a credencial `httpHeaderAuth` com o `X-APP-TOKEN` real do Klingo Telefonia é pré-requisito manual do usuário, só quando houver clínica real. Até lá, os nodes HTTP ficam com credencial vazia/placeholder e os workflows não são testados de ponta a ponta. +- **Lição da seção 17 do runbook — `$fromAI` só com um único `=` no início**: ao preencher `workflowInputs.value` dos nodes `Call n8n Sub-Workflow Tool` do servidor, cada expressão deve ser exatamente `"={{ $fromAI(...) }}"` (um `=`, não `"=={{ ... }}"`). Depois de qualquer `patchNodeField`/`updateNode` que escreva essas expressões, **reler o node com `n8n_get_workflow` (mode `filtered`) e conferir visualmente o texto salvo** antes de prosseguir — o erro não aparece em `n8n_validate_workflow`, só em runtime (campos chegando `null`). +- **Lição da seção 17 — campos opcionais precisam de valor-padrão no `$fromAI`**: todo campo não obrigatório deve usar a forma de 4 argumentos, `$fromAI('nome', 'descrição', 'tipo', valorPadrão)` (ex.: `$fromAI('obs', 'Observação opcional', 'string', '')`), senão o schema gerado da tool trata o campo como obrigatório e a chamada MCP falha com `Required → at `. +- Operações que trocam node + conexões (adicionar novo, religar, remover antigo) devem ser feitas numa **única chamada** `n8n_update_partial_workflow` — o n8n rejeita salvar com nós desconectados no meio do caminho. +- Todo workflow (servidor + sub-workflows) precisa estar `active: true` para responder via protocolo MCP (`tools/call`) — inclusive para o teste estrutural via `n8n_validate_workflow`, que não exige ativação, mas qualquer teste real futuro vai exigir. +- Taxonomia de erro padrão (mesma dos outros MCP Servers do projeto): `AUTHENTICATION_ERROR` (401/403), `NOT_FOUND` (404), `VALIDATION_ERROR` (400/422), `KLINGO_TELEFONIA_API_ERROR` (demais códigos de erro). +- `POST /entrada` fica **fora de escopo** — não implementar nenhuma tool para esse endpoint. + +Spec de referência: `docs/superpowers/specs/2026-08-11-confirmacao-agenda-klingo-design.md` + +--- + +## Campos confirmados no spec OpenAPI (Task 1 já executada nesta sessão) + +Consultados via `api.swaggerhub.com/apis/agsx30/klingo-api-telefonia/1.0.0` (técnica de acesso direto ao raw spec, já validada nesta sessão pra SwaggerHub). Resumo que todas as tasks abaixo devem usar como fonte da verdade: + +### `GET /lista/{data}` +- Path param: `data` (`YYYY-MM-DD`). +- Body (todos opcionais): `ate` (string), `atendidos` (boolean), `cancelados` (boolean), `endereco_unidade` (boolean), `links` (boolean), `instrucao_medico` (boolean), `autorizacao` (boolean). +- Resposta: array de objetos `marcacao` (ver schema abaixo). + +### `GET /{id}` +- Path param: `id` (id da marcação). +- Resposta: `{ "item": { ...marcacao } }`. + +### `GET /lista_ultimo_por_cpf` +- Body: `cpfs` (array de strings, obrigatório). +- Resposta: array de últimos atendimentos — **schema dos itens não documentado no spec** (pendência: tratar a resposta como objeto genérico, sem assumir campos específicos além dos já confirmados no schema `marcacao`). + +### `POST /confirmar` +- Body: `id_marcacao` (number, obrigatório — **nota de inconsistência**: o schema de `marcacao` lista `id_marcacao` como string; ao montar o corpo desta chamada, converter para number com `Number(...)`), `status` (string, obrigatório — `"C"` Confirmado, `"S"` Sem Resposta, `"N"` Não Confirmada/cancela), `obs` (string, opcional), `callid` (string, **opcional** — nenhum campo do schema JSON está marcado `required`, apesar do texto descritivo do primeiro endpoint sugerir obrigatoriedade; pendência documentada: tratar como opcional e deixar vazio quando a confirmação vier do WhatsApp, sem chamada telefônica real). + +### `POST /nps` +- Body: `id_marcacao` (number, obrigatório), `nota` (number, obrigatório, 0–10), `obs` (string, opcional). + +### `GET /bloqueios` +- Body: `id_medico` (number, opcional), `de` (string, opcional, `YYYY-MM-DD`), `ate` (string, opcional, `YYYY-MM-DD`), `ref` (string, opcional, `"I"` ou `"R"`). +- Resposta: lista de bloqueios — **schema não documentado no spec** (pendência: tratar como objeto genérico). + +### Schema `marcacao` (usado em `/lista/{data}` e `/{id}`) +`id_paciente`, `id_marcacao`, `nome`, `telefone`, `celular`, `cpf`, `data` (`YYYY-MM-DD HH:MM`), `hora` (`HH:MM`), `dt_atendimento`, `dt_fim_atendimento`, `procedimento`, `medico`, `unidade`, `unidade_id` (number), `status`, `status_confirmacao`, `status_confirmacao_id`, `data_confirmacao`, `responsavel_confirmacao`, `origem_confirmacao`, `obs_confirmacao`, `codigo_reserva`, `id_atendimento`, `instrucao` — todos string exceto `unidade_id`. + +**Nota sobre GET com body**: os endpoints GET desta API aceitam filtros no corpo da requisição (incomum, mas é o que o spec documenta). No node `HTTP Request` do n8n, isso significa usar `method: "GET"` com `sendBody: true` — confirmar que o n8n permite essa combinação (o node HTTP Request do n8n suporta body em GET desde que `sendBody` seja habilitado manualmente; não é o padrão da UI, então cada sub-workflow GET com filtros precisa habilitar isso explicitamente). + +--- + +## Pré-requisito (ação manual do usuário, quando houver clínica Klingo real) + +Criar no n8n uma Credential do tipo **Header Auth** (`httpHeaderAuth`), nome do header `X-APP-TOKEN`, valor = token real da integração Klingo Telefonia daquela clínica. Nomear como `Klingo Telefonia - `. **Não é pré-requisito para este plano** — os workflows são construídos e validados estruturalmente sem essa credencial; ela só é necessária para teste real de ponta a ponta, fora de escopo aqui. + +--- + +### Task 1: Sub-workflow `listar_agendamentos_do_dia` + +**Interfaces:** +- Consumes: nenhuma (primeira peça construída). +- Produces: workflow `MCP Server - Klingo Telefonia - listar_agendamentos_do_dia`, consumido pela Task 6 (servidor) e pela Task 8 (lembrete proativo). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - listar_agendamentos_do_dia", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { + "values": [ + { "name": "data", "type": "string" }, + { "name": "ate", "type": "string" }, + { "name": "atendidos", "type": "boolean" }, + { "name": "cancelados", "type": "boolean" }, + { "name": "endereco_unidade", "type": "boolean" }, + { "name": "links", "type": "boolean" }, + { "name": "instrucao_medico", "type": "boolean" }, + { "name": "autorizacao", "type": "boolean" } + ] + } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [448, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "requestBody", "type": "object", "value": "={{ { ate: $json.ate, atendidos: $json.atendidos, cancelados: $json.cancelados, endereco_unidade: $json.endereco_unidade, links: $json.links, instrucao_medico: $json.instrucao_medico, autorizacao: $json.autorizacao } }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "GET lista do dia", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [672, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/lista/{{ $json.data }}", + "sendBody": true, + "bodyParameters": { "parameters": [] }, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [896, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou a listagem de agendamentos do dia (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "GET lista do dia", "type": "main", "index": 0 }]] }, + "GET lista do dia": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado — necessário na Task 6. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. Se a combinação `method: "GET"` + `sendBody: true` gerar warning, aceitar (é o comportamento documentado da API Klingo, não um erro de configuração). + +--- + +### Task 2: Sub-workflow `consultar_agendamento_por_id` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - Klingo Telefonia - consultar_agendamento_por_id`, consumido pela Task 6. + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - consultar_agendamento_por_id", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { "values": [{ "name": "id", "type": "string" }] } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "GET marcação por id", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [448, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/{{ $json.id }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [672, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou a consulta da marcação por id (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "GET marcação por id", "type": "main", "index": 0 }]] }, + "GET marcação por id": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 3: Sub-workflow `buscar_agendamento_pendente_por_cpf` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - Klingo Telefonia - buscar_agendamento_pendente_por_cpf`, consumido pela Task 6 e pelo workflow de exemplo (Task 10). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - buscar_agendamento_pendente_por_cpf", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { "values": [{ "name": "cpf", "type": "string" }] } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [448, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "requestBody", "type": "object", "value": "={{ { cpfs: [$json.cpf] } }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "GET lista_ultimo_por_cpf", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [672, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/lista_ultimo_por_cpf", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [896, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou a busca por CPF (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "GET lista_ultimo_por_cpf", "type": "main", "index": 0 }]] }, + "GET lista_ultimo_por_cpf": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 4: Sub-workflow `confirmar_presenca` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - Klingo Telefonia - confirmar_presenca`, consumido pela Task 6 e pelo workflow de exemplo (Task 10). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - confirmar_presenca", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { + "values": [ + { "name": "id_marcacao", "type": "string" }, + { "name": "status", "type": "string" }, + { "name": "obs", "type": "string" }, + { "name": "callid", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [448, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "requestBody", "type": "object", "value": "={{ { id_marcacao: Number($json.id_marcacao), status: $json.status, obs: $json.obs || undefined, callid: $json.callid || undefined } }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "POST confirmar", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [448, 0], + "parameters": { + "method": "POST", + "url": "={{ $json.baseUrl }}/confirmar", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [672, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou a confirmação de presença (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "POST confirmar", "type": "main", "index": 0 }]] }, + "POST confirmar": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +`status` deve ser documentado na `description` da tool (Task 6) como aceitando só `"C"` (Confirmado), `"S"` (Sem Resposta) ou `"N"` (Não Confirmada — cancela a marcação), conforme o spec. + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 5: Sub-workflows `registrar_nps` e `consultar_bloqueios` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflows `MCP Server - Klingo Telefonia - registrar_nps` e `MCP Server - Klingo Telefonia - consultar_bloqueios`, consumidos pela Task 6. + +- [ ] **Step 1: Criar `registrar_nps`** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - registrar_nps", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { + "values": [ + { "name": "id_marcacao", "type": "string" }, + { "name": "nota", "type": "number" }, + { "name": "obs", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [448, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "requestBody", "type": "object", "value": "={{ { id_marcacao: Number($json.id_marcacao), nota: Number($json.nota), obs: $json.obs || undefined } }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "POST nps", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [672, 0], + "parameters": { + "method": "POST", + "url": "={{ $json.baseUrl }}/nps", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [896, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou o registro de NPS (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "POST nps", "type": "main", "index": 0 }]] }, + "POST nps": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar `registrar_nps`** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +- [ ] **Step 3: Criar `consultar_bloqueios`** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - Klingo Telefonia - consultar_bloqueios", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { + "values": [ + { "name": "id_medico", "type": "number" }, + { "name": "de", "type": "string" }, + { "name": "ate", "type": "string" }, + { "name": "ref", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config Klingo Telefonia", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "baseUrl", "type": "string", "value": "https://api-externa.klingo.app/api/telefonia" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [448, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "requestBody", "type": "object", "value": "={{ { id_medico: $json.id_medico || undefined, de: $json.de || undefined, ate: $json.ate || undefined, ref: $json.ref || undefined } }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "http", + "name": "GET bloqueios", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [672, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/bloqueios", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "sendHeaders": false, + "options": { "response": { "response": { "neverError": true, "fullResponse": true } } } + }, + "credentials": { + "httpHeaderAuth": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Klingo Telefonia - (sem clínica real)" } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [896, 0], + "parameters": { + "jsCode": "const statusCode = $input.item.json.statusCode;\nconst body = $input.item.json.body;\nif (statusCode >= 200 && statusCode < 300) {\n return [{ json: { success: true, data: body } }];\n}\nlet code = 'KLINGO_TELEFONIA_API_ERROR';\nif (statusCode === 401 || statusCode === 403) code = 'AUTHENTICATION_ERROR';\nelse if (statusCode === 404) code = 'NOT_FOUND';\nelse if (statusCode === 400 || statusCode === 422) code = 'VALIDATION_ERROR';\nreturn [{ json: { success: false, error: { code, message: `A API de Telefonia do Klingo rejeitou a consulta de bloqueios (status ${statusCode}).`, details: body } } }];" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config Klingo Telefonia", "type": "main", "index": 0 }]] }, + "Config Klingo Telefonia": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "GET bloqueios", "type": "main", "index": 0 }]] }, + "GET bloqueios": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 4: Validar `consultar_bloqueios`** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 6: Servidor MCP "Klingo Telefonia" com as 6 tools + +**Interfaces:** +- Consumes: os 6 workflow ids das Tasks 1–5. +- Produces: workflow `MCP Server - Klingo Telefonia` (servidor), path `mcp-server-klingo-telefonia`, consumido pela Task 10 (workflow de exemplo). + +- [ ] **Step 1: Criar o servidor com o `MCP Server Trigger` e os 6 nodes de tool** + +`n8n_create_workflow` — substituir `ID_LISTAR`, `ID_CONSULTAR_ID`, `ID_BUSCAR_CPF`, `ID_CONFIRMAR`, `ID_NPS`, `ID_BLOQUEIOS` pelos ids reais anotados nas Tasks 1–5: + +```json +{ + "name": "MCP Server - Klingo Telefonia", + "nodes": [ + { + "id": "mcp-trigger", + "name": "MCP Server Trigger", + "type": "@n8n/n8n-nodes-langchain.mcpTrigger", + "typeVersion": 1, + "position": [0, 256], + "parameters": { "path": "mcp-server-klingo-telefonia", "authentication": "none" } + }, + { + "id": "tool-listar", + "name": "listar_agendamentos_do_dia", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [-320, 448], + "parameters": { + "description": "Lista os agendamentos (marcações) de uma data específica no Klingo. Usado como base do lembrete proativo de confirmação — busque a data de amanhã para preparar os lembretes do dia seguinte.", + "workflowId": { "value": "ID_LISTAR", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "data": "={{ $fromAI('data', 'Data dos agendamentos a listar, no formato YYYY-MM-DD', 'string') }}", + "ate": "={{ $fromAI('ate', 'Data final do intervalo de busca, formato YYYY-MM-DD (opcional)', 'string', '') }}", + "atendidos": "={{ $fromAI('atendidos', 'Se true, inclui agendamentos já atendidos (opcional)', 'boolean', false) }}", + "cancelados": "={{ $fromAI('cancelados', 'Se true, inclui agendamentos cancelados (opcional)', 'boolean', false) }}", + "endereco_unidade": "={{ $fromAI('endereco_unidade', 'Se true, inclui o endereço da unidade na resposta (opcional)', 'boolean', false) }}", + "links": "={{ $fromAI('links', 'Se true, inclui links relacionados na resposta (opcional)', 'boolean', false) }}", + "instrucao_medico": "={{ $fromAI('instrucao_medico', 'Se true, inclui instruções médicas na resposta (opcional)', 'boolean', false) }}", + "autorizacao": "={{ $fromAI('autorizacao', 'Se true, inclui dados de autorização na resposta (opcional)', 'boolean', false) }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "data", "displayName": "data", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "ate", "displayName": "ate", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "atendidos", "displayName": "atendidos", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }, + { "id": "cancelados", "displayName": "cancelados", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }, + { "id": "endereco_unidade", "displayName": "endereco_unidade", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }, + { "id": "links", "displayName": "links", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }, + { "id": "instrucao_medico", "displayName": "instrucao_medico", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }, + { "id": "autorizacao", "displayName": "autorizacao", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-consultar-id", + "name": "consultar_agendamento_por_id", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [-160, 448], + "parameters": { + "description": "Consulta o detalhe de um agendamento (marcação) específico do Klingo pelo id.", + "workflowId": { "value": "ID_CONSULTAR_ID", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { "id": "={{ $fromAI('id', 'Id do agendamento/marcação a consultar', 'string') }}" }, + "matchingColumns": [], + "schema": [{ "id": "id", "displayName": "id", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-buscar-cpf", + "name": "buscar_agendamento_pendente_por_cpf", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [0, 448], + "parameters": { + "description": "Busca o último atendimento/agendamento de um paciente pelo CPF. Use isso para descobrir qual agendamento o paciente está confirmando quando ele responde 'sim'/'não' sem informar o id diretamente.", + "workflowId": { "value": "ID_BUSCAR_CPF", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { "cpf": "={{ $fromAI('cpf', 'CPF do paciente, somente números', 'string') }}" }, + "matchingColumns": [], + "schema": [{ "id": "cpf", "displayName": "cpf", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-confirmar", + "name": "confirmar_presenca", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [160, 448], + "parameters": { + "description": "Registra a confirmação de presença de um agendamento no Klingo. status deve ser exatamente 'C' (Confirmado), 'S' (Sem Resposta) ou 'N' (Não Confirmada — cancela o agendamento). Use depois de identificar o id_marcacao correto (ex.: via buscar_agendamento_pendente_por_cpf).", + "workflowId": { "value": "ID_CONFIRMAR", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "id_marcacao": "={{ $fromAI('id_marcacao', 'Id do agendamento/marcação a confirmar', 'string') }}", + "status": "={{ $fromAI('status', \"Status da confirmação: 'C' para Confirmado, 'S' para Sem Resposta, 'N' para Não Confirmada (cancela o agendamento)\", 'string') }}", + "obs": "={{ $fromAI('obs', 'Observação sobre a confirmação (opcional)', 'string', '') }}", + "callid": "={{ $fromAI('callid', 'Id de chamada telefônica associada, se houver (opcional — deixe vazio para confirmações via WhatsApp)', 'string', '') }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "id_marcacao", "displayName": "id_marcacao", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "status", "displayName": "status", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "obs", "displayName": "obs", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "callid", "displayName": "callid", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-nps", + "name": "registrar_nps", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [320, 448], + "parameters": { + "description": "Registra a nota de NPS (0 a 10) de um paciente sobre um agendamento já realizado.", + "workflowId": { "value": "ID_NPS", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "id_marcacao": "={{ $fromAI('id_marcacao', 'Id do agendamento/marcação avaliado', 'string') }}", + "nota": "={{ $fromAI('nota', 'Nota de NPS de 0 a 10 dada pelo paciente', 'number') }}", + "obs": "={{ $fromAI('obs', 'Comentário do paciente sobre a nota (opcional)', 'string', '') }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "id_marcacao", "displayName": "id_marcacao", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "nota", "displayName": "nota", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "number" }, + { "id": "obs", "displayName": "obs", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-bloqueios", + "name": "consultar_bloqueios", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [480, 448], + "parameters": { + "description": "Consulta bloqueios de agenda no Klingo, opcionalmente filtrando por médico e/ou intervalo de datas.", + "workflowId": { "value": "ID_BLOQUEIOS", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "id_medico": "={{ $fromAI('id_medico', 'Id do médico para filtrar os bloqueios (opcional)', 'number', 0) }}", + "de": "={{ $fromAI('de', 'Data inicial do intervalo, formato YYYY-MM-DD (opcional)', 'string', '') }}", + "ate": "={{ $fromAI('ate', 'Data final do intervalo, formato YYYY-MM-DD (opcional)', 'string', '') }}", + "ref": "={{ $fromAI('ref', \"Tipo de referência do bloqueio: 'I' ou 'R' (opcional)\", 'string', '') }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "id_medico", "displayName": "id_medico", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "number" }, + { "id": "de", "displayName": "de", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "ate", "displayName": "ate", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "ref", "displayName": "ref", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + } + ], + "connections": { + "listar_agendamentos_do_dia": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "consultar_agendamento_por_id": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "buscar_agendamento_pendente_por_cpf": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "confirmar_presenca": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "registrar_nps": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "consultar_bloqueios": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] } + } +} +``` + +Anotar o `id` do servidor retornado — necessário na Task 10. + +- [ ] **Step 2: Conferir visualmente os textos salvos dos `$fromAI`** + +`n8n_get_workflow` com `{"id": "", "mode": "filtered", "nodeNames": ["confirmar_presenca", "registrar_nps", "consultar_bloqueios", "listar_agendamentos_do_dia", "consultar_agendamento_por_id", "buscar_agendamento_pendente_por_cpf"]}`. Conferir que cada valor de `workflowInputs.value` começa com um único `=` (não `==`) — a lição da seção 17 do runbook (ver Global Constraints). + +- [ ] **Step 3: Validar** + +`n8n_validate_workflow` com o id do servidor. Esperado: `errorCount: 0`. + +--- + +### Task 7: Ativar os 7 workflows do MCP Server + +**Interfaces:** +- Consumes: os 7 workflow ids (6 sub-workflows das Tasks 1–5 + servidor da Task 6). +- Produces: MCP Server "Klingo Telefonia" respondendo via protocolo MCP (estrutura pronta; sem teste de chamada real, por falta de credencial). + +- [ ] **Step 1: Ativar cada um dos 7 workflows** + +Para cada um dos 7 ids (6 sub-workflows + servidor), rodar `n8n_update_partial_workflow` com `{"id": "", "operations": [{"type": "activateWorkflow"}]}`. + +- [ ] **Step 2: Confirmar ativação** + +`n8n_get_workflow` com `mode: "minimal"` em cada um dos 7 ids — confirmar `"active": true` em todos. + +--- + +### Task 8: Workflow proativo "Lembrete de Confirmação — Klingo (exemplo)" + +**Interfaces:** +- Consumes: workflow `listar_agendamentos_do_dia` (Task 1) via `Execute Workflow`; tool `enviar_texto` do MCP de Mensageria ZPRO (`Y4ndG7GuIUrmVRxV`, já em produção) via `Execute Workflow`. +- Produces: workflow `Lembrete de Confirmação - Klingo (exemplo)`. + +- [ ] **Step 1: Criar o workflow** + +`n8n_create_workflow` — substituir `ID_LISTAR_AGENDAMENTOS` pelo id anotado na Task 1: + +```json +{ + "name": "Lembrete de Confirmação - Klingo (exemplo)", + "nodes": [ + { + "id": "cron", + "name": "Cron 8h diário", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [0, 0], + "parameters": { + "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * *" }] } + } + }, + { + "id": "calcular-data", + "name": "Calcular data de amanhã", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "dataAmanha", "type": "string", "value": "={{ $now.plus({ days: 1 }).toFormat('yyyy-LL-dd') }}" } + ] + } + } + }, + { + "id": "listar", + "name": "Listar agendamentos de amanhã", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [448, 0], + "parameters": { + "workflowId": { "value": "ID_LISTAR_AGENDAMENTOS", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { "data": "={{ $json.dataAmanha }}" } + } + } + }, + { + "id": "extrair-lista", + "name": "Extrair lista de agendamentos", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [672, 0], + "parameters": { + "jsCode": "const result = $input.item.json;\nif (!result.success) {\n throw new Error('Falha ao listar agendamentos do dia: ' + JSON.stringify(result.error));\n}\nconst marcacoes = Array.isArray(result.data) ? result.data : [];\nreturn marcacoes.map(m => ({ json: m }));" + } + }, + { + "id": "montar-mensagem", + "name": "Montar mensagem de lembrete", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [896, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "numeroWhatsapp", "type": "string", "value": "={{ $json.celular || $json.telefone }}" }, + { "name": "textoLembrete", "type": "string", "value": "=Olá, {{ $json.nome }}! Passando para confirmar sua consulta de {{ $json.procedimento }} com {{ $json.medico }} amanhã ({{ $json.data }} às {{ $json.hora }}) na unidade {{ $json.unidade }}. Você confirma presença? Responda SIM ou NÃO." }, + { "name": "externalKey", "type": "string", "value": "=lembrete-klingo-{{ $json.id_marcacao }}" } + ] + }, + "includeOtherFields": true + } + }, + { + "id": "enviar", + "name": "Enviar lembrete via Mensageria ZPRO", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [1120, 0], + "parameters": { + "workflowId": { "value": "Y4ndG7GuIUrmVRxV", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "number": "={{ $json.numeroWhatsapp }}", + "body": "={{ $json.textoLembrete }}", + "externalKey": "={{ $json.externalKey }}" + } + } + } + } + ], + "connections": { + "Cron 8h diário": { "main": [[{ "node": "Calcular data de amanhã", "type": "main", "index": 0 }]] }, + "Calcular data de amanhã": { "main": [[{ "node": "Listar agendamentos de amanhã", "type": "main", "index": 0 }]] }, + "Listar agendamentos de amanhã": { "main": [[{ "node": "Extrair lista de agendamentos", "type": "main", "index": 0 }]] }, + "Extrair lista de agendamentos": { "main": [[{ "node": "Montar mensagem de lembrete", "type": "main", "index": 0 }]] }, + "Montar mensagem de lembrete": { "main": [[{ "node": "Enviar lembrete via Mensageria ZPRO", "type": "main", "index": 0 }]] } + } +} +``` + +Nota: o node `Listar agendamentos de amanhã` chama diretamente o sub-workflow `listar_agendamentos_do_dia` (mesmo sub-workflow usado pela tool do MCP na Task 1) via `Execute Workflow` comum, não via MCP — fluxo determinístico, sem IA envolvida. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +- [ ] **Step 3: Deixar inativo** + +Este workflow **não deve ser ativado** neste plano — rodar um cron diário de verdade sem clínica real dispararia chamadas reais desnecessárias contra a API do Klingo (que falhariam por falta de credencial) e, pior, tentaria enviar mensagens reais via Mensageria ZPRO com dados vazios. Ativação fica para quando houver clínica Klingo real configurada com credencial válida. + +--- + +### Task 9: Sub-workflow de exemplo "AI Agent — Confirmação Klingo (exemplo)" + +**Interfaces:** +- Consumes: nenhuma (é o workflow raiz do exemplo). +- Produces: workflow `Confirmação de Agenda - GChat - Klingo+Z-PRO (exemplo)`, consumido pela Task 10. + +- [ ] **Step 1: Ler a estrutura do webhook e AI Agent já em produção, como referência** + +`n8n_get_workflow` com `{"id": "yG2mi6ECBpPv4Hmn", "mode": "structure"}` — usar como referência de contrato de webhook (seção 6 do runbook) e estrutura de nodes (`Webhook`, `AI Agent`, modelo de chat, memória) a replicar no novo workflow de exemplo. Não modificar o workflow de produção. + +- [ ] **Step 2: Criar o workflow de exemplo** + +`n8n_create_workflow` — reproduzir o mesmo node de `Webhook` com o mesmo contrato de payload documentado na seção 6 do runbook (campos `sessionId`, `mensagem`, `nomeContato`, `telefone`), um `AI Agent` com `systemMessage` fixo, um modelo de chat (mesmo tipo já usado na clínica exemplo) e uma `Postgres Chat Memory` **sem credencial ainda** (placeholder, já que não há cliente real): + +```json +{ + "name": "Confirmação de Agenda - GChat - Klingo+Z-PRO (exemplo)", + "nodes": [ + { + "id": "webhook", + "name": "Webhook GChat", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [0, 0], + "parameters": { "path": "confirmacao-agenda-klingo-exemplo", "httpMethod": "POST", "responseMode": "lastNode" } + }, + { + "id": "extrair-campos", + "name": "Extrair campos do webhook", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [224, 0], + "parameters": { + "assignments": { + "assignments": [ + { "name": "sessionId", "type": "string", "value": "={{ $json.body.sessionId }}" }, + { "name": "mensagem", "type": "string", "value": "={{ $json.body.mensagem }}" }, + { "name": "nomeContato", "type": "string", "value": "={{ $json.body.nomeContato }}" }, + { "name": "telefone", "type": "string", "value": "={{ $json.body.telefone }}" } + ] + } + } + }, + { + "id": "ai-agent", + "name": "AI Agent", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 2.1, + "position": [448, 0], + "parameters": { + "text": "={{ $json.mensagem }}", + "systemMessage": "Você é um assistente de confirmação de presença em consultas médicas, via WhatsApp. Seu único trabalho é: 1) entender se a mensagem do paciente é uma resposta de confirmação (ex.: 'sim', 'confirmo', 'vou sim') ou recusa (ex.: 'não', 'não posso ir', 'quero cancelar') a uma consulta agendada; 2) se for, use a tool buscar_agendamento_pendente_por_cpf com o CPF do paciente (pergunte o CPF se ainda não souber) para achar o id_marcacao da consulta pendente; 3) use a tool confirmar_presenca com esse id_marcacao e status 'C' (se confirmou) ou 'N' (se recusou); 4) responda ao paciente confirmando que o registro foi feito. Se a mensagem não for uma resposta de confirmação/recusa clara, peça para o paciente esclarecer. Nunca invente um id_marcacao — sempre busque antes de confirmar." + } + }, + { + "id": "memoria", + "name": "Memória da Sessão (Postgres)", + "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat", + "typeVersion": 1.4, + "position": [448, -160], + "parameters": { + "sessionIdType": "customKey", + "sessionKey": "={{ $json.sessionId }}", + "tableName": "n8n_chat_histories", + "contextWindowLength": 20 + }, + "credentials": { + "postgres": { "id": "PLACEHOLDER_SEM_CREDENCIAL", "name": "Postgres - (sem cliente real)" } + } + } + ], + "connections": { + "Webhook GChat": { "main": [[{ "node": "Extrair campos do webhook", "type": "main", "index": 0 }]] }, + "Extrair campos do webhook": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] }, + "Memória da Sessão (Postgres)": { "ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 3: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: possível warning sobre credencial `postgres` placeholder inválida (aceitável — sem cliente real, credencial real não existe ainda) e sobre a falta de um modelo de chat (`languageModel`) conectado ao `AI Agent` (aceitável pelo mesmo motivo: sem esse node o `n8n_validate_workflow` provavelmente aponta "AI Agent requires a language model" — se isso vier como **erro**, não apenas warning, adicionar um node `OpenAI Chat Model` (mesmo tipo já usado na clínica exemplo, ver Step 1) com credencial placeholder antes de prosseguir, para o workflow ficar estruturalmente completo mesmo sem poder rodar de verdade). + +- [ ] **Step 4: Adicionar o modelo de chat se necessário** + +Se a validação do Step 3 apontou erro (não apenas warning) pela ausência de `languageModel`, rodar `n8n_update_partial_workflow` numa única chamada atômica adicionando o node de modelo (mesmo `type`/`typeVersion` usado no node de modelo do workflow de produção lido no Step 1) e a conexão `ai_languageModel` para `AI Agent`, com credencial placeholder. Revalidar em seguida (`n8n_validate_workflow`) — esperado `errorCount: 0` (warnings de credencial placeholder são aceitáveis). + +--- + +### Task 10: Conectar as tools do MCP Klingo Telefonia ao AI Agent de exemplo + +**Interfaces:** +- Consumes: workflow servidor `MCP Server - Klingo Telefonia` (Task 6, id do servidor) e workflow `Confirmação de Agenda - GChat - Klingo+Z-PRO (exemplo)` (Task 9). +- Produces: `AI Agent` do workflow de exemplo com acesso às tools `buscar_agendamento_pendente_por_cpf` e `confirmar_presenca` via `MCP Client Tool`. + +- [ ] **Step 1: Adicionar o node `MCP Client Tool` conectado ao AI Agent** + +`n8n_update_partial_workflow` no workflow da Task 9, numa única chamada atômica (adicionar node + conectar): + +```json +{ + "id": "", + "operations": [ + { + "type": "addNode", + "node": { + "id": "mcp-client-klingo-telefonia", + "name": "MCP Klingo Telefonia", + "type": "@n8n/n8n-nodes-langchain.mcpClientTool", + "typeVersion": 1, + "position": [448, 160], + "parameters": { + "endpointUrl": "https://devn8n.gtigrupo.com.br/mcp/mcp-server-klingo-telefonia", + "authentication": "none", + "include": "selected", + "includeTools": ["buscar_agendamento_pendente_por_cpf", "confirmar_presenca"] + } + } + }, + { "type": "addConnection", "source": "MCP Klingo Telefonia", "target": "AI Agent", "sourceOutput": "ai_tool", "targetIndex": 0 } + ] +} +``` + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` no workflow da Task 9. Esperado: `errorCount: 0` (fora dos warnings de credencial placeholder já aceitos na Task 9). + +- [ ] **Step 3: Deixar inativo** + +Mesma razão da Task 8 — sem credenciais reais (Postgres e Klingo Telefonia), ativar este workflow não teria efeito útil e só acumularia execuções com erro. Fica pronto pra ativar quando houver cliente real. + +--- + +### Task 11: Atualizar o runbook e commit + +**Interfaces:** +- Consumes: todos os ids de workflow criados nas Tasks 1–10. + +- [ ] **Step 1: Adicionar seção 18 ao runbook** + +Adicionar em `docs/n8n-agendamento-runbook.md`, após a seção 17, uma seção "18. Confirmação de Agenda via WhatsApp (Klingo Telefonia)" cobrindo: as 6 tools do MCP Server e seus endpoints, os ids de todos os 9 workflows criados (6 sub-workflows + servidor + lembrete proativo + AI Agent de exemplo), as pendências documentadas (schema de `/lista_ultimo_por_cpf` e `/bloqueios` não documentados no spec, ambiguidade de obrigatoriedade do `callid` em `/confirmar`, inconsistência de tipo do `id_marcacao` entre o schema `marcacao` (string) e o body de `/confirmar`/`/nps` (number)), e o status "estrutura pronta, sem teste de ponta a ponta — falta credencial Klingo Telefonia real". Seguir o mesmo estilo de prosa das seções 14 e 17 (motivação, arquitetura, bugs/lições reais encontradas, resultado). + +- [ ] **Step 2: Commit** + +```bash +cd "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO" +git add docs/n8n-agendamento-runbook.md +git commit -m "docs: registra MCP Klingo Telefonia e confirmação de agenda via WhatsApp + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +## Observação sobre execução + +Mesma disciplina dos planos anteriores deste projeto: validar +(`n8n_validate_workflow`) depois de cada workflow criado/modificado, e +conferir visualmente o texto salvo de qualquer expressão `$fromAI` +antes de considerar uma tool pronta (Task 6, Step 2) — esse bug real já +aconteceu uma vez nesta sessão (seção 17 do runbook) e não é pego pela +validação estrutural do n8n. Os workflows de lembrete proativo (Task 8) +e de AI Agent de exemplo (Tasks 9–10) ficam **intencionalmente +inativos** ao final deste plano — sem clínica Klingo real e sem +credencial `X-APP-TOKEN`/Postgres válida, ativá-los só geraria +execuções com erro.