56 KiB
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_cpfe/bloqueios, obrigatoriedade decallid), 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
httpHeaderAuthcom oX-APP-TOKENreal 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 —
$fromAIsó com um único=no início: ao preencherworkflowInputs.valuedos nodesCall n8n Sub-Workflow Tooldo servidor, cada expressão deve ser exatamente"={{ $fromAI(...) }}"(um=, não"=={{ ... }}"). Depois de qualquerpatchNodeField/updateNodeque escreva essas expressões, reler o node comn8n_get_workflow(modefiltered) e conferir visualmente o texto salvo antes de prosseguir — o erro não aparece emn8n_validate_workflow, só em runtime (campos chegandonull). - 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 comRequired → at <campo>. - 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: truepara responder via protocolo MCP (tools/call) — inclusive para o teste estrutural vian8n_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 /entradafica 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 demarcacaolistaid_marcacaocomo string; ao montar o corpo desta chamada, converter para number comNumber(...)),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á marcadorequired, 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 - <nome da clínica>. 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:
{
"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:
{
"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:
{
"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:
{
"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_npseMCP Server - Klingo Telefonia - consultar_bloqueios, consumidos pela Task 6. -
Step 1: Criar
registrar_nps
n8n_create_workflow:
{
"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:
{
"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), pathmcp-server-klingo-telefonia, consumido pela Task 10 (workflow de exemplo). -
Step 1: Criar o servidor com o
MCP Server Triggere 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:
{
"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": "<id do servidor>", "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": "<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) viaExecute Workflow; toolenviar_textodo MCP de Mensageria ZPRO (Y4ndG7GuIUrmVRxV, já em produção) viaExecute 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:
{
"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):
{
"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 workflowConfirmação de Agenda - GChat - Klingo+Z-PRO (exemplo)(Task 9). -
Produces:
AI Agentdo workflow de exemplo com acesso às toolsbuscar_agendamento_pendente_por_cpfeconfirmar_presencaviaMCP Client Tool. -
Step 1: Adicionar o node
MCP Client Toolconectado ao AI Agent
n8n_update_partial_workflow no workflow da Task 9, numa única chamada atômica (adicionar node + conectar):
{
"id": "<id do workflow da Task 9>",
"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
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 <noreply@anthropic.com>"
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.