Files
KLINGO-CLINICAL-SYSLIFE-GCHAT/docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md
T
2026-08-11 16:07:05 -03:00

1053 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<data>`
- 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": "<id do servidor>", "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": "<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": "<id do workflow da Task 8>",
"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 <noreply@anthropic.com>"
```
---
## 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.