Files
KLINGO-CLINICAL-SYSLIFE-GCHAT/docs/superpowers/plans/2026-08-11-confirmacao-agenda-syslife.md
2026-08-11 16:21:10 -03:00

793 lines
37 KiB
Markdown
Raw Permalink 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 (SysLife) 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 "SysLife - Confirmação de Agenda" (3 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 SysLife real disponível para teste de ponta a ponta — mesmo padrão já executado para Klingo (`docs/superpowers/plans/2026-08-11-confirmacao-agenda-klingo.md`) e ClinicAll (`docs/superpowers/plans/2026-08-11-confirmacao-agenda-clinicall.md`).
**Architecture:** Mesmo padrão de MCP Server nativo n8n dos outros providers: `MCP Server Trigger` + N × `Call n8n Sub-Workflow Tool`, cada um apontando pra um sub-workflow `Execute Workflow Trigger` → `Config SysLife` → `Montar query`/`Montar corpo` (Code) → `HTTP Request` → `Classificar resultado` (Code). Reaproveita **exatamente** o estilo de node já usado no MVP de agendamento do SysLife (`MCP Server - SysLife - consultar_horarios`, `GCbnkMM1JTnVXRSr`, lido nesta sessão como referência): `HTTP Request` com `authentication: "genericCredentialType"` + `genericAuthType: "httpBasicAuth"`, credencial `httpBasicAuth` já existente (`u8XXJ6wyrKuGJp4Y`), `sendBody: true` mesmo em métodos GET (documentado assim no manual do SysLife). O recurso `AgendaWSApp` é único e serve três propósitos diferentes conforme o parâmetro enviado — duas tools deste MCP (`listar_agendamentos_do_dia`, `buscar_agendamento_por_telefone`) apontam pra sub-workflows distintos que chamam o mesmo endpoint com parâmetros diferentes. O workflow de lembrete chama a tool via `Execute Workflow` direto (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 SysLife via `MCP Client Tool`.
**Tech Stack:** n8n (`devn8n.gtigrupo.com.br`), MCP tools do n8n (`n8n_create_workflow`, `n8n_update_partial_workflow`, `n8n_validate_workflow`, `n8n_get_workflow`), SysLife WebService (`http://<IP>:9000/AgendaWSApp`, Basic Auth), 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/Manual WS SysLife V3.4 (1).pdf`).
- **Credencial Basic Auth já existe e é reaproveitada**: `httpBasicAuth`, id `u8XXJ6wyrKuGJp4Y`, nome `SysLife Basic Auth - clínica pendente` — usuário/senha fixos do WebService (`WebSysLife`/`$Sys10Life`), documentados literalmente no manual, não variam por clínica. `baseUrl` fica com o mesmo placeholder já usado nos sub-workflows do MVP de agendamento: `"http://SUBSTITUIR-PELO-IP-REAL-DA-CLINICA:9000"`.
- **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, o bug do `==` duplicado não deve ocorrer — conferir mesmo assim com `n8n_get_workflow` (mode `filtered`) antes de validar.
- **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. Neste plano, todos os campos das 3 tools são obrigatórios (o manual não documenta nenhum campo opcional em `AgendaWSApp`), então nenhum `$fromAI` precisa de 4º argumento — mas confira mesmo assim.
- 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 SysLife): `AUTHENTICATION_ERROR` (401/403), `NOT_FOUND` (404), `VALIDATION_ERROR` (400/422), `SYSLIFE_API_ERROR` (demais).
- `AgendaCentral`, `AgendaRetorno`, `AgendaStatus`, `AgendaConfirmado` ficam **fora de escopo** — não implementar tools para esses endpoints.
- **Regra do manual**: `GET /AgendaWSApp` aceita `QuantidadeDias` **ou** `CodigoAgenda` **ou** `NumeroCelular` — nunca mais de um ao mesmo tempo. Cada sub-workflow de leitura deste plano monta o body só com o parâmetro que lhe cabe, nunca os dois.
Spec de referência: `docs/superpowers/specs/2026-08-11-confirmacao-agenda-syslife-design.md`
---
## Campos confirmados no manual (`docs/Manual WS SysLife V3.4 (1).pdf`, já lido nesta sessão)
### `GET /AgendaWSApp`
- Body: **um único** de `QuantidadeDias` (number), `CodigoAgenda` (number) ou `NumeroCelular` (string, DDD + número).
- Resposta: array de objetos com `coAgenda`, `dtAtendimento`, `hrAtendimentoAlterado`, `nmPaciente`, `nmLogonWeb`, `nmSenhaWeb`, `coMedico`, `txTituloMedico`, `nmMedico`, `nmEspecialidade`, `nmOrigemDestino`, `coProcedimento`, `nmProcedimento`, `nuTelefone`, `nuTelefoneDdd`, `nuCelular`, `nuCelularDdd`, `txObservacao`, `txInstrucaoProc`, `txWhatsappProcedimento`, `txWhatsappHorario`, `txWhatsappMedico`, `txEndereco`, `txBairro`, `txComplemento`, `txCidade`, `txEstado`, `nmMsgSms`, `nmMsgSmsExtra`.
### `POST /AgendaWSApp`
- Body: `CodigoAgenda` (number, obrigatório), `Resposta` (string, obrigatório — `"1"` = SIM, `"2"` = NÃO).
- Resposta: `{ "Status": "?" }` (mensagem de sucesso ou erro).
---
## Pré-requisito
Nenhum — a credencial Basic Auth já existe no n8n (`u8XXJ6wyrKuGJp4Y`), criada durante o MVP de agendamento do SysLife. Quando houver clínica real: preencher o `baseUrl` real (IP:9000) nos 3 sub-workflows.
---
### Task 1: Sub-workflow `listar_agendamentos_do_dia`
**Interfaces:**
- Consumes: nenhuma.
- Produces: workflow `MCP Server - SysLife - Confirmação - listar_agendamentos_do_dia`, consumido pela Task 4 (servidor) e pela Task 6 (lembrete proativo).
- [ ] **Step 1: Criar o sub-workflow**
`n8n_create_workflow`:
```json
{
"name": "MCP Server - SysLife - 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": "quantidadeDias", "type": "number" }] }
}
},
{
"id": "config",
"name": "Config SysLife",
"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-IP-REAL-DA-CLINICA:9000" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryBody: {\n QuantidadeDias: input.quantidadeDias,\n },\n },\n};"
}
},
{
"id": "get-agendawsapp",
"name": "GET /AgendaWSApp",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/AgendaWSApp",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.queryBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
},
"credentials": {
"httpBasicAuth": { "id": "u8XXJ6wyrKuGJp4Y", "name": "SysLife Basic Auth - clínica pendente" }
}
},
{
"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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] },
"Config SysLife": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /AgendaWSApp", "type": "main", "index": 0 }]] },
"GET /AgendaWSApp": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
Anotar o `id` retornado — necessário nas Tasks 4 e 6.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`.
---
### Task 2: Sub-workflow `buscar_agendamento_por_telefone`
**Interfaces:**
- Consumes: nenhuma.
- Produces: workflow `MCP Server - SysLife - Confirmação - buscar_agendamento_por_telefone`, consumido pela Task 4 e pelo workflow de exemplo (Task 7).
- [ ] **Step 1: Criar o sub-workflow**
`n8n_create_workflow`:
```json
{
"name": "MCP Server - SysLife - Confirmação - buscar_agendamento_por_telefone",
"nodes": [
{
"id": "trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [0, 0],
"parameters": {
"workflowInputs": { "values": [{ "name": "numeroCelular", "type": "string" }] }
}
},
{
"id": "config",
"name": "Config SysLife",
"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-IP-REAL-DA-CLINICA:9000" }
]
}
}
},
{
"id": "montar-query",
"name": "Montar query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nreturn {\n json: {\n baseUrl: input.baseUrl,\n queryBody: {\n NumeroCelular: input.numeroCelular,\n },\n },\n};"
}
},
{
"id": "get-agendawsapp",
"name": "GET /AgendaWSApp",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "GET",
"url": "={{ $json.baseUrl }}/AgendaWSApp",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.queryBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
},
"credentials": {
"httpBasicAuth": { "id": "u8XXJ6wyrKuGJp4Y", "name": "SysLife Basic Auth - clínica pendente" }
}
},
{
"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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 telefone\") } };\n}\n\nreturn { json: { success: true, data: rawBody } };"
}
}
],
"connections": {
"Execute Workflow Trigger": { "main": [[{ "node": "Config SysLife", "type": "main", "index": 0 }]] },
"Config SysLife": { "main": [[{ "node": "Montar query", "type": "main", "index": 0 }]] },
"Montar query": { "main": [[{ "node": "GET /AgendaWSApp", "type": "main", "index": 0 }]] },
"GET /AgendaWSApp": { "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 - SysLife - Confirmação - confirmar_presenca`, consumido pela Task 4 e pelo workflow de exemplo (Task 7).
- [ ] **Step 1: Criar o sub-workflow**
`n8n_create_workflow`:
```json
{
"name": "MCP Server - SysLife - 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": "codigoAgenda", "type": "number" },
{ "name": "resposta", "type": "string" }
]
}
}
},
{
"id": "config",
"name": "Config SysLife",
"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-IP-REAL-DA-CLINICA:9000" }
]
}
}
},
{
"id": "montar-corpo",
"name": "Montar corpo",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [520, 0],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $json;\nreturn {\n json: {\n baseUrl: input.baseUrl,\n requestBody: {\n CodigoAgenda: input.codigoAgenda,\n Resposta: input.resposta,\n },\n },\n};"
}
},
{
"id": "post-agendawsapp",
"name": "POST /AgendaWSApp",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [780, 0],
"parameters": {
"method": "POST",
"url": "={{ $json.baseUrl }}/AgendaWSApp",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.requestBody) }}",
"options": {
"response": { "response": { "fullResponse": true, "neverError": true, "responseFormat": "autodetect" } },
"timeout": 15000
}
},
"credentials": {
"httpBasicAuth": { "id": "u8XXJ6wyrKuGJp4Y", "name": "SysLife Basic Auth - clínica pendente" }
}
},
{
"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 SysLife ao ${context} (status ${status}).` };\n }\n if (status === 404) {\n return { code: \"NOT_FOUND\", message: `Recurso não encontrado na API do SysLife ao ${context} (status ${status}).` };\n }\n if (status === 400 || status === 422) {\n return { code: \"VALIDATION_ERROR\", message: `A API do SysLife rejeitou os dados enviados ao ${context} (status ${status}).` };\n }\n return { code: \"SYSLIFE_API_ERROR\", message: `A API do SysLife 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 SysLife", "type": "main", "index": 0 }]] },
"Config SysLife": { "main": [[{ "node": "Montar corpo", "type": "main", "index": 0 }]] },
"Montar corpo": { "main": [[{ "node": "POST /AgendaWSApp", "type": "main", "index": 0 }]] },
"POST /AgendaWSApp": { "main": [[{ "node": "Classificar resultado", "type": "main", "index": 0 }]] }
}
}
```
`resposta` deve ser documentado na `description` da tool (Task 4) como
aceitando só `"1"` (SIM) ou `"2"` (NÃO), conforme o manual.
Anotar o `id` retornado.
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`.
---
### Task 4: Servidor MCP "SysLife - Confirmação de Agenda" com as 3 tools
**Interfaces:**
- Consumes: os 3 workflow ids das Tasks 1–3.
- Produces: workflow `MCP Server - SysLife - Confirmação de Agenda` (servidor), path `mcp-server-syslife-confirmacao`, consumido pela Task 7 (workflow de exemplo).
- [ ] **Step 1: Criar o servidor com o `MCP Server Trigger` e os 3 nodes de tool**
`n8n_create_workflow` — substituir `ID_LISTAR`, `ID_BUSCAR_TELEFONE`,
`ID_CONFIRMAR` pelos ids reais anotados nas Tasks 1–3:
```json
{
"name": "MCP Server - SysLife - 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-syslife-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 de uma data futura no SysLife, contando a partir de hoje. Usado como base do lembrete proativo de confirmação — use quantidadeDias=1 para os agendamentos de amanhã.",
"workflowId": { "value": "ID_LISTAR", "mode": "id" },
"workflowInputs": {
"mappingMode": "defineBelow",
"value": { "quantidadeDias": "={{ $fromAI('quantidadeDias', 'Quantidade de dias a partir de hoje (1 = amanhã, 2 = depois de amanhã, etc.)', 'number') }}" },
"matchingColumns": [],
"schema": [{ "id": "quantidadeDias", "displayName": "quantidadeDias", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "number" }],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
}
},
{
"id": "tool-buscar-telefone",
"name": "buscar_agendamento_por_telefone",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [-160, 448],
"parameters": {
"description": "Busca o(s) agendamento(s) pendente(s) de um paciente pelo número de celular (DDD + número, só dígitos). Use isso para descobrir qual agendamento o paciente está confirmando quando ele responde 'sim'/'não'.",
"workflowId": { "value": "ID_BUSCAR_TELEFONE", "mode": "id" },
"workflowInputs": {
"mappingMode": "defineBelow",
"value": { "numeroCelular": "={{ $fromAI('numeroCelular', 'Número de celular do paciente, com DDD, somente dígitos (ex.: 71999999999)', 'string') }}" },
"matchingColumns": [],
"schema": [{ "id": "numeroCelular", "displayName": "numeroCelular", "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 SysLife. resposta deve ser exatamente '1' (paciente confirmou) ou '2' (paciente não confirmou/recusou) — nenhum outro valor é aceito. Use depois de identificar o codigoAgenda correto (ex.: via buscar_agendamento_por_telefone).",
"workflowId": { "value": "ID_CONFIRMAR", "mode": "id" },
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"codigoAgenda": "={{ $fromAI('codigoAgenda', 'Código do agendamento (coAgenda) a confirmar', 'number') }}",
"resposta": "={{ $fromAI('resposta', \"'1' se o paciente confirmou presença, '2' se recusou/não vai comparecer\", 'string') }}"
},
"matchingColumns": [],
"schema": [
{ "id": "codigoAgenda", "displayName": "codigoAgenda", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "number" },
{ "id": "resposta", "displayName": "resposta", "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_telefone": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
"confirmar_presenca": { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }
}
}
```
Anotar o `id` do servidor retornado — necessário na Task 7.
- [ ] **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_telefone", "confirmar_presenca"]}`. Conferir que cada valor de `workflowInputs.value` começa com um único `=` (não `==`).
- [ ] **Step 3: Validar**
`n8n_validate_workflow` com o id do servidor. Esperado: `errorCount: 0`.
---
### Task 5: Ativar os 4 workflows do MCP Server
**Interfaces:**
- Consumes: os 4 workflow ids (3 sub-workflows das Tasks 1–3 + servidor da Task 4).
- Produces: MCP Server "SysLife - Confirmação de Agenda" respondendo via protocolo MCP.
- [ ] **Step 1: Ativar cada um dos 4 workflows**
Para cada um dos 4 ids (3 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 4 ids —
confirmar `"active": true` em todos.
---
### Task 6: Workflow proativo "Lembrete de Confirmação - SysLife (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 - SysLife (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 - SysLife (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": "listar",
"name": "Listar agendamentos de amanhã",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [224, 0],
"parameters": {
"workflowId": { "value": "ID_LISTAR_AGENDAMENTOS", "mode": "id" },
"workflowInputs": {
"mappingMode": "defineBelow",
"value": { "quantidadeDias": 1 }
}
}
},
{
"id": "extrair-lista",
"name": "Extrair lista de agendamentos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [448, 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 agendamentos = Array.isArray(result.data) ? result.data : [];\nreturn agendamentos.map(a => ({ json: a }));"
}
},
{
"id": "montar-mensagem",
"name": "Montar mensagem de lembrete",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [672, 0],
"parameters": {
"assignments": {
"assignments": [
{ "name": "numeroWhatsapp", "type": "string", "value": "={{ $json.nuCelularDdd }}{{ $json.nuCelular }}" },
{ "name": "textoLembrete", "type": "string", "value": "=Olá, {{ $json.nmPaciente }}! Passando para confirmar sua consulta{{ $json.txWhatsappProcedimento ? ' de ' + $json.txWhatsappProcedimento : ($json.nmProcedimento ? ' de ' + $json.nmProcedimento : '') }}{{ $json.txWhatsappMedico ? ' com ' + $json.txWhatsappMedico : ($json.nmMedico ? ' com ' + $json.nmMedico : '') }}{{ $json.txWhatsappHorario ? ' em ' + $json.txWhatsappHorario : ' em ' + $json.dtAtendimento + ' às ' + $json.hrAtendimentoAlterado }}. Você confirma presença? Responda SIM ou NÃO." },
{ "name": "externalKey", "type": "string", "value": "=lembrete-syslife-{{ $json.coAgenda }}" }
]
},
"includeOtherFields": true
}
},
{
"id": "enviar",
"name": "Enviar lembrete via Mensageria ZPRO",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [896, 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": "Listar agendamentos de amanhã", "type": "main", "index": 0 }]] },
"Listar agendamentos de amanhã": { "main": [[{ "node": "Extrair lista de agendamentos", "type": "main", "index": 0 }]] },
"Extrair lista de agendamentos": { "main": [[{ "node": "Montar mensagem de lembrete", "type": "main", "index": 0 }]] },
"Montar mensagem de lembrete": { "main": [[{ "node": "Enviar lembrete via Mensageria ZPRO", "type": "main", "index": 0 }]] }
}
}
```
Nota: `quantidadeDias: 1` é passado como literal fixo (não expressão) —
diferente do Klingo/ClinicAll, o SysLife já calcula "amanhã" no próprio
servidor a partir desse número, então não precisa de um node
`Calcular data` separado. Telefone montado direto de
`nuCelularDdd`+`nuCelular` do próprio item (sem objeto aninhado,
diferente do ClinicAll). O texto usa os campos já formatados pro
WhatsApp (`txWhatsappProcedimento`/`txWhatsappMedico`/
`txWhatsappHorario`) quando presentes, com fallback pros campos brutos
(`nmProcedimento`/`nmMedico`/`dtAtendimento`+`hrAtendimentoAlterado`).
- [ ] **Step 2: Validar**
`n8n_validate_workflow` com o id retornado. Esperado: `errorCount: 0`.
- [ ] **Step 3: Deixar inativo**
Mesma razão do Klingo e do ClinicAll — sem clínica SysLife 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 (a credencial Basic Auth já existe; só falta o `baseUrl`).
---
### Task 7: Workflow de exemplo "AI Agent — Confirmação SysLife (exemplo)"
**Interfaces:**
- Consumes: workflow servidor `MCP Server - SysLife - Confirmação de Agenda` (Task 4, id do servidor).
- Produces: workflow `Confirmação de Agenda - GChat - SysLife+Z-PRO (exemplo)`.
- [ ] **Step 1: Criar o workflow de exemplo**
`n8n_create_workflow` — mesmo contrato de webhook Z-PRO/GChat já usado
nos exemplos do Klingo e do ClinicAll (`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 - SysLife+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-syslife-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 SysLife. 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, use o telefone do paciente (o telefone do contato do WhatsApp já está disponível — {{ $json.telefone }} — mas confirme com o paciente se não tiver certeza) e chame a tool buscar_agendamento_por_telefone para achar o codigoAgenda pendente; 3) chame confirmar_presenca com esse codigoAgenda e resposta='1' (se o paciente confirmou) ou resposta='2' (se recusou) — nenhum outro valor de resposta é aceito; 4) responda ao paciente confirmando que o registro foi feito. Se a mensagem não for uma resposta de confirmação/recusa clara, peça esclarecimento. Nunca invente um codigoAgenda — sempre busque 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 8: Conectar as tools do MCP SysLife ao AI Agent de exemplo
**Interfaces:**
- Consumes: workflow servidor `MCP Server - SysLife - Confirmação de Agenda` (Task 4, id do servidor) e workflow `Confirmação de Agenda - GChat - SysLife+Z-PRO (exemplo)` (Task 7).
- Produces: `AI Agent` do workflow de exemplo com acesso às tools `buscar_agendamento_por_telefone` e `confirmar_presenca` via `MCP Client Tool`.
- [ ] **Step 1: Adicionar o node `MCP Client Tool` conectado ao AI Agent**
`n8n_update_partial_workflow` no workflow da Task 7, numa única chamada
atômica:
```json
{
"id": "<id do workflow da Task 7>",
"operations": [
{
"type": "addNode",
"node": {
"id": "mcp-client-syslife-confirmacao",
"name": "MCP SysLife Confirmação",
"type": "@n8n/n8n-nodes-langchain.mcpClientTool",
"typeVersion": 1,
"position": [448, 320],
"parameters": {
"endpointUrl": "https://devn8n.gtigrupo.com.br/mcp/mcp-server-syslife-confirmacao",
"authentication": "none",
"include": "selected",
"includeTools": ["buscar_agendamento_por_telefone", "confirmar_presenca"]
}
}
},
{ "type": "addConnection", "source": "MCP SysLife Confirmação", "target": "AI Agent", "sourceOutput": "ai_tool", "targetIndex": 0 }
]
}
```
- [ ] **Step 2: Validar**
`n8n_validate_workflow` no workflow da Task 7. Esperado: `errorCount: 0`.
- [ ] **Step 3: Deixar inativo**
Mesma razão das outras tasks — sem credencial Postgres real nem
clínica SysLife real, ativar não teria efeito útil. Fica pronto pra
ativar quando houver cliente real.
---
### Task 9: Atualizar o runbook e commit
**Interfaces:**
- Consumes: todos os ids de workflow criados nas Tasks 1–8.
- [ ] **Step 1: Adicionar seção 20 ao runbook**
Adicionar em `docs/n8n-agendamento-runbook.md`, após a seção 19, uma
seção "20. Confirmação de Agenda via WhatsApp (SysLife)" cobrindo: as
3 tools do MCP Server e seus endpoints (com destaque pro fato de
`GET /AgendaWSApp` servir três propósitos via parâmetro exclusivo), os
ids de todos os 7 workflows criados (3 sub-workflows + servidor +
lembrete proativo + AI Agent de exemplo), a nota de que a credencial
Basic Auth já existe e foi reaproveitada do MVP de agendamento (não
precisou criar nada novo), a simplicidade do enum `Resposta` (`"1"`/`"2"`,
sem terceiro estado como no Klingo e sem strings longas como no
ClinicAll), e o status "estrutura pronta, sem teste de ponta a ponta —
falta só o `baseUrl` real de uma clínica". Seguir o mesmo estilo de
prosa das seções 14, 17, 18 e 19.
- [ ] **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 SysLife 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 4, Step 2). Os workflows de
lembrete proativo (Task 6) e de AI Agent de exemplo (Tasks 7–8) ficam
**intencionalmente inativos** ao final deste plano — sem clínica
SysLife real (mesmo com a credencial Basic Auth já pronta, falta o
`baseUrl` real) e sem credencial Postgres válida, ativá-los só geraria
execuções com erro. Diferente do Klingo e do ClinicAll, este é o único
dos três providers de confirmação onde a autenticação **já está
resolvida** (credencial `httpBasicAuth` `u8XXJ6wyrKuGJp4Y` reaproveitada
do MVP de agendamento) — só falta o endereço da clínica real pra testar
de ponta a ponta.