From 97dff5cebb9f4ca4cc6391917af300f9e34c5b21 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Tue, 11 Aug 2026 16:07:05 -0300 Subject: [PATCH] =?UTF-8?q?Plano:=20confirma=C3=A7=C3=A3o=20de=20agenda=20?= =?UTF-8?q?via=20WhatsApp=20(ClinicAll)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- ...2026-08-11-confirmacao-agenda-clinicall.md | 1052 +++++++++++++++++ 1 file changed, 1052 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md diff --git a/docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md b/docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md new file mode 100644 index 0000000..7559461 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md @@ -0,0 +1,1052 @@ +# Confirmação de Agenda via WhatsApp (ClinicAll) 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 "ClinicAll - Confirmação de Agenda" (5 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 clínica ClinicAll real disponível para teste de ponta a ponta — mesmo padrão já executado para o Klingo (`docs/superpowers/plans/2026-08-11-confirmacao-agenda-klingo.md`). + +**Architecture:** Mesmo padrão de MCP Server nativo n8n já usado nos outros providers: `MCP Server Trigger` + N × `Call n8n Sub-Workflow Tool`, cada um apontando pra um sub-workflow `Execute Workflow Trigger` → `Config ClinicAll` → `Montar query`/`Montar corpo` (Code) → `HTTP Request` → `Classificar resultado` (Code). Reaproveita **exatamente** o estilo de node já usado no MVP de agendamento do ClinicAll (`MCP Server - ClinicAll - unidade` `rcxO2wM00wBn8yZr`, `MCP Server - ClinicAll - agendar` `WBJrNhXhX7W7hGOY`, lidos nesta sessão como referência): nodes `Code` com `mode: "runOnceForEachItem"`, `HTTP Request` com `authentication: "none"` (sem credencial — mesma pendência já documentada), `neverError: true` + `fullResponse: true`, função `classifyError` inline. O workflow de lembrete chama as tools via `Execute Workflow` direto (fluxo determinístico, sem IA), reaproveitando o MCP de Mensageria (Z-PRO) já validado. O workflow de exemplo reativo usa `AI Agent` + `Postgres Chat Memory` + tools do MCP ClinicAll via `MCP Client Tool` nativo. + +**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), MCP tools do n8n (`n8n_create_workflow`, `n8n_update_partial_workflow`, `n8n_validate_workflow`, `n8n_get_workflow`), ClinicAll API (`/datasnap/rest/TCalendarController`, `/datasnap/rest/TDomainController`, `authentication: none`), MCP Server - Mensageria ZPRO (`WsL4RlSD0hZ34gpB` → sub-workflow `enviar_texto` `Y4ndG7GuIUrmVRxV`, já em produção). + +## Global Constraints + +- Nenhum campo de request/response é inventado — usar só os campos confirmados no manual (`docs/API Clinicall.pdf`, v1.1.16). Onde o manual não documenta algo (formato exato de `agendamentoSemPaciente` — GET com query string vs. body —, schema de erro), registrar como pendência explícita no código (comentário/descrição do node) e no runbook. +- **Nenhuma credencial de autenticação é usada** — mesma decisão já tomada no MVP de agendamento do ClinicAll: todos os nodes HTTP ficam com `authentication: "none"` (não `httpHeaderAuth` como no Klingo, porque o manual do ClinicAll não documenta nenhum mecanismo de autenticação por requisição). `baseUrl` fica com o mesmo placeholder já usado nos outros sub-workflows do ClinicAll: `"http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA"`. +- **Lição da seção 17 do runbook — `$fromAI` só com um único `=` no início**: ao montar `workflowInputs.value` dos nodes `Call n8n Sub-Workflow Tool` do servidor, cada expressão deve ser exatamente `"={{ $fromAI(...) }}"` (um `=`). Como este plano cria os nodes via `n8n_create_workflow` diretamente (não copiando texto na UI), o bug do `==` duplicado não deve ocorrer — mas confira mesmo assim com `n8n_get_workflow` (mode `filtered`) antes de validar, mesma disciplina do plano do Klingo. +- **Campos opcionais precisam de valor-padrão no `$fromAI`**: `$fromAI('nome', 'descrição', 'tipo', valorPadrão)` — senão o schema gerado da tool trata o campo como obrigatório. +- Operações que trocam node + conexões devem ser feitas numa única chamada `n8n_update_partial_workflow`. +- Todo workflow (servidor + sub-workflows) precisa estar `active: true` para responder via protocolo MCP. +- Taxonomia de erro padrão (idêntica à já usada no MVP de agendamento do ClinicAll): `AUTHENTICATION_ERROR` (401/403), `NOT_FOUND` (404), `VALIDATION_ERROR` (400/422), `CLINICALL_API_ERROR` (demais). +- NPS e consulta de bloqueios ficam **fora de escopo** — não documentados no manual do ClinicAll para este módulo. + +Spec de referência: `docs/superpowers/specs/2026-08-11-confirmacao-agenda-clinicall-design.md` + +--- + +## Campos confirmados no manual (`docs/API Clinicall.pdf`, v1.1.16, já lido nesta sessão) + +### `GET /datasnap/rest/TCalendarController/agendamento/` +- Parâmetro: `data` obrigatório (route param, formato `YYYY-MM-DD`, conforme os exemplos do manual). +- Resposta: array de objetos `marcacao` com campos: `idAgenda`, `idUnidade`, `nomeUnidade`, `idPaciente` (pode ser `null` se livre), `Paciente` (objeto aninhado, pode ser `null`), `idProfissional`, `idProcedimento`, `nomeProcedimento`, `grupoProcedimento`, `dataAgenda`, `horaAgenda`, `idConvenio`, `carteiraConvenio`, `validadeCarteiraConvenio`, `situacaoAgenda`, `estadoAgenda`, `usuario`, `senhaAutorizacao`. O objeto `Paciente` aninhado (quando presente) tem `telefone` e `telefoneCelular` entre outros campos do paciente completo (seção 4.7 do manual). + +### `GET /datasnap/rest/TCalendarController/agendamentoSemPaciente` +- Parâmetros: `cpf` e `dataNascimento`, ambos obrigatórios (conforme documentado — "Parâmetro: Mandatório cpf e dataNascimento"). +- **Pendência não verificada**: o manual não mostra o formato exato da requisição (só a resposta) e lista `Methods: GET, POST`. Implementação desta etapa: `GET` com `cpf` e `dataNascimento` como **query string** — decisão por analogia com todos os demais endpoints GET deste manual (nenhum usa body), registrada como suposição não testada. +- Resposta: `{ paciente: {nome, nascimento, cpf, telefone, celular, idConvenio}, agendamentos: [{idAgenda, idunidade, idProfissional, idProcedimento, dataAgenda, horaAgenda}] }`. + +### `POST /datasnap/rest/TCalendarController/AgendamentoFlags` +- Body: `idAgenda` (obrigatório), `situacaoAgenda` (obrigatório, string exata), `estadoAgenda` (obrigatório, string exata), `confirmadoPor` (opcional, string), `usuarioConfirmou` (opcional, string), `dataConfirmacao` (opcional, string `YYYY-MM-DD`), `horarioFlag` (opcional, string `HH:MM`), `usuarioFlag` (opcional, string). +- Resposta: não documentada em detalhe — tratada como corpo genérico (`success`/`data` via classificação por status HTTP, mesmo padrão dos outros endpoints). + +### `GET /datasnap/rest/TDomainController/estadoAgenda` +- Parâmetro: opcional, descrição a pesquisar. +- Resposta: array de `{flagEstadoAgenda, estadoAgenda}`. Valores documentados: `P`→`PRESENTE`, `A`→`ATENDIDO`, `B`→`BLOQUEADO PODE SER IMAGE OU STATE INDEX`, `T`→`ATRASADO`, `E`→`EXAME`, `U`→`URGENTE`, `X`→`EXTRA`, `V`→`1A. VEZ`, `I`→`PRONTO OU MEDICO AVISADO`, `C`→`CONFIRMADO PELA TELEFONISTA`, `O`→`NAO CONFIRMADO`, `F`→`FALTOU`, `Z`→`FALTOU E NAO AVISOU`, `Y`→`MEDICO FALTOU`, `W`→`DESISTIU`, `H`→`ESPECIAL`, `K`→`REMARCOU`, `L`→`LIVRE`, `N`→`ENFERMAGEM (INTERNAMENTO AMBULATORIAL)`. + +### `GET /datasnap/rest/TDomainController/situacaoAgenda` +- Parâmetro: opcional, descrição a pesquisar. +- Resposta: array de `{flagSituacaoAgenda, situacaoAgenda}`. Valores documentados: `B`→`Bloqueado`, `G`→`Agendado`, `X`→`Extra`, `V`→`1a. Vez`, `R`→`Entrega de Resultado`, `D`→`Deu atendimento`, `N`→`Preventivo`, `M`→`Mensagem`, `S`→`Sem prontuário (paciente sem cadastro)`, `P`→`Presente`, `T`→`Atrasado`, `L`→`Livre`. + +--- + +## Pré-requisito (ação manual do usuário, quando houver clínica ClinicAll real) + +Nenhum pré-requisito de credencial nesta etapa — mesma decisão do MVP de +agendamento do ClinicAll (`authentication: none`). Quando houver clínica +real: preencher o `baseUrl` real nos 5 sub-workflows (hoje com +placeholder `http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA`) e +resolver a autenticação (contato com a Focus Tecnologia em Sistemas ou +teste empírico), documentado como pendência tanto no MVP de agendamento +quanto neste plano. + +--- + +### Task 1: Sub-workflow `listar_agendamentos_do_dia` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - ClinicAll - Confirmação - listar_agendamentos_do_dia`, consumido pela Task 6 (servidor) e pela Task 7 (lembrete proativo). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação - 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" }] } + } + }, + { + "id": "config", + "name": "Config ClinicAll", + "type": "n8n-nodes-base.set", + "typeVersion": 3.5, + "position": [260, 0], + "parameters": { + "mode": "manual", + "includeOtherFields": true, + "assignments": { + "assignments": [ + { "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" } + ] + } + } + }, + { + "id": "get-agendamento", + "name": "GET /agendamento", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [520, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/datasnap/rest/TCalendarController/agendamento/{{ $json.data }}", + "authentication": "none", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [780, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"listar agendamentos do dia\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] }, + "Config ClinicAll": { "main": [[{ "node": "GET /agendamento", "type": "main", "index": 0 }]] }, + "GET /agendamento": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado — necessário nas Tasks 6 e 7. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 2: Sub-workflow `buscar_agendamento_por_cpf` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - ClinicAll - Confirmação - buscar_agendamento_por_cpf`, consumido pela Task 6 e pelo workflow de exemplo (Task 9). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação - buscar_agendamento_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" }, + { "name": "dataNascimento", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config ClinicAll", + "type": "n8n-nodes-base.set", + "typeVersion": 3.5, + "position": [260, 0], + "parameters": { + "mode": "manual", + "includeOtherFields": true, + "assignments": { + "assignments": [ + { "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" } + ] + } + } + }, + { + "id": "montar-query", + "name": "Montar query", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [520, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $json;\nconst queryParams = { cpf: input.cpf, dataNascimento: input.dataNascimento };\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};" + } + }, + { + "id": "get-agendamento-sem-paciente", + "name": "GET /agendamentoSemPaciente", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/datasnap/rest/TCalendarController/agendamentoSemPaciente", + "authentication": "none", + "sendQuery": true, + "specifyQuery": "json", + "jsonQuery": "={{ JSON.stringify($json.queryParams) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"buscar agendamento por CPF\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] }, + "Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /agendamentoSemPaciente", "type": "main", "index": 0 }]] }, + "GET /agendamentoSemPaciente": { "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 `confirmar_presenca` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflow `MCP Server - ClinicAll - Confirmação - confirmar_presenca`, consumido pela Task 6 e pelo workflow de exemplo (Task 9). + +- [ ] **Step 1: Criar o sub-workflow** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação - confirmar_presenca", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { + "values": [ + { "name": "idAgenda", "type": "string" }, + { "name": "situacaoAgenda", "type": "string" }, + { "name": "estadoAgenda", "type": "string" }, + { "name": "confirmadoPor", "type": "string" }, + { "name": "usuarioConfirmou", "type": "string" }, + { "name": "dataConfirmacao", "type": "string" }, + { "name": "horarioFlag", "type": "string" }, + { "name": "usuarioFlag", "type": "string" } + ] + } + } + }, + { + "id": "config", + "name": "Config ClinicAll", + "type": "n8n-nodes-base.set", + "typeVersion": 3.5, + "position": [260, 0], + "parameters": { + "mode": "manual", + "includeOtherFields": true, + "assignments": { + "assignments": [ + { "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" } + ] + } + } + }, + { + "id": "montar-corpo", + "name": "Montar corpo", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [520, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $json;\nconst requestBody = {\n idAgenda: input.idAgenda,\n situacaoAgenda: input.situacaoAgenda,\n estadoAgenda: input.estadoAgenda,\n};\nif (input.confirmadoPor) requestBody.confirmadoPor = input.confirmadoPor;\nif (input.usuarioConfirmou) requestBody.usuarioConfirmou = input.usuarioConfirmou;\nif (input.dataConfirmacao) requestBody.dataConfirmacao = input.dataConfirmacao;\nif (input.horarioFlag) requestBody.horarioFlag = input.horarioFlag;\nif (input.usuarioFlag) requestBody.usuarioFlag = input.usuarioFlag;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n requestBody,\n },\n};" + } + }, + { + "id": "post-agendamento-flags", + "name": "POST /AgendamentoFlags", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "POST", + "url": "={{ $json.baseUrl }}/datasnap/rest/TCalendarController/AgendamentoFlags", + "authentication": "none", + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify($json.requestBody) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"confirmar presença\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] }, + "Config ClinicAll": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] }, + "Montar corpo": { "main": [[{ "node": "POST /AgendamentoFlags", "type": "main", "index": 0 }]] }, + "POST /AgendamentoFlags": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +`estadoAgenda` e `situacaoAgenda` devem ser documentados na +`description` da tool (Task 6) como exigindo as strings **exatas** +retornadas por `consultar_estados_agenda`/`consultar_situacoes_agenda` +— nunca inventadas pelo modelo. + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 4: Sub-workflows `consultar_estados_agenda` e `consultar_situacoes_agenda` + +**Interfaces:** +- Consumes: nenhuma. +- Produces: workflows `MCP Server - ClinicAll - Confirmação - consultar_estados_agenda` e `MCP Server - ClinicAll - Confirmação - consultar_situacoes_agenda`, consumidos pela Task 6 e pelo workflow de exemplo (Task 9). + +- [ ] **Step 1: Criar `consultar_estados_agenda`** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação - consultar_estados_agenda", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { "values": [{ "name": "nome", "type": "string" }] } + } + }, + { + "id": "config", + "name": "Config ClinicAll", + "type": "n8n-nodes-base.set", + "typeVersion": 3.5, + "position": [260, 0], + "parameters": { + "mode": "manual", + "includeOtherFields": true, + "assignments": { + "assignments": [ + { "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" } + ] + } + } + }, + { + "id": "montar-query", + "name": "Montar query", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [520, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};" + } + }, + { + "id": "get-estado-agenda", + "name": "GET /estadoAgenda", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/datasnap/rest/TDomainController/estadoAgenda", + "authentication": "none", + "sendQuery": true, + "specifyQuery": "json", + "jsonQuery": "={{ JSON.stringify($json.queryParams) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar estados de agenda\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] }, + "Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /estadoAgenda", "type": "main", "index": 0 }]] }, + "GET /estadoAgenda": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar `consultar_estados_agenda`** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +- [ ] **Step 3: Criar `consultar_situacoes_agenda`** + +`n8n_create_workflow`: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação - consultar_situacoes_agenda", + "nodes": [ + { + "id": "trigger", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "parameters": { + "workflowInputs": { "values": [{ "name": "nome", "type": "string" }] } + } + }, + { + "id": "config", + "name": "Config ClinicAll", + "type": "n8n-nodes-base.set", + "typeVersion": 3.5, + "position": [260, 0], + "parameters": { + "mode": "manual", + "includeOtherFields": true, + "assignments": { + "assignments": [ + { "id": "cfg-baseurl", "name": "baseUrl", "type": "string", "value": "http://SUBSTITUIR-PELO-HOST-REAL-DA-CLINICA:PORTA" } + ] + } + } + }, + { + "id": "montar-query", + "name": "Montar query", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [520, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $json;\nconst queryParams = {};\nif (input.nome) queryParams.nome = input.nome;\n\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryParams,\n },\n};" + } + }, + { + "id": "get-situacao-agenda", + "name": "GET /situacaoAgenda", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [780, 0], + "parameters": { + "method": "GET", + "url": "={{ $json.baseUrl }}/datasnap/rest/TDomainController/situacaoAgenda", + "authentication": "none", + "sendQuery": true, + "specifyQuery": "json", + "jsonQuery": "={{ JSON.stringify($json.queryParams) }}", + "options": { + "response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } }, + "timeout": 15000 + } + } + }, + { + "id": "classificar", + "name": "Classificar resultado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 0], + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const status = $json.statusCode;\nconst rawBody = $json.body;\n\nfunction classifyError(status, context) {\n if (status === 401 || status === 403) {\n return { code: \"AUTHENTICATION_ERROR\", message: `Falha de autenticação na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do ClinicAll ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do ClinicAll rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"CLINICALL_API_ERROR\", message: `A API do ClinicAll retornou um erro inesperado ao ${context} (status ${status}).` };\n}\n\nif (status < 200 || status >= 300) {\n return { json: { success: false, error: classifyError(status, \"consultar situações de agenda\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };" + } + } + ], + "connections": { + "Execute Workflow Trigger": { "main": [[{ "node": "Config ClinicAll", "type": "main", "index": 0 }]] }, + "Config ClinicAll": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] }, + "Montar query": { "main": [[{ "node": "GET /situacaoAgenda", "type": "main", "index": 0 }]] }, + "GET /situacaoAgenda": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 4: Validar `consultar_situacoes_agenda`** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +--- + +### Task 5: Servidor MCP "ClinicAll - Confirmação de Agenda" com as 5 tools + +**Interfaces:** +- Consumes: os 5 workflow ids das Tasks 1–4. +- Produces: workflow `MCP Server - ClinicAll - Confirmação de Agenda` (servidor), path `mcp-server-clinicall-confirmacao`, consumido pela Task 9 (workflow de exemplo). + +- [ ] **Step 1: Criar o servidor com o `MCP Server Trigger` e os 5 nodes de tool** + +`n8n_create_workflow` — substituir `ID_LISTAR`, `ID_BUSCAR_CPF`, +`ID_CONFIRMAR`, `ID_ESTADOS`, `ID_SITUACOES` pelos ids reais anotados +nas Tasks 1–4: + +```json +{ + "name": "MCP Server - ClinicAll - Confirmação de Agenda", + "nodes": [ + { + "id": "mcp-trigger", + "name": "MCP Server Trigger", + "type": "@n8n/n8n-nodes-langchain.mcpTrigger", + "typeVersion": 1, + "position": [0, 256], + "parameters": { "path": "mcp-server-clinicall-confirmacao", "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 ClinicAll, incluindo livres e agendados. 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') }}" }, + "matchingColumns": [], + "schema": [{ "id": "data", "displayName": "data", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-buscar-cpf", + "name": "buscar_agendamento_por_cpf", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [-160, 448], + "parameters": { + "description": "Busca o paciente e a lista de agendamentos pelo CPF e data de nascimento. Use isso para descobrir qual agendamento o paciente está confirmando — precisa pedir CPF E data de nascimento ao paciente, os dois são obrigatórios.", + "workflowId": { "value": "ID_BUSCAR_CPF", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "cpf": "={{ $fromAI('cpf', 'CPF do paciente, somente números', 'string') }}", + "dataNascimento": "={{ $fromAI('dataNascimento', 'Data de nascimento do paciente, formato YYYY-MM-DD', 'string') }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "cpf", "displayName": "cpf", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "dataNascimento", "displayName": "dataNascimento", "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": [0, 448], + "parameters": { + "description": "Registra a confirmação de presença de um agendamento no ClinicAll. estadoAgenda e situacaoAgenda devem ser strings EXATAS obtidas antes via consultar_estados_agenda e consultar_situacoes_agenda — nunca invente esses valores. Ex.: para confirmar, use estadoAgenda='CONFIRMADO PELA TELEFONISTA' e situacaoAgenda='Agendado'.", + "workflowId": { "value": "ID_CONFIRMAR", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "idAgenda": "={{ $fromAI('idAgenda', 'Id do agendamento/marcação a confirmar', 'string') }}", + "situacaoAgenda": "={{ $fromAI('situacaoAgenda', 'Valor exato de situacaoAgenda obtido via consultar_situacoes_agenda', 'string') }}", + "estadoAgenda": "={{ $fromAI('estadoAgenda', 'Valor exato de estadoAgenda obtido via consultar_estados_agenda', 'string') }}", + "confirmadoPor": "={{ $fromAI('confirmadoPor', 'Nome de quem confirmou, quando não é o próprio paciente respondendo (opcional)', 'string', '') }}", + "usuarioConfirmou": "={{ $fromAI('usuarioConfirmou', 'Identificador do usuário/sistema que processou a confirmação (opcional)', 'string', 'bot-whatsapp') }}", + "dataConfirmacao": "={{ $fromAI('dataConfirmacao', 'Data da confirmação, formato YYYY-MM-DD (opcional)', 'string', '') }}", + "horarioFlag": "={{ $fromAI('horarioFlag', 'Horário da confirmação, formato HH:MM (opcional)', 'string', '') }}", + "usuarioFlag": "={{ $fromAI('usuarioFlag', 'Identificador do usuário/sistema que gerou a flag (opcional)', 'string', 'bot-whatsapp') }}" + }, + "matchingColumns": [], + "schema": [ + { "id": "idAgenda", "displayName": "idAgenda", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "situacaoAgenda", "displayName": "situacaoAgenda", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "estadoAgenda", "displayName": "estadoAgenda", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "confirmadoPor", "displayName": "confirmadoPor", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "usuarioConfirmou", "displayName": "usuarioConfirmou", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "dataConfirmacao", "displayName": "dataConfirmacao", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "horarioFlag", "displayName": "horarioFlag", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }, + { "id": "usuarioFlag", "displayName": "usuarioFlag", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-estados", + "name": "consultar_estados_agenda", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [160, 448], + "parameters": { + "description": "Lista os valores exatos e válidos de estadoAgenda (ex.: 'CONFIRMADO PELA TELEFONISTA', 'NAO CONFIRMADO', 'FALTOU'). Chame isso ANTES de confirmar_presenca para saber a string exata a usar — nunca invente um valor de estadoAgenda.", + "workflowId": { "value": "ID_ESTADOS", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { "nome": "={{ $fromAI('nome', 'Texto para filtrar a busca (opcional)', 'string', '') }}" }, + "matchingColumns": [], + "schema": [{ "id": "nome", "displayName": "nome", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" }], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + } + }, + { + "id": "tool-situacoes", + "name": "consultar_situacoes_agenda", + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [320, 448], + "parameters": { + "description": "Lista os valores exatos e válidos de situacaoAgenda (ex.: 'Agendado', 'Presente', 'Atrasado'). Chame isso ANTES de confirmar_presenca para saber a string exata a usar — nunca invente um valor de situacaoAgenda.", + "workflowId": { "value": "ID_SITUACOES", "mode": "id" }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { "nome": "={{ $fromAI('nome', 'Texto para filtrar a busca (opcional)', 'string', '') }}" }, + "matchingColumns": [], + "schema": [{ "id": "nome", "displayName": "nome", "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 }]] }, + "buscar_agendamento_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 }]] }, + "consultar_estados_agenda": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }, + "consultar_situacoes_agenda": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] } + } +} +``` + +Anotar o `id` do servidor retornado — necessário na Task 9. + +- [ ] **Step 2: Conferir visualmente os textos salvos dos `$fromAI`** + +`n8n_get_workflow` com `{"id": "", "mode": "filtered", "nodeNames": ["listar_agendamentos_do_dia", "buscar_agendamento_por_cpf", "confirmar_presenca", "consultar_estados_agenda", "consultar_situacoes_agenda"]}`. Conferir que cada valor de `workflowInputs.value` começa com um único `=` (não `==`) — lição da seção 17 do runbook. + +- [ ] **Step 3: Validar** + +`n8n_validate_workflow` com o id do servidor. Esperado: `errorCount: 0`. + +--- + +### Task 6: Ativar os 6 workflows do MCP Server + +**Interfaces:** +- Consumes: os 6 workflow ids (5 sub-workflows das Tasks 1–4 + servidor da Task 5). +- Produces: MCP Server "ClinicAll - Confirmação de Agenda" respondendo via protocolo MCP (estrutura pronta; sem teste de chamada real, por falta de clínica/autenticação real). + +- [ ] **Step 1: Ativar cada um dos 6 workflows** + +Para cada um dos 6 ids (5 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 6 ids — +confirmar `"active": true` em todos. + +--- + +### Task 7: Workflow proativo "Lembrete de Confirmação - ClinicAll (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 - ClinicAll (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 - ClinicAll (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 agendamentos com paciente", + "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 : [];\n// Só interessam marcações com paciente vinculado — itens \"Livre\" (idPaciente\n// null) não têm ninguém pra confirmar presença.\nconst comPaciente = marcacoes.filter(m => m.idPaciente && m.Paciente);\nreturn comPaciente.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.Paciente.telefoneCelular || $json.Paciente.telefone }}" }, + { "name": "textoLembrete", "type": "string", "value": "=Olá, {{ $json.Paciente.nomePaciente }}! Passando para confirmar sua consulta de {{ $json.nomeProcedimento }} amanhã ({{ $json.dataAgenda }} às {{ $json.horaAgenda }}) na unidade {{ $json.nomeUnidade }}. Você confirma presença? Responda SIM ou NÃO." }, + { "name": "externalKey", "type": "string", "value": "=lembrete-clinicall-{{ $json.idAgenda }}" } + ] + }, + "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 agendamentos com paciente", "type": "main", "index": 0 }]] }, + "Extrair agendamentos com paciente": { "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: `Extrair agendamentos com paciente` filtra fora os itens +`"Livre"` (sem `idPaciente`/`Paciente`) — diferente do Klingo, onde +`/lista/{data}` já retorna só marcações reais. O telefone vem de +`item.Paciente.telefoneCelular` (objeto aninhado), não de um campo +direto do item. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`. + +- [ ] **Step 3: Deixar inativo** + +Mesma razão do Klingo — sem clínica ClinicAll real, um cron ativo +geraria chamadas reais com erro contra a API e tentativas de envio com +dados vazios. Ativação fica para quando houver clínica real. + +--- + +### Task 8: Workflow de exemplo "AI Agent — Confirmação ClinicAll (exemplo)" + +**Interfaces:** +- Consumes: nenhuma (é o workflow raiz do exemplo). +- Produces: workflow `Confirmação de Agenda - GChat - ClinicAll+Z-PRO (exemplo)`, consumido pela Task 9. + +- [ ] **Step 1: Criar o workflow** + +`n8n_create_workflow` — mesmo contrato de webhook Z-PRO/GChat já usado +no exemplo do Klingo (`body.protocol`→`sessionId`, `body.entrada`→`mensagem`, +`body.contactName`→`nomeContato`, `body.number`→`telefone`), mesma +credencial OpenAI real e compartilhada (`29vMIDah5DTIhUYJ`, "GPT"): + +```json +{ + "name": "Confirmação de Agenda - GChat - ClinicAll+Z-PRO (exemplo)", + "nodes": [ + { + "id": "webhook", + "name": "Webhook GChat", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [0, 0], + "parameters": { "httpMethod": "POST", "path": "confirmacao-agenda-clinicall-exemplo", "responseMode": "lastNode", "options": {} } + }, + { + "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.protocol }}" }, + { "name": "mensagem", "type": "string", "value": "={{ String($json.body.entrada || '').trim() }}" }, + { "name": "nomeContato", "type": "string", "value": "={{ $json.body.contactName }}" }, + { "name": "telefone", "type": "string", "value": "={{ $json.body.number }}" } + ] + } + } + }, + { + "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, para uma clínica que usa o sistema ClinicAll. Seu trabalho: 1) entender se a mensagem do paciente é uma resposta de confirmação (ex.: 'sim', 'confirmo') ou recusa (ex.: 'não', 'não posso ir') a uma consulta agendada; 2) se for, peça ao paciente o CPF E a data de nascimento (os dois são obrigatórios no ClinicAll, diferente de outros sistemas que só pedem CPF) e use a tool buscar_agendamento_por_cpf para achar o idAgenda pendente; 3) antes de confirmar, use consultar_estados_agenda e consultar_situacoes_agenda para saber os valores EXATOS aceitos — nunca invente uma string de estado ou situação; 4) chame confirmar_presenca com idAgenda, situacaoAgenda='Agendado' e estadoAgenda='CONFIRMADO PELA TELEFONISTA' (se o paciente confirmou) ou estadoAgenda='NAO CONFIRMADO'/'FALTOU' (se recusou ou não pôde comparecer), conforme o caso; 5) responda ao paciente confirmando que o registro foi feito. Se a mensagem não for uma resposta de confirmação/recusa clara, peça esclarecimento. Nunca invente um idAgenda, estadoAgenda ou situacaoAgenda — sempre busque/consulte antes de confirmar." + } + }, + { + "id": "openai-chat-model", + "name": "OpenAI Chat Model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.3, + "position": [448, -160], + "parameters": { "model": { "mode": "id", "value": "gpt-4o-mini" }, "builtInTools": {}, "options": {} }, + "credentials": { "openAiApi": { "id": "29vMIDah5DTIhUYJ", "name": "GPT" } } + }, + { + "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 }]] }, + "OpenAI Chat Model": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] }, + "Memória da Sessão (Postgres)": { "ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]] } + } +} +``` + +Anotar o `id` retornado. + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0` +(a credencial `postgres` é placeholder — aceitável, sem cliente real). + +--- + +### Task 9: Conectar as tools do MCP ClinicAll ao AI Agent de exemplo + +**Interfaces:** +- Consumes: workflow servidor `MCP Server - ClinicAll - Confirmação de Agenda` (Task 5, id do servidor) e workflow `Confirmação de Agenda - GChat - ClinicAll+Z-PRO (exemplo)` (Task 8). +- Produces: `AI Agent` do workflow de exemplo com acesso às 4 tools de confirmação (não `listar_agendamentos_do_dia`, que só serve ao lembrete proativo). + +- [ ] **Step 1: Adicionar o node `MCP Client Tool` conectado ao AI Agent** + +`n8n_update_partial_workflow` no workflow da Task 8, numa única chamada +atômica: + +```json +{ + "id": "", + "operations": [ + { + "type": "addNode", + "node": { + "id": "mcp-client-clinicall-confirmacao", + "name": "MCP ClinicAll Confirmação", + "type": "@n8n/n8n-nodes-langchain.mcpClientTool", + "typeVersion": 1, + "position": [448, 320], + "parameters": { + "endpointUrl": "https://devn8n.gtigrupo.com.br/mcp/mcp-server-clinicall-confirmacao", + "authentication": "none", + "include": "selected", + "includeTools": ["buscar_agendamento_por_cpf", "confirmar_presenca", "consultar_estados_agenda", "consultar_situacoes_agenda"] + } + } + }, + { "type": "addConnection", "source": "MCP ClinicAll Confirmação", "target": "AI Agent", "sourceOutput": "ai_tool", "targetIndex": 0 } + ] +} +``` + +- [ ] **Step 2: Validar** + +`n8n_validate_workflow` no workflow da Task 8. Esperado: `errorCount: 0`. + +- [ ] **Step 3: Deixar inativo** + +Mesma razão das outras tasks — sem credenciais reais (Postgres) nem +clínica ClinicAll real, ativar não teria efeito útil. Fica pronto pra +ativar quando houver cliente real. + +--- + +### Task 10: Atualizar o runbook e commit + +**Interfaces:** +- Consumes: todos os ids de workflow criados nas Tasks 1–9. + +- [ ] **Step 1: Adicionar seção 19 ao runbook** + +Adicionar em `docs/n8n-agendamento-runbook.md`, após a seção 18, uma +seção "19. Confirmação de Agenda via WhatsApp (ClinicAll)" cobrindo: as +5 tools do MCP Server e seus endpoints, os ids de todos os 8 workflows +criados (5 sub-workflows + servidor + lembrete proativo + AI Agent de +exemplo), as diferenças reais em relação ao Klingo (strings exatas de +`estadoAgenda`/`situacaoAgenda` em vez de código de 1 letra; CPF + +data de nascimento em vez de só CPF; telefone aninhado em +`Paciente.telefoneCelular`; sem NPS/bloqueios), a pendência de formato +não verificado de `agendamentoSemPaciente` (GET com query string, +suposição não testada), e o status "estrutura pronta, sem teste de +ponta a ponta — falta clínica ClinicAll real e resolução da +autenticação". Seguir o mesmo estilo de prosa das seções 14, 17 e 18. + +- [ ] **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 ClinicAll Confirmação e confirmação de agenda via WhatsApp + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +## Observação sobre execução + +Mesma disciplina dos planos anteriores: 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 5, Step 2). Os workflows de +lembrete proativo (Task 7) e de AI Agent de exemplo (Tasks 8–9) ficam +**intencionalmente inativos** ao final deste plano — sem clínica +ClinicAll real, sem autenticação resolvida, e sem credencial Postgres +válida, ativá-los só geraria execuções com erro. Diferente do Klingo +(que usa `httpHeaderAuth`), todos os nodes HTTP deste plano usam +`authentication: "none"`, replicando exatamente a decisão já tomada e +documentada no MVP de agendamento do ClinicAll — não é uma +inconsistência, é a mesma pendência de autenticação ainda não +resolvida.