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

52 KiB
Raw Blame History

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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"):

{
  "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:

{
  "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
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.