# Runbook — Workflow de Agendamento no n8n (GChat/Z-PRO + gti-scheduling-mcp)
> Registra problemas reais encontrados ao montar o workflow
> `Agendamento - GChat - Z-PRO (clínica exemplo)` (id `yG2mi6ECBpPv4Hmn`, servidor
> `srvdevn8ngti` / `devn8n.gtigrupo.com.br`), e como foram resolvidos — para não
> repetir o mesmo processo de descoberta ao duplicar este workflow para uma nova
> clínica. Ver também `docs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md`
> (design) e `docs/superpowers/plans/2026-08-10-n8n-agendamento-workflow.md` (plano).
## 1. Node MCP: qual usar e como instalar
- O node **oficial** do n8n (`MCP Client Tool`, `@n8n/n8n-nodes-langchain`) só
conecta em MCP remoto via HTTP Streamable/SSE — **não** roda comando local
(stdio). Para rodar o `gti-scheduling-mcp` como subprocesso local, use o
pacote **comunitário** `n8n-nodes-mcp` (autor `nerding-io`).
- Instalação: **Settings → Community Nodes → Install a community node** →
`n8n-nodes-mcp`.
- Ao conectar como tool de um AI Agent, o **tipo do node no JSON do workflow**
precisa ser `n8n-nodes-mcp.mcpClientTool` (não `n8n-nodes-mcp.mcpClient`) —
é uma variante "Tool" que o n8n gera automaticamente para nodes marcados
como usáveis em AI Agents. Usar `mcpClient` na conexão `ai_tool` falha a
validação com "cannot output ai_tool connections".
- É necessário setar a variável de ambiente do **processo do n8n**
(não é configurável pela UI):
```
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
```
Nesse servidor, o n8n roda via **systemd** (`n8n.service`), com
`EnvironmentFile=/opt/n8n.env`. A variável foi adicionada nesse arquivo,
seguido de `systemctl restart n8n`. (Se o n8n rodasse em Docker, seria em
`environment:` no `docker-compose.yml`.)
## 2. Credencial MCP (STDIO) — dois bugs reais
Tipo de credencial: `mcpClientApi` ("MCP Client (STDIO) API"), campos:
`command`, `args`, `environments`.
- **Bug 1 — `command: "node"` sozinho falhou** com "Connection closed". O
serviço systemd tem um `PATH` mais restrito que um shell interativo. Fix:
usar o **caminho absoluto** (`which node` no servidor → `/usr/bin/node`).
- **Bug 2 — o campo "Environments" é um `` de uma linha só (mascarado
como senha) na UI do n8n.** O código real do node
(`nodes/McpClient/McpClient.node.ts` no pacote `n8n-nodes-mcp`) espera o
formato `NOME=VALOR` **separado por quebra de linha real** (`split('\n')`),
mas navegadores removem quebras de linha ao digitar OU colar em campos de
uma linha — então múltiplas variáveis nunca conseguem ser inseridas
corretamente pela UI (tentamos: uma por linha, vírgula, espaço — todas
colapsam a mesma forma, e a primeira variável acaba recebendo o resto do
texto como valor, quebrando a validação).
**Fix**: criar/recriar a credencial via **API REST do n8n**, que aceita o
JSON com `\n` de verdade (não passa pelo campo quebrado da UI):
```bash
# 1. Gerar uma API key: Settings → n8n API → Create an API key
# 2. DELETE na credencial antiga (se existir), depois POST na nova:
curl -X POST "https://devn8n.gtigrupo.com.br/api/v1/credentials" \
-H "X-N8N-API-KEY: " \
-H "Content-Type: application/json" \
-d '{
"name": "Client (STDIO) account",
"type": "mcpClientApi",
"data": {
"command": "/usr/bin/node",
"args": "/opt/gti-scheduling-mcp/dist/index.js",
"environments": "SCHEDULING_PROVIDER=zpro\nZPRO_BASE_URL=https://...\nZPRO_API_TOKEN=...\nZPRO_API_ID=..."
}
}'
```
Sintoma quando esse bug está presente: o log do n8n
(`journalctl -u n8n -f`) mostra exatamente a mensagem de erro do
`gti-scheduling-mcp` (`Falha ao carregar a configuração: SCHEDULING_PROVIDER
deve ser "zpro" ou "klingo".`), seguida de `MCP error -32000: Connection
closed` — o processo sobe e cai na validação de config antes de completar o
handshake stdio.
## 3. Deploy do `gti-scheduling-mcp` no servidor do n8n
O `node dist/index.js` executado pela credencial precisa estar no **mesmo
host** onde o n8n roda (o node comunitário spawna o processo localmente).
Processo usado (sem repositório git configurado ainda neste projeto):
`tar` local (excluindo `node_modules`/`dist`) → `scp` pro servidor → `npm
install && npm run build` lá. Publicado em `/opt/gti-scheduling-mcp`.
## 4. Modelo de IA (OpenAI Chat Model node) — gotchas de configuração
Mesmo usando um provider "compatível com OpenAI" (ex.: Z.ai/GLM), o node
`OpenAI Chat Model` (`@n8n/n8n-nodes-langchain.lmChatOpenAi`, v1.3) tem
comportamento por padrão que quebra com endpoints não-OpenAI:
- **`responsesApiEnabled` é `true` por padrão** nessa versão — usa a
"Responses API" da OpenAI (`/responses`), um endpoint proprietário que
providers OpenAI-compatíveis geralmente não implementam (eles só têm
`/chat/completions`). Sintoma: `404 Not Found` /
`MODEL_NOT_FOUND`. **Fix**: setar `responsesApiEnabled: false`
explicitamente.
- **Campo `model`** é um `resourceLocator` — precisa de `{mode: "id", value:
"nome-do-modelo"}` explícito. Omitir `mode` pode fazer o n8n tentar listar
modelos via uma chamada de API (`GET /models`) que o provider alternativo
pode não suportar do mesmo jeito, gerando erro parecido com o anterior.
- **Base URL da credencial**: o campo correto na credencial `openAiApi` é
`url` (aparece como "Base URL" na UI, geralmente atrás de "Add Option").
## 5. Expressões em sub-nodes de IA (Memory/Tools) dentro do AI Agent
Dentro de nodes conectados via `ai_memory`/`ai_tool`/`ai_languageModel`
(sub-nodes do AI Agent), referenciar um node upstream explicitamente
(`={{ $('Normalizar entrada').item.json.sessionId }}`) falhou com "Key
parameter is empty" — mesmo com o dado presente no item que chegou no AI
Agent. **Fix**: usar `={{ $json.sessionId }}` (referência ao item atual, sem
nome de node) — é o padrão que o próprio n8n usa por padrão nesse campo, e
funciona porque sub-nodes de IA herdam o item do node pai (AI Agent) na
execução.
## 6. Contrato do webhook do Z-PRO/GChat (verificado, não documentado
oficialmente em lugar nenhum)
- Uma execução do workflow **por mensagem** do cliente — o Z-PRO não
enfileira mensagens seguintes se o workflow não responder imediatamente.
- Corpo recebido: `{step, entrada, number, contactName, protocol}` — `step`
deve ser `"process"`; `protocol` é o identificador do **atendimento**
(não do telefone).
- Resposta esperada: **texto puro**, nunca JSON — node `Respond to Webhook`
em modo `text`.
## 7. Z.ai/GLM como modelo temporário — limitação real de tool-calling
Usado como substituto temporário enquanto a chave da OpenAI não chegava.
Dois endpoints testados:
- **`https://api.z.ai/api/coding/paas/v4`** ("GLM Coding Plan"): funciona
bem para chat simples, mas é **restrito a ferramentas oficialmente
suportadas** (Claude Code, Goose, etc. — ver
`https://docs.z.ai/devpack/faq`). Ao anexar uma tool MCP genérica ao AI
Agent, o modelo passou a devolver respostas **vazias silenciosamente**
(sem erro) em qualquer mensagem, mesmo sem chamar a tool. Confirmado
desabilitando a tool: a conversa voltou a funcionar normalmente.
- **`https://api.z.ai/api/paas/v4/`** (endpoint geral, mesma conta): suporta
tool-calling arbitrário segundo a documentação, mas apresentou timeout
consistente nesta conta/plano (possivelmente saldo separado do "Coding
Plan").
- **Conclusão**: Z.ai/GLM não é uma substituição direta e confiável da
OpenAI para um AI Agent com tools neste projeto. Recomendado usar a chave
real da OpenAI (ou outro provider com suporte robusto e testado de
function-calling) assim que disponível — só trocar a credencial e o
`model`/`responsesApiEnabled` de volta, sem tocar no resto do workflow.
## 8. Tratamento de erro adicionado
`Normalizar entrada` e `AI Agent` têm `onError: "continueErrorOutput"`,
ambos roteados para um node `Set` ("Erro - Resposta padrão") com uma
mensagem fixa amigável, que alimenta o `Respond to Webhook`. Isso garante
que o Z-PRO **sempre** recebe alguma resposta, mesmo se a normalização
lançar uma exceção ou o AI Agent falhar (timeout, erro de modelo, etc.) —
testado com um timeout real do endpoint geral do Z.ai, confirmado
funcionando.
## 9. Bug crítico — `toolParameters` do node MCP nunca era preenchido pelo modelo
**Sintoma**: toda chamada de `agendar` falhava com
`MCP error -32602: Input validation error: Invalid arguments for tool
agendar: Required at clinicId, Required at startAt, Required at endAt,
Required at patient.` — mesmo com o prompt do sistema explicando o formato
esperado em detalhe.
**Causa raiz**: o node `n8n-nodes-mcp` (operação `executeTool`) tem um
campo próprio **`toolParameters`** (JSON, obrigatório, valor padrão
`'{}'`) que carrega os argumentos reais enviados à tool. Ele **não é
preenchido automaticamente** só por a tool estar conectada a um AI Agent —
é preciso configurá-lo explicitamente com o helper `$fromAI()` do n8n, que
é o mecanismo real usado para "deixar o modelo decidir este valor em cada
chamada". Sem isso, o campo fica travado no `{}` padrão, e nenhum valor
que o modelo "pensa" que está enviando chega de fato ao MCP.
**Fix**: configurar `toolParameters` assim (mistura valores fixos vindos
do próprio workflow com `$fromAI()` só para o que o modelo precisa mesmo
decidir):
```
={{ JSON.stringify({
clinicId: $('Normalizar entrada').item.json.clinicId,
startAt: $fromAI('startAt', 'Data e hora de início, ISO 8601 completo', 'string'),
endAt: $fromAI('endAt', 'Data e hora de término, ISO 8601 completo', 'string'),
patient: {
name: $fromAI('patientName', 'Nome completo confirmado do paciente', 'string'),
phone: $('Normalizar entrada').item.json.telefone
}
}) }}
```
## 10. Bug crítico — `systemMessage` do AI Agent nunca era avaliado como expressão
**Sintoma**: um agendamento de teste foi criado com o campo de telefone
contendo literalmente o texto `{{ $('Normalizar entrada').item.json.telefone
}}` em vez de um número de telefone real.
**Causa raiz**: o campo `parameters.options.systemMessage` do node AI Agent
só é avaliado como expressão do n8n se o **valor inteiro do campo** começar
com `=`. Sem esse prefixo, tudo — inclusive trechos `{{ ... }}` escritos
"como exemplo" dentro do texto — é enviado ao modelo como **texto literal**,
e o modelo, sem saber que aquilo deveria virar um valor real, às vezes
copia o placeholder ao pé da letra.
**Fix**: sempre prefixar o campo inteiro com `=` quando ele precisar
referenciar dados reais do workflow (`={{ $('Normalizar entrada')... }}`).
**Consequência que isso causou (transparência total)**: dois agendamentos
de teste fictícios ficaram duplicados no Z-PRO (`Consulta - TESTE-N8N
Paciente Final`, mesma data/hora) antes desse bug ser identificado — uma
chamada que o agente reportou como "erro" na verdade teve sucesso
silenciosamente no lado do Z-PRO. Os testes fictícios foram apagados
manualmente depois. Isso reforça por que a disciplina de "uma execução por
vez, conferir visualmente antes de tentar de novo" existe em todos os
testes reais deste projeto — mesmo seguindo ela, um bug de configuração
ainda pode gerar um resultado inesperado, e só a conferência visual pega
isso.
## 11. Bug — modelo não sabe a data de hoje
**Sintoma**: pedido de agendamento para "amanhã" resultou em uma data no
passado (2023) sendo enviada ao Z-PRO.
**Causa raiz**: modelos de linguagem não têm acesso à data/hora real do
momento da conversa — sem essa informação no prompt, o modelo "chuta" uma
data (geralmente relacionada à data de corte do treinamento).
**Fix**: incluir a data/hora atual no `systemMessage` via expressão do
próprio n8n:
```
A data de hoje é {{ $now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd (cccc)') }}
```
## 12. Bug — horário criado 3h adiantado/atrasado (fuso horário)
**Sintoma**: cliente pediu "sexta às 10h", o agendamento foi criado em produção
às 7h — exatamente 3h a menos (o offset de America/Sao_Paulo, UTC-3).
**Causa raiz**: o exemplo de formato dado ao modelo usava sufixo `Z` (UTC)
— ex. `2026-04-20T18:00:00.000Z`. O modelo tratou o horário local que o
cliente pediu (10h) como se já fosse UTC, sem converter, e o Z-PRO (que
aparentemente armazena/exibe em UTC) mostrou 10h UTC como 7h local.
**Fix**: instruir o modelo a **nunca usar `Z`/UTC** e sempre escrever o
mesmo número de hora que o cliente pediu, com offset `-03:00` explícito
(ex.: cliente pediu 10h → `2026-08-14T10:00:00-03:00`). Isso evita
qualquer aritmética de fuso horário pelo modelo — ele só copia o número
que o cliente já disse, sem calcular nada. Ajustado tanto na descrição do
`$fromAI('startAt', ...)`/`$fromAI('endAt', ...)` quanto numa regra
explícita no `systemMessage`.
**Se isso acontecer nas próximas clínicas**: o offset `-03:00` só está
certo para fuso de Brasília. Se uma clínica futura estiver em outro fuso,
ajustar o offset (e o `$now.setZone(...)` do prompt) de acordo.
## 13. Migração — MCP Server nativo no n8n (substitui o node comunitário, só Z-PRO)
**Data:** 11/08/2026. Ver design completo em
`docs/superpowers/specs/2026-08-10-mcp-server-nativo-n8n-zpro-design.md` e
plano em `docs/superpowers/plans/2026-08-10-mcp-server-nativo-n8n-zpro.md`.
**Motivação**: o node comunitário (`n8n-nodes-mcp`, seções 1–2 acima) exigiu
depuração pesada (bug de credencial STDIO, variável de ambiente do processo,
`toolParameters`/`$fromAI` não documentado) e não é editável visualmente —
qualquer manutenção de memória/prompt/tool exige mexer em processo externo.
A alternativa: reconstruir o mesmo "MCP Server" **inteiramente dentro do
n8n**, com nodes **oficiais** (`MCP Server Trigger` + `Call n8n Sub-Workflow
Tool` do lado servidor, `MCP Client Tool` oficial via HTTP Streamable do
lado cliente) — sem processo Node.js externo, sem pacote comunitário.
**Arquitetura nova** (3 workflows adicionais, o workflow de agendamento
existente só trocou o node MCP):
- `MCP Server - ZPRO` (`qjSaKllvneBIZ7mC`) — `MCP Server Trigger`
(`path: mcp-server-zpro-clinica-exemplo`) + 2 `Call n8n Sub-Workflow Tool`
(`agendar`, `consultar_horarios`).
- `MCP Server - ZPRO - agendar` (`5ZEOqTOk4Xex6jUQ`) — porta fiel da lógica
já testada em `gti-scheduling-mcp` (`zpro.mapper.ts`/`zpro.adapter.ts`/
`zpro.errors.ts`) para nodes `Set`/`Code`/`HTTP Request`: monta o corpo,
chama `POST /v2/api/external/{apiId}/appointment/create` com
`neverError: true` + `fullResponse: true` (captura manual de status/body,
nunca deixa o n8n abortar em 4xx/5xx), classifica o resultado (qualquer
2xx é sucesso operacional — mesma proteção contra duplicidade de sempre).
- `MCP Server - ZPRO - consultar_horarios` (`yfKkhSTDZaIYCPqP`) — sempre
retorna `NOT_SUPPORTED` (mesma decisão de produto de sempre, Z-PRO não
expõe esse endpoint).
- No workflow de agendamento (`yG2mi6ECBpPv4Hmn`): saiu o node comunitário
`MCP - gti-scheduling-mcp` (`n8n-nodes-mcp.mcpClientTool`, stdio), entrou
`MCP - ZPRO (nativo n8n)` (`@n8n/n8n-nodes-langchain.mcpClientTool`,
`serverTransport: httpStreamable`, apontando pra URL do `MCP Server
Trigger` acima). Nada mais no workflow mudou (prompt, memória, fallback de
erro).
**Gotcha 1 — todos os workflows envolvidos precisam estar `active: true`**,
inclusive os dois sub-workflows (`agendar`/`consultar_horarios`) chamados
via `Call n8n Sub-Workflow Tool` — não só o servidor MCP e o workflow de
agendamento. O erro sintoma é `"Workflow is not active and cannot be
executed."` no node `MCP Client Tool` do lado do AI Agent.
**Gotcha 2 — diagnóstico via chamada MCP crua (`curl`), sem depender do AI
Agent**: pra isolar se o problema está na cadeia de sub-workflows ou no
transporte/roteamento do AI Agent, dá pra chamar o endpoint MCP diretamente:
```bash
# 1. Handshake — captura o mcp-session-id no header da resposta
curl -s -i "https://devn8n.gtigrupo.com.br/mcp/mcp-server-zpro-clinica-exemplo" -X POST \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'
# 2. tools/call — usa o mcp-session-id do passo 1
curl -s "https://devn8n.gtigrupo.com.br/mcp/mcp-server-zpro-clinica-exemplo" -X POST \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: " \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"agendar","arguments":{"startAt":"...","endAt":"...","patientName":"...","patientPhone":"..."}}}'
```
Isso foi o que revelou que a cadeia de sub-workflows já funcionava (chegava
até o Z-PRO de verdade) enquanto o caminho via AI Agent ainda reportava
"not active" — depois de reativar os 3 workflows envolvidos e aguardar um
pouco, o caminho via AI Agent voltou a funcionar igual ao teste direto
(não ficou claro se foi só o tempo/cache do n8n resolvendo, ou a
reativação — mas o sintoma não voltou a aparecer depois disso).
**Gotcha 3 — causa raiz real do primeiro `403` em produção**: não foi
formato de header nem token expirado — foi **ApiID e token de integrações
diferentes**. A clínica tem mais de uma integração externa cadastrada no
Z-PRO (ex.: uma pro canal de WhatsApp "Canal QR", outra pro agendamento),
cada uma com seu próprio `ApiID` (segmento da URL,
`/v2/api/external/{ApiID}/...`) **e** seu próprio Bearer token — não são
intercambiáveis. O node `Config ZPRO` precisa ter o `apiId` da **mesma**
integração de onde veio o token salvo na credencial `httpHeaderAuth`. Os
sintomas por tipo de descasamento, na ordem em que apareceram:
| Corpo de erro do Z-PRO (403) | Causa |
| --- | --- |
| `{"error": "ERR_SESSION_NOT_AUTH_TOKEN"}` | token nunca usado/nunca válido pra nenhuma integração |
| `{"error": "Invalid token."}` | token de uma integração, `apiId` de outra (ou vice-versa) |
| (sucesso, `200`) | `apiId` e token da **mesma** integração |
Ao duplicar este padrão pra uma nova clínica: sempre confirmar, na tela de
integrações do Z-PRO daquela clínica, que o `ApiID` copiado pra URL e o
Bearer token copiado pra credencial vêm da **mesma linha/integração**.
**Teste real de validação (11/08/2026)**: um agendamento fictício foi
criado de ponta a ponta pelo caminho de produção completo (webhook →
Normalizar entrada → AI Agent → `MCP - ZPRO (nativo n8n)` → `MCP Server -
ZPRO` → sub-workflow `agendar` → Z-PRO), com `startAt` local correto
(`2026-08-12T11:00:00-03:00`, sem bug de timezone), retorno `200` do
Z-PRO, `id` de agendamento real mapeado, e resposta final coerente pro
cliente. Conferido visualmente no Z-PRO e apagado logo em seguida.
**Resultado**: o node comunitário `n8n-nodes-mcp` não é mais necessário
pro Z-PRO (segue instalado no servidor, mas sem uso nesse workflow). As
seções 1–2 deste runbook (instalação do node comunitário, bugs da
credencial STDIO) ficam como **referência histórica** — não se aplicam
mais a este workflow, mas continuam relevantes se algum dia o padrão nativo
não for suficiente e for necessário voltar ao processo externo
(`gti-scheduling-mcp`, monorepo TypeScript pausado).
## Status final (11/08/2026)
Testado de ponta a ponta com sucesso, tanto com a chave real da OpenAI
(`gpt-4o-mini`) quanto com o MCP Server nativo do n8n (seção 13): telefone
correto, data correta, um único agendamento por teste, confirmado
visualmente no Z-PRO. Workflow **ativo em produção** (`yG2mi6ECBpPv4Hmn`),
junto com os 3 workflows do MCP Server nativo (`qjSaKllvneBIZ7mC`,
`5ZEOqTOk4Xex6jUQ`, `yfKkhSTDZaIYCPqP`) — todos precisam permanecer
`active: true`.