@lionchat/mcp-server 1.10.1 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,6 +90,25 @@ daquela caixa, mas fica FORA da distribuição automática (round-robin nunca at
90
90
  caixas das quais participa — exceção é estar atribuído a um card, que libera só aquela conversa (ver
91
91
  `lionchat://docs/kanban-deep-dive`).
92
92
 
93
+ ### Supervisor de time (TeamMember.auto_assignable) — novo 2026-07-30
94
+
95
+ O TIME passou a ter os MESMOS dois papéis da caixa. Supervisor de time é membro pra TUDO — menção ao
96
+ time, e-mail de automação, acesso ao funil do Kanban, relatório, contagem e atribuição MANUAL — mas
97
+ fica FORA da distribuição automática do time (nem conversa, nem rodízio de card do funil).
98
+
99
+ - Flag: `team_members.auto_assignable` (`false` = supervisor, `true` = agente). A coluna nasce `true`:
100
+ todo vínculo anterior a 30/07 é Agente.
101
+ - Tools: `lionchat_team_members_create` / `_update`. **O contrato é DIFERENTE do da caixa, de
102
+ propósito:** aqui `user_ids[]` é a lista COMPLETA de membros e `supervisor_ids[]` é um SUBCONJUNTO
103
+ dela. Chave `supervisor_ids` **ausente = não mexe em papel nenhum** (só entra/sai membro) — assim um
104
+ cliente antigo que só manda `user_ids` não zera em silêncio os supervisores marcados na tela.
105
+ - `lionchat_team_members_list` traz `is_supervisor` por agente.
106
+
107
+ **As duas marcas são INDEPENDENTES (decisão do dono 30/07):** a da caixa protege da distribuição da
108
+ CAIXA; a do time protege da distribuição do TIME. Supervisor de caixa que é agente de um time
109
+ CONTINUA recebendo conversa por aquele time — é intencional, não é defeito. Nunca diga ao cliente que
110
+ marcar supervisor num lugar protege no outro.
111
+
93
112
  ## 3. Convenções da API
94
113
 
95
114
  ### Datas
@@ -302,6 +321,7 @@ e pular evita que ela receba tudo em dobro. Quem está sendo atendido pela equip
302
321
  | Rate limit leitura | 1200/min por token |
303
322
  | Rate limit escrita | 600/min por token |
304
323
  | Loop detection | bloqueia chamada idêntica >10x em 10s |
324
+ | Variável de Conta — tamanho do valor | 32 KB por variável (novo 01/08/2026; passou disso = HTTP 422 na conta). Vale só pra variável DECLARADA na tela; chave interna do sistema não é barrada. É por variável, não pela soma — conta com dezenas de tokens legítimos não é punida |
305
325
 
306
326
  Exceção ao teto de 300: em ação em massa de **contatos** com `select_all: true` (agir sobre todos os
307
327
  contatos do filtro) não há teto de IDs — o servidor refaz o filtro e processa em lotes no fundo. O
@@ -352,6 +372,12 @@ Antes de escolher a ferramenta, cheque se a intenção bate com a coluna da dire
352
372
  | **Importar histórico** de caixa WhatsApp Cloud (QR) | `lionchat_inboxes_whatsapp_history_start` / `_status` / `_cancel` | Admin-only + flag `qr_history_import`; só caixa oficial. Acompanhe `state` no `_status` |
353
373
  | Ver / limpar **falhas de envio** de uma caixa | `lionchat_inboxes_failed_messages_summary` primeiro; depois `_bulk_retry` (reenviar) ou `_bulk_cancel` (descartar) | Admin-only. Reenvio em massa e cancelamento em massa são ações irreversíveis: mostre as contagens do summary e confirme (seção 7a/7b). Numa mensagem só: `lionchat_conversations_messages_create_1` (reenviar) / `lionchat_conversations_messages_cancel_retry` (cancelar) |
354
374
  | **Definir supervisor** de caixa | `lionchat_inbox_members_create` / `_update` com `supervisor_ids[]` | Supervisor vê TUDO da caixa mas fica fora do rodízio (ver abaixo) |
375
+ | **Definir supervisor** de time | `lionchat_team_members_create` / `_update` com `supervisor_ids[]` | Contrato diferente do da caixa: `user_ids[]` é a lista COMPLETA e `supervisor_ids[]` um subconjunto dela. Omitir `supervisor_ids` não mexe em papel nenhum (seção 2) |
376
+ | **Mudar a fila do time** (teto por atendente, janela, online/offline) | `lionchat_teams_update` (`assignment_mode`, `fair_distribution_limit`, `fair_distribution_window`) | Time tem PREFERÊNCIA sobre a caixa na distribuição. Leia a config atual antes de mudar (ver `lionchat://docs/conversation-flows`) |
377
+ | **Criar/atualizar time** | `lionchat_teams_create` / `_update` | **Preencha `description`**: desde 03/08 é ela que o AI Agente lê pra decidir o roteamento por time. Descreva QUANDO mandar pra lá, não o que o time é |
378
+ | **Pausar a automação no meio** (esperar atribuição/card antes da próxima ação) | ação `wait` em `lionchat_automation_rules_create` / `_update` | Segundos, teto 300. Acima disso é caso de FlowBuilder. Ver `lionchat://docs/conversation-flows` |
379
+ | **Buscar contato/conversa/card** | `lionchat_search_*`, `lionchat_contacts_search` | **Termo com letra exige 3+ caracteres** (dígito é isento). Menos que isso volta VAZIO sem erro — não conclua que o registro não existe. Ver `lionchat://docs/api-conventions` |
380
+ | **Migrar conversas** de uma caixa WhatsApp pra outra (ex.: QR Code → Oficial) | `lionchat_inboxes_inbox_migration_list` (prévia) → `_execute` → acompanhar status | Admin-only e IRREVERSÍVEL: sempre mostre os números da prévia e confirme (seção 7a). Grupos não vão pro canal oficial — ficam na caixa de origem |
355
381
  | Qualquer coisa de Ligação WP (Wavoip) | **NÃO HÁ TOOL** — removidas em 2026-07-16 por decisão do dono | Ligação WP é contratada e configurada **manualmente pelo cliente no painel**. O MCP não conecta, não desconecta, não entrega token de discagem nem muda config. Oriente o usuário a fazer pelo painel (Configurações da caixa → Ligação WP). |
356
382
  | Restringir agendas que a IA oferece | `lionchat_captain_assistants_update` (`config.booking_event_type_ids: [Int]`) | binding de agenda por cenário NÃO é settable via API |
357
383
  | Configurar o Copiloto (modelo/temperatura/prompt base) | `lionchat_copilot_settings_update` | admin-only; copiloto tem motor próprio (padrão temp 0.3) |
@@ -379,6 +379,29 @@ falha silenciosa, sem erro na tela. Conversa se altera por `fields` (+ `labels:
379
379
  **Multi-termo com E:** `q` aceita String (espaços = AND implícito) ou Array (`q[]=joao&q[]=silva`),
380
380
  cap de 5 termos — cada termo precisa bater (cada um mantém seu OR entre campos).
381
381
 
382
+ **PISO DE 3 LETRAS (novo 2026-08-04) — leia antes de concluir "não existe".**
383
+
384
+ Termo com LETRA precisa de **3 caracteres ou mais**. Termo **só de dígitos** é isento (é assim
385
+ que se busca pelo número da conversa ou do card, e conta nova tem todos com 1-2 dígitos).
386
+
387
+ | Busca | O que acontece |
388
+ |---|---|
389
+ | `q=Bo` | Devolve **vazio**. Não é erro, não é 422 — vem `payload: []` |
390
+ | `q=Bor` | Busca normal |
391
+ | `q=42` | Busca normal (só dígitos, isento) |
392
+ | `q=jo silva` | Vale: basta **um** termo utilizável na frase |
393
+ | `q=zq` + `contact_filters=[...]` | Vazio. O termo curto **invalida a busca inteira**, mesmo com filtro junto |
394
+
395
+ Vale nas CINCO buscas (contato, conversa, mensagem, card e artigo).
396
+
397
+ **Consequência prática:** resultado vazio com termo de 1-2 letras **não significa que o registro
398
+ não existe** — significa que a busca não rodou. Nunca responda "não encontrei esse contato" nessa
399
+ situação. Ou complete o termo, ou pergunte o nome inteiro, ou use `/contacts/filter` com
400
+ `filter_operator: "contains"`, que não tem esse piso.
401
+
402
+ Sem `q` e só com filtros estruturados continua funcionando normalmente (contrato antigo, busca só
403
+ por condições) — o piso só descarta termo de texto curto, não o pedido inteiro.
404
+
382
405
  **Busca cadastral:** CPF/CNPJ/RG/endereço/profissão entram na busca de contatos e conversas, com
383
406
  match por dígitos que ignora pontuação (`31.104.475/0001-51` == `31104475000151`; mín 3 dígitos).
384
407
  Telefone idem. Cards do Kanban são achados pelo contato/conversa vinculados.
@@ -131,6 +131,32 @@ Pra reativar: `paused=false`. Toggle é auditado (`audited only: [:paused]`).
131
131
 
132
132
  O `config` DEVE ser enviado como **objeto** (ex.: `config: { disabled_tools: [...] }`), NUNCA como string/texto. Se vier em formato errado, o backend retorna **422** (não salva ignorando em silêncio). O update do `config` é **merge parcial** — mandar só o subset que muda (ex.: só `disabled_tools`) preserva os demais campos.
133
133
 
134
+ ## AI Agente roteando por TIME (novo 2026-08-03)
135
+
136
+ O AI Agente ganhou 3 ferramentas de time: **Listar Times**, **Mover para Time** e **Tirar do Time**.
137
+ Antes ele só conseguia atribuir uma PESSOA; trocar de time exigia automação ou bloco de fluxo, com
138
+ o time escolhido de antemão por quem montou a regra.
139
+
140
+ **A descrição do time virou peça funcional.** É lendo `teams.description` que a IA decide para onde
141
+ mandar a conversa. Time sem descrição obriga a chumbar o nome no cenário — que é justamente o que
142
+ essas ferramentas vieram resolver.
143
+
144
+ > **Ao criar ou atualizar time (`lionchat_teams_create` / `_update`), preencha `description`
145
+ > dizendo QUANDO mandar para lá.** Ex.: `"Financeiro — cobrança, boleto, nota fiscal, reembolso,
146
+ > segunda via"`. Não é enfeite de tela: é o que a IA lê.
147
+
148
+ Travas que valem conhecer (não dá pra desligar):
149
+
150
+ | Trava | Efeito |
151
+ |---|---|
152
+ | Conta sem nenhum time | As 3 ferramentas nem aparecem para o agente |
153
+ | Alguém está atendendo (`assignee_id` preenchido) | Mover e Tirar **não agem**. Evita a IA arrancar a conversa da mão do atendente |
154
+ | Nome do time no cenário | Resolve por nome exato; não achando, tenta CONTÉM. Se der mais de um candidato, devolve a lista e **não adivinha** |
155
+ | Mover de time | **Não** faz handoff sozinho. Se a intenção é o humano assumir, o cenário precisa mandar chamar o handoff no mesmo turno |
156
+
157
+ `list_teams` é ferramenta de descoberta: fica escondida da tela de ferramentas e só carrega se
158
+ "Mover para Time" estiver ativa. Não tente ligá-la ou desligá-la sozinha em `config.disabled_tools`.
159
+
134
160
  ## Limite do prompt e cache OpenAI (2026-05-22)
135
161
 
136
162
  - `config.instructions` (system prompt do agente) aceita até **20.000 caracteres** (antes 10k). Acima de 15k, o frontend mostra aviso "lost in the middle" — prefira colocar instruções críticas no início ou final.
@@ -247,6 +273,24 @@ pula o follow-up). Tipos:
247
273
  (não empurra pra frente — evita disparo em massa na abertura); a cadência segue nos passos seguintes.
248
274
  `start == end` = janela inválida (ignorada). Ideal pra não incomodar cliente de madrugada.
249
275
 
276
+ ## Follow-up fora da janela de 24h usa TEMPLATE (novo 2026-07-30)
277
+
278
+ Em caixa **WhatsApp oficial**, passadas 24h da última mensagem do cliente, a Meta recusa qualquer texto
279
+ escrito pela IA. Até 30/07 o follow-up nem perguntava: escrevia, o canal recusava e sobrava um balão
280
+ vermelho com o erro enganoso "Template not found or invalid template name" (392 balões assim num único
281
+ dia numa conta). Agora o gerador de follow-up recebe os modelos aprovados da caixa ANTES de escrever e
282
+ escolhe um deles quando a janela está fechada.
283
+
284
+ O que isso significa na prática, pra explicar ao cliente:
285
+ - **Conta em caixa oficial que quer follow-up depois de 24h PRECISA ter template aprovado na Meta.**
286
+ Sem nenhum template, não há como falar fora da janela — não é limitação da plataforma, é regra da Meta.
287
+ - **Mandar template não reabre a janela.** Só a resposta do cliente reabre. Um follow-up por template
288
+ pode ser respondido — e é a resposta dele que libera texto livre de novo.
289
+ - **Trava anti-repetição:** modelo já enviado naquele ciclo de follow-up sai da lista. Numa conta com um
290
+ único template (normalmente o mesmo da campanha), sem essa trava o cliente receberia minutos depois o
291
+ mesmo texto que acabou de ler.
292
+ - Dentro da janela nada muda: o follow-up continua sendo texto livre, escrito pela IA.
293
+
250
294
  ## Desligar a IA quando um humano assume (2026-07)
251
295
 
252
296
  `config.feature_pause_on_human_reply` (bool, padrão false). Ligado, a IA **se desliga sozinha** na
@@ -379,6 +423,17 @@ o `exclusion` (e o `audience_mode`) vão TOP-LEVEL; ao CRIAR/EDITAR a campanha e
379
423
  `{assignee_id, priority, labels}`: `contact_labels: []` (aplica etiqueta ao CONTATO que recebeu) e
380
424
  `attribute_changes: [{scope:'contact'|'conversation', key, value}]` (grava/sobrescreve atributo de quem recebeu).
381
425
 
426
+ **Público por PLANILHA (novo 2026-07-31):** o painel passou a aceitar CSV/XLS como público do disparo,
427
+ nas três abas de campanha. **Não existe tipo novo de público** — a importação marca os contatos da
428
+ planilha com uma **etiqueta de contato** e/ou um **atributo de contato**, e a campanha usa os tipos que
429
+ já existiam (`ContactLabel` / `ContactAttribute`).
430
+
431
+ Ou seja: **dá pra reproduzir pela API.** Importe os contatos aplicando uma marca própria da lista (ex.:
432
+ etiqueta `lista-black-friday`) e monte o `audience` com `{"type": "ContactLabel", "id": <id>}`. Se o
433
+ cliente pedir "disparar pra essa planilha", esse é o caminho. A marcação em lote é feita em silêncio
434
+ (sem callback nem evento), então importar uma lista grande **não acorda automação nem fluxo** — foi
435
+ desenhada assim de propósito, pra não repetir a tempestade de eventos de 29/07.
436
+
382
437
  SEMPRE rode `campaigns_estimate_audience` com o MESMO audience + audience_mode + exclusion antes de
383
438
  criar a campanha e mostre a contagem ao usuário — estimativa e disparo usam o mesmo motor (não divergem).
384
439
 
@@ -399,6 +454,12 @@ Pra dados fixos que se repetem (slogans, endereços, horários):
399
454
  - Resolve em: mensagens, respostas prontas, campanhas e automações. NÃO resolve nas instruções do AI Agente (base/cenário) — ali só `contact.*` e `conversation.*`.
400
455
  - `secret` nunca aparece em template (sai vazio); só resolve em nós API Request do FlowBuilder.
401
456
  - Atualiza UMA vez, propaga pra todo lugar.
457
+ - **Teto de 32 KB por variável (novo 01/08/2026).** Valor maior que isso é recusado com HTTP 422. É por
458
+ VARIÁVEL, não pela soma — conta com dezenas de tokens legítimos (uma delas tem 23 tokens de
459
+ integração) não é punida. A maior variável legítima medida na plataforma tem 348 bytes, então o teto
460
+ dá ~90x de folga: cabe qualquer token, URL, endereço ou texto de configuração; **não** cabe base de
461
+ dados, JSON gigante nem script. Se o cliente quiser guardar algo desse tamanho, o lugar é um arquivo
462
+ ou uma integração, não uma Variável de Conta.
402
463
 
403
464
  Nunca hard-code esses dados em respostas geradas.
404
465
 
@@ -467,6 +528,20 @@ opcional `variable_mapping` junto.
467
528
  - Só entram no mapping as que devem auto-preencher; as demais `{{N}}` continuam manuais.
468
529
  - O `variable_mapping` é salvo localmente (NÃO vai pra Meta) e usado no momento do envio.
469
530
 
531
+ **Variável sem mapping tem preço (2026-08-04).** Placeholder que ninguém disse como preencher:
532
+
533
+ 1. **Tira o modelo do alcance do AI Agente.** Basta UMA variável sem mapping para o modelo inteiro
534
+ deixar de ser oferecido no follow-up automático fora da janela de 24h.
535
+ 2. **Faz o envio manual falhar** com o erro `#132000` da Meta (número de parâmetros não bate).
536
+
537
+ Vale para os dois formatos de marcador: posicional (`{{1}}`) e nomeado (`{{nome_cliente}}`).
538
+
539
+ > Ao registrar modelo por API, **mapeie todas as variáveis do corpo** — ou preencha na hora do envio
540
+ > via `processed_params`. Modelo com variável órfã é modelo que a IA não usa e que o disparo derruba.
541
+
542
+ Confira o que ficou de fora lendo o modelo com `lionchat_inboxes_whatsapp_templates_list` e
543
+ comparando os `{{...}}` do texto do BODY com as chaves do `variable_mapping`.
544
+
470
545
  Exemplo — `{{1}}` vira o primeiro nome do contato:
471
546
 
472
547
  ```json
@@ -131,6 +131,42 @@ Regra prática:
131
131
  Quem entra no rodízio: só `inbox_members` com `auto_assignable: true` (supervisor de caixa fica de
132
132
  fora) e, se "distribuir para agentes offline" estiver desligado, só quem está online.
133
133
 
134
+ ### A fila do TIME tem preferência sobre a da caixa (novo 2026-07-31)
135
+
136
+ Existe um TERCEIRO nível de distribuição, e ele vem ANTES dos dois de cima. A regra, por conversa:
137
+
138
+ > **conversa está num time que distribui → manda o TIME (quem entra e qual o teto).
139
+ > Senão → cai pra caixa/política, exatamente como antes.**
140
+
141
+ Decisão do dono: **time tem preferência, caixa é reserva.** O time decide QUEM e QUANTO; a
142
+ caixa/política continua decidindo a ORDEM da fila, as exclusões e a capacidade por atendente.
143
+
144
+ Campos do time (tools `lionchat_teams_create` / `_update`):
145
+
146
+ | Campo | O que faz | Padrão |
147
+ |---|---|---|
148
+ | `allow_auto_assign` | liga/desliga a distribuição do time | `true` |
149
+ | `assignment_mode` | `online_only` (só quem está online) ou `include_offline` (todo mundo do time) | `online_only` |
150
+ | `fair_distribution_limit` | teto de conversas por atendente na janela | `10` |
151
+ | `fair_distribution_window` | tamanho da janela, **em segundos** | `600` |
152
+
153
+ Quem entra no bolo do time: membros com `auto_assignable: true` (supervisor de time fica de fora)
154
+ **que também sejam membros da caixa** — quem não é da caixa nunca entra, senão a conversa cairia com
155
+ alguém que nem enxerga aquela caixa.
156
+
157
+ Antes de 31/07 o time sorteava UMA vez, no instante em que a conversa caía nele; não achando ninguém
158
+ disponível, desistia PARA SEMPRE e a conversa ficava aberta sem dono. Agora a mesma varredura que já
159
+ existia para a caixa reprocessa a fila do time. Se o cliente reclamar de "conversas com time e sem
160
+ dono" de antes dessa data, a causa é essa — e já está corrigida.
161
+
162
+ **LEIA o valor do time antes de responder** — a resposta de `lionchat_teams_list` / `_show` traz os
163
+ quatro campos. Como em toda configuração, nunca recite o padrão de fábrica como se fosse o que o
164
+ cliente configurou: cite o número que a API devolveu.
165
+
166
+ > Nota de compatibilidade: `fair_distribution_limit` e `fair_distribution_window` só passaram a sair
167
+ > na resposta em **03/08/2026**. Se vierem ausentes, a instalação é anterior a essa data — nesse caso
168
+ > não afirme o valor, mande conferir em Configurações → Times → editar o time.
169
+
134
170
  **LEIA o valor da conta antes de responder — NUNCA recite um número de cabeça.** Os valores reais
135
171
  vêm na resposta da API:
136
172
  - `has_assignment_policy` (resposta da caixa) diz QUAL dos dois caminhos vale ali. `true` = política
@@ -219,6 +255,33 @@ Conversa resolvida que recebe nova mensagem do cliente:
219
255
  - Default: cria NOVA conversa (mantém histórico)
220
256
  - Se locked: reabre a mesma conversa (`status` volta a 0)
221
257
 
258
+ ### 10. Criar conversa por API — REGRA NOVA (2026-08-04)
259
+
260
+ **Nunca nasce conversa nova se o contato já tem uma ABERTA naquela caixa.** Vale para todo caminho
261
+ iniciado por nós: `lionchat_conversations_create`, campanha, flow, booking, integração.
262
+
263
+ | Situação do contato naquela caixa | O que `conversations_create` devolve |
264
+ |---|---|
265
+ | Tem conversa **aberta / pendente / adiada / silenciada** | A **conversa existente**, com o mesmo `id`. Não cria |
266
+ | Só tem conversa **resolvida** | Cria conversa NOVA (invariante do produto: "fechou = abre nova") |
267
+ | Caixa com `lock_to_single_conversation` LIGADO | **Sempre** a existente, mesmo resolvida — reabre |
268
+ | Caixa de **e-mail** | Sempre cria nova (o assunto da thread vem da conversa; reusar mandaria o e-mail com o assunto velho) |
269
+
270
+ **Como saber se reusou:** compare o `id` devolvido com o que você esperava. Vem HTTP 200 nos dois
271
+ casos, sem aviso. Se precisa garantir conversa nova, feche a anterior antes
272
+ (`conversations_toggle_status` para `resolved`).
273
+
274
+ **O que é DESCARTADO no reuso** (a conversa já existe; o disparo não reescreve o estado dela):
275
+ `status`, `snoozed_until`, `additional_attributes` e `assignee_id`. Se você precisa trocar o
276
+ responsável, chame `conversations_update` depois — não adianta mandar junto na criação.
277
+
278
+ **O que É aproveitado:** `custom_attributes` entram por merge, **sem sobrescrever chave que já tem
279
+ valor**. A conversa mantém a origem com que nasceu e só ganha o que ainda não tinha.
280
+
281
+ **Reuso não dispara `conversation_created`** — automação, gatilho de flow, webhook de saída,
282
+ notificação e a Agenda não rodam. Se o seu fluxo depende de um desses, ele não vai acontecer
283
+ quando houver reuso.
284
+
222
285
  ## Estado relacional
223
286
 
224
287
  ```
@@ -266,6 +329,41 @@ Conversation
266
329
  2. Tem SLA policy ativando state change?
267
330
  3. Veio de external API call?
268
331
 
332
+ ## Pesquisa de satisfação (CSAT) — vale em TODO canal (corrigido 2026-07-31)
333
+
334
+ A pesquisa de satisfação é enviada **dentro da própria conversa** (modo `in_chat`) em **todos os
335
+ canais** — WhatsApp, Telegram, Instagram, SMS, site. O backend sempre funcionou assim; era a TELA que
336
+ só mostrava a configuração quando a caixa era WhatsApp, então de 01/04 a 31/07 quem usava outro canal
337
+ via a aba quase vazia e herdava um texto padrão que não conseguia editar. Hoje a configuração aparece
338
+ igual pra todos.
339
+
340
+ Duas coisas pra não errar ao explicar:
341
+ - **A regra de etiqueta (`survey_rules`) agora é respeitada de verdade.** Ela aparecia na tela e era
342
+ IGNORADA no envio — a pesquisa saía pra todo mundo. Se o cliente configurou "só enviar quando a
343
+ conversa tem a etiqueta X" antes de 31/07 e reclamou que ia pra todos, a causa era essa e já está
344
+ corrigida.
345
+ - **Caixa que nunca teve a aba salva** pode continuar sem `csat_mode` gravado e cair no caminho antigo.
346
+ Se duas caixas iguais se comportarem diferente, mande abrir a aba de Pesquisa de Satisfação e salvar
347
+ uma vez.
348
+
349
+ ## Migrar conversas entre caixas de WhatsApp (2026-08-01)
350
+
351
+ Serve pra quem está saindo do WhatsApp por QR Code e indo pro Oficial (ou trocando de número): leva as
352
+ conversas e os contatos da caixa antiga pra nova. Tools: `lionchat_inboxes_inbox_migration_list`
353
+ (prévia), `_execute` (executa) e o acompanhamento de status. **Admin-only e irreversível** — mostre os
354
+ números da prévia e confirme antes.
355
+
356
+ O que a prévia devolve (e o que ela conta): a contagem é de **CONVERSAS**, dividida em migráveis,
357
+ conflitantes e não-migráveis; contato sem conversa é ignorado, e os baldes fecham com o total.
358
+
359
+ **Regras que mudam o resultado — diga ao cliente ANTES de executar:**
360
+ - **Grupos não vão pro canal Oficial.** A Meta não tem grupos: conversa de grupo (`@g.us`), lista de
361
+ transmissão e canal ficam na caixa de ORIGEM e aparecem contados como pulados. Não é falha.
362
+ - QR Code → Oficial passou a funcionar de verdade em 01/08. Antes a migração falhava — em uma conta
363
+ gravou "0 migradas" por causa de um grupo, e na prática teria falhado em 100% dos contatos, porque o
364
+ formato de identificador do QR Code não estava sendo convertido. Se o cliente tentou migrar antes
365
+ dessa data e não deu certo, mande tentar de novo.
366
+
269
367
  ## Receitas de ação (qual tool usar pra cada coisa)
270
368
 
271
369
  ### ENVIAR MENSAGEM (`conversations_messages_create`)
@@ -563,10 +661,41 @@ Todos os formatos abaixo foram conferidos linha a linha em `app/services/action_
563
661
  | `add_note_to_kanban_item` | `[{"funnel_id": 31, "text": "texto da nota"}]` — a chave e **`text`**, nao `note` |
564
662
  | `set_kanban_item_status` | `[{"funnel_id": 31, "status": "won"}]` — `won`, `lost` ou `open`. Status fora disso e ignorado |
565
663
  | `start_kanban_item_timer` / `stop_kanban_item_timer` | `[{"funnel_id": 31}]` |
664
+ | `wait` | `[30]` — segundos. Ver "Acao Aguardar" abaixo |
566
665
 
567
666
  **Acoes de Kanban:** todas precisam que a conversa ja tenha um card no funil informado. Sem card,
568
667
  a acao e pulada (fica so no log do servidor). Use `create_kanban_item` antes, na mesma regra.
569
668
 
669
+ ### Acao `wait` (Aguardar) — novo 2026-08-04
670
+
671
+ Pausa a regra: as acoes **seguintes** so rodam depois do tempo passar. As anteriores ja rodaram.
672
+
673
+ ```json
674
+ { "action_name": "wait", "action_params": [30] }
675
+ ```
676
+
677
+ | Regra | Detalhe |
678
+ |---|---|
679
+ | Unidade | **segundos** |
680
+ | Teto | **300** (5 minutos). Valor maior e limitado a 300 em silencio, sem erro |
681
+ | Formatos aceitos | `[30]`, `30`, `"30"` e `{"seconds": 30}` — todos funcionam |
682
+ | Tempo invalido | `[]`, `[0]`, `[""]` ou ausente = **nao pausa**, a regra segue direto. Nao da erro |
683
+ | Ultima acao da regra | Nao agenda nada (nao ha o que retomar depois) |
684
+ | Pilula "Automacao X disparou" | So aparece na 1a rodada, nao se repete na volta |
685
+
686
+ **Para que serve.** O caso que originou: a regra mandava a mensagem **antes** da politica de
687
+ atribuicao carimbar o responsavel, entao a variavel com o nome do atendente saia vazia. Meio
688
+ segundo de espera resolve. Use quando a acao seguinte depende de algo que outro processo ainda
689
+ esta gravando (atribuicao, card recem-criado, atributo que outra regra vai escrever).
690
+
691
+ **Precisa de mais de 5 minutos?** O lugar certo e o FlowBuilder, que tem espera em horas e sessao
692
+ propria pra sustentar o estado. A automacao nao foi feita pra isso.
693
+
694
+ **A regra nao "dorme":** ao encontrar a espera ela encerra a rodada e agenda a retomada. Varias
695
+ conversas esperam ao mesmo tempo sem travar o atendimento. Na volta, o sistema confere se a regra
696
+ ainda existe e esta ligada, se a conversa ainda existe e se a caixa nao foi excluida — se algo
697
+ disso mudou, ele simplesmente nao retoma.
698
+
570
699
  Lista autoritativa de nomes validos: `AutomationRule#actions_attributes` no backend. Nome de acao
571
700
  fora dessa lista e recusado com 422 ao salvar — isso a API valida. O que ela **nao** valida e o
572
701
  conteudo do `action_params`. `apply_kanban_checklist_template` existe no motor mas **nao** e uma
@@ -412,14 +412,25 @@ Atributos de sistema no CONTATO (prefixo `eclinica_`, protegidos, usáveis como
412
412
  | `eclinica_idagenda` | text | ID do último agendamento |
413
413
  | `eclinica_data_consulta` | date | Data da consulta, ISO |
414
414
  | `eclinica_hora_consulta` | **time** | Hora da consulta 24h `"HH:MM"` (novo 2026-07-06) |
415
+ | `eclinica_hora_final` | **time** | Hora final da consulta (novo 2026-07-27) |
415
416
  | `eclinica_status_agendamento` | text | agendado / no_show / atendido / desmarcado |
416
- | `eclinica_compromisso` | text | Tipo da consulta (ex: Consulta, Retorno, Exame) |
417
+ | `eclinica_situacao` | text | Situação crua da agenda (age_situacao do webhook) |
418
+ | `eclinica_compromisso` | text | Tipo da consulta — TEXTO LIVRE da recepção (ex: Consulta, Retorno) |
419
+ | `eclinica_agendatipo` | text | Tipo de Agendamento — NOME da lista fixa do painel, resolvido por unidade (novo 2026-07-28). **Filtrar sempre pelo NOME, nunca pelo número: os números colidem entre filiais** |
420
+ | `eclinica_agendatipo_id` | text | Tipo de Agendamento — número cru (não comparável entre filiais) |
421
+ | `eclinica_profissional` | text | NOME do profissional, resolvido por unidade (novo 2026-07-10) |
422
+ | `eclinica_profissional_id` | text | Número do profissional (não comparável entre filiais) |
423
+ | `eclinica_convenio_id` | text | Convênio (número) |
424
+ | `eclinica_ultimo_evento` | text | Tipo do último webhook recebido (ex: falta, agendamento_novo) |
417
425
  | `eclinica_ultimo_pagamento` | text | Valor do último pagamento |
418
426
  | `eclinica_ultimo_pagamento_data` | date | Data do último pagamento/baixa (novo 2026-07-06) |
419
427
  | `eclinica_ultimo_pagamento_descricao` | text | Descrição da baixa (ex: CONSULTA INICIAL) |
420
428
  | `eclinica_laboratorio_data_prevista` | date | Previsão de entrega da medicação (novo 2026-07-23) |
421
429
  | `eclinica_laboratorio_data_moldagem` | date | Data do pedido da medicação |
422
- | `eclinica_laboratorio_data_entrega` | date | Data de entrega da medicação |
430
+ | `eclinica_laboratorio_data_entrega` | date | Data em que a medicação CHEGOU na unidade — é a data que dispara o aviso de retirada do flow |
431
+ | `eclinica_laboratorio_data_retirada` | date | Data em que o paciente RETIROU a medicação (coa_datainstalacao; novo 2026-08-03). NÃO confundir com a entrega. Vazia = peça atual não retirada |
432
+ | `eclinica_laboratorio_trabalho_id` | text | Trabalho/tipo da medicação — número cru (aparelho_id; novo 2026-08-03). Lista própria de cada unidade (COMPRIMIDO/POMADA/SPRAY...); número não comparável entre filiais |
433
+ | `eclinica_laboratorio_trabalho` | text | Trabalho/tipo da medicação — NOME resolvido na lista da própria unidade ("SPRAY", "POMADA"...; novo 2026-08-03). Best-effort: pode ficar vazio se a API da unidade estiver fora do ar. Para filtrar/personalizar flows, use SEMPRE este NOME, nunca o número |
423
434
  | `eclinica_laboratorio_situacao` | text | Situação do pedido no laboratório |
424
435
  | `eclinica_ultima_cobranca_valor` | text | Valor da última cobrança/carnê gerado |
425
436
  | `eclinica_ultima_cobranca_vencimento` | date | Vencimento da última cobrança |
@@ -29,13 +29,44 @@ E/OU é encadeado condição a condição (sem parênteses).
29
29
  | `display_id` | equal_to, not_equal_to, contains, does_not_contain (o `contains`, busca por trecho do número, passou a funcionar de verdade em 2026-07-25 — antes dava erro de banco) |
30
30
  | `referer`, `mail_subject` | equal_to, not_equal_to, contains, does_not_contain |
31
31
  | `browser_language`, `conversation_language` | equal_to, not_equal_to |
32
- | `created_at`, `last_activity_at` (valor = data) | is_greater_than, is_less_than, days_before |
32
+ | `created_at`, `last_activity_at` (valor = data) | is_greater_than, is_less_than, days_before + relativos `today`, `yesterday`, `last_days`, `days_ago` (novo 01/08 — ver abaixo) |
33
33
  | `card_funnel_stage` (conversa POR CARD do Kanban — valor `funnel:<id>` ou `stage:<id>:<chave_etapa>`) | equal_to, not_equal_to |
34
34
  | **qualquer outra chave** = atributo personalizado | conforme o tipo do atributo (ver tabela em api-conventions) |
35
35
 
36
36
  Atributo personalizado: informe `custom_attribute_type` na condição — `'conversation_attribute'`
37
37
  (atributo da conversa) ou `'contact_attribute'`. Sem ele o escopo pode sair errado.
38
38
 
39
+ ### Data relativa — "Hoje", "Ontem", "últimos X dias" (novo 2026-08-01)
40
+
41
+ Não calcule data no seu lado pra perguntar "quantas conversas entraram hoje". Existem operadores
42
+ próprios, e eles resolvem tudo em **dias INTEIROS no fuso da CONTA** (`Account#timezone`, padrão
43
+ `America/Sao_Paulo`). Valem em `created_at` e `last_activity_at`, tanto em
44
+ `lionchat_conversations_filter` quanto em `lionchat_contacts_filter`.
45
+
46
+ | Operador | Valor | Janela |
47
+ |---|---|---|
48
+ | `today` | **nenhum** | hoje, 00:00 → 23:59 |
49
+ | `yesterday` | **nenhum** | ontem, 00:00 → 23:59 |
50
+ | `last_days` | `values: [N]` | últimos N dias **incluindo hoje** (N=7 → de 6 dias atrás 00:00 até hoje 23:59) |
51
+ | `days_ago` | `values: [N]` | o dia EXATO de N dias atrás (um dia só, não um intervalo) |
52
+ | `days_before` | `values: [N]` | tudo **mais antigo** que N dias atrás (o comportamento que já existia) |
53
+
54
+ ```json
55
+ { "attribute_key": "created_at", "filter_operator": "today", "values": [], "query_operator": null }
56
+ { "attribute_key": "created_at", "filter_operator": "last_days", "values": [7], "query_operator": null }
57
+ ```
58
+
59
+ **Regras que derrubam a chamada se ignoradas:**
60
+ - `today` e `yesterday` NÃO levam valor — o servidor nem espera o campo.
61
+ - Nos outros três, N tem que ser inteiro entre **1 e 998**. Zero, negativo, texto ou vazio = HTTP 422
62
+ (`InvalidValue`). Não existe `last_days: 0` pra dizer "hoje" — pra hoje use `today`.
63
+ - `is_greater_than` / `is_less_than` continuam sendo o caminho da data FIXA (`values: ["2026-08-01"]`).
64
+ Use-os quando o cliente citou um dia específico; use os relativos quando ele disse "hoje", "ontem"
65
+ ou "últimos X dias".
66
+
67
+ Por que mudou: até 01/08 a borda do dia era montada em UTC, o que errava **3 horas** no Brasil —
68
+ conversa criada às 22h aparecia no dia seguinte. Agora a borda é a do fuso da conta.
69
+
39
70
  ## 2. Contatos — `lionchat_contacts_filter`
40
71
 
41
72
  Mesmo shape de condição. 15/página.
@@ -186,6 +186,13 @@ Webhooks embutidos NÃO aparecem na listagem de integrações standalone; exclui
186
186
  - `title` = texto **EXATO** do botão quick-reply aprovado na Meta (o WhatsApp devolve o TÍTULO no clique; o match é por ele). `value` = slug do título (deduplicado se dois títulos colidem).
187
187
  - Handles: `button_<value>` por botão + `no_response` (sempre — texto livre em vez de clique) + `no_reply_timeout` (só com `buttons_timeout > 0`; `buttons_timeout_unit` `minutes|hours|days`; `0` = sem timeout).
188
188
  - Modo síncrono (`ai_tool`) e `dry_run` NÃO pausam/roteiam — o template só é enviado (botões decorativos). Só faz sentido em caixa WhatsApp com template de botões aprovado.
189
+ - **O item guarda uma FOTO do template** (corpo + botões) tirada quando foi configurado. Se o modelo for
190
+ alterado na Meta depois disso, a foto envelhece: até 2026-07-30 a atualização automática ao reabrir o
191
+ bloco comparava só o CORPO, então mudança APENAS de botão passava batido — a tela mostrava os botões
192
+ novos (ela lê o modelo vivo) e o flow continuava roteando pelos ANTIGOS. Caso real: um botão renomeado
193
+ fazia todo mundo cair em "outra resposta". Hoje corpo E botões são comparados. **Se você mudou os
194
+ botões de um template na Meta, avise que é preciso reabrir o bloco e RELIGAR a saída renomeada** — a
195
+ aresta do botão antigo é removida junto.
189
196
 
190
197
  > ⚠️ **SAÍDAS CONDICIONAIS — NUNCA ligue edge nelas sem ativar a condição.** Estas saídas SÓ existem
191
198
  > quando a config abaixo está presente. Ligar edge nelas sem isso cria uma **aresta fantasma** (linha
@@ -269,6 +276,13 @@ No editor visual esses destinos só aparecem quando a validação bate (computed
269
276
 
270
277
  **REGRA:** depois de wait_response com `options` OU `varied_options`, NUNCA coloque node `condition` pra ramificar — ligue os edges direto nos handles `option_X` (em `varied_options`, `X` é o `id` do grupo).
271
278
 
279
+ **Node que NÃO salva também avança (corrigido 2026-08-01):** até essa data, o avanço do `wait_response`
280
+ dependia de ter uma variável configurada — com `saveTo: ""` o node RECEBIA a resposta e reentrava em si
281
+ mesmo, e quem respondia no prazo acabava saindo pelo caminho de `timeout`. Sintoma que o cliente
282
+ relatava: "mando a mensagem e o fluxo trava em aguardando resposta". Hoje o node avança sempre; salvar
283
+ o valor é opcional de novo, como o schema sempre prometeu. Se um flow antigo tiver ganhado uma variável
284
+ só pra destravar, ela pode sair.
285
+
272
286
  ### 2.4 `condition`
273
287
 
274
288
  ```json
@@ -325,7 +339,8 @@ No editor visual esses destinos só aparecem quando a validação bate (computed
325
339
  | `equal_any` / `not_equal_any` / `contains_any` / `not_contains_any` | multi-valor (usa `values` array). `contains_any` = contém ALGUMA; `not_contains_any` = não contém NENHUMA (útil pra "seguir só quando a msg não tem nenhuma das palavras-chave de outros flows") |
326
340
  | `business_hours` / `outside_business_hours` | horário comercial (par: dentro/fora). `business_hours` aceita `start_hour`/`end_hour` (0-23), `days` (array 0=Dom..6=Sab, ausente=todos) e **`timezone`** (IANA, ex.: `America/Sao_Paulo` — default se ausente). **REGRA:** a saída `outside_business_hours` HERDA `start_hour`/`end_hour`/`days`/`timezone` da `business_hours` ANTERIOR no array — pode deixá-los ausentes na "fora" (o backend preenche). O horário é avaliado no `timezone` (não em UTC) |
327
341
  | `can_reply` / `can_reply_closed` | janela 24h aberta/fechada |
328
- | `conversation_has_agent` / `conversation_no_agent` / `conversation_not_agent` | agente atribuído |
342
+ | `conversation_has_agent` / `conversation_no_agent` / `conversation_not_agent` | agente atribuído (HUMANO) |
343
+ | `conversation_has_ai_agent` / `conversation_no_ai_agent` / `conversation_not_ai_agent` | agente de IA atribuído (novo 2026-08-01) — ver abaixo |
329
344
  | `contact_has_label` / `contact_no_label` / `conversation_has_label` / `conversation_no_label` | labels |
330
345
  | `kanban_exists` / `kanban_in_stage` / `kanban_won` / `kanban_lost` | card no funil. `funnel_id` é CHAVE SEPARADA da condição (número, ex.: `"funnel_id": 37`). A etapa vai em `value` (slug puro da etapa, ex.: `"avaliacao_aceita"`) ou em `stage` — NÃO no formato `"37:etapa"`. Ex.: `{ "operator": "kanban_in_stage", "funnel_id": 37, "value": "avaliacao_aceita" }` |
331
346
  | `card_attr_equals` / `card_attr_contains` | atributo do card (`attrSource: 'card'` + `attr_key`; aceita `card_source: 'trigger'` p/ ler o card que iniciou o flow) |
@@ -342,6 +357,22 @@ No editor visual esses destinos só aparecem quando a validação bate (computed
342
357
 
343
358
  `greater_than`/`less_than` valem em atributo número (valor numérico) E temporais `date`/`time`/`datetime` (o backend detecta o formato do valor: instante > dia > minutos > número). `number_range` SÓ em número. Nunca use maior/menor em texto/lista.
344
359
 
360
+ **Agente de IA — `conversation_has_ai_agent` e irmãos (novo 2026-08-01):** o flow sempre soube LIGAR e
361
+ DESLIGAR a IA (ações `assign_captain` / `deactivate_captain`), mas não sabia PERGUNTAR se ela estava
362
+ ligada. Agora sabe. É o espelho exato do trio do agente humano:
363
+
364
+ | Forma | Verdadeiro quando |
365
+ |---|---|
366
+ | `{ "operator": "conversation_has_ai_agent" }` (sem valor) | tem QUALQUER IA atribuída |
367
+ | `{ "operator": "conversation_has_ai_agent", "values": [12, 15] }` | a IA atribuída é uma dessas |
368
+ | `{ "operator": "conversation_has_ai_agent", "value": 12 }` | a IA atribuída é exatamente essa |
369
+ | `{ "operator": "conversation_no_ai_agent" }` | não tem IA nenhuma |
370
+ | `{ "operator": "conversation_not_ai_agent", "values": [12] }` | tem IA, mas não é nenhuma dessas |
371
+
372
+ **Semântica (decisão do dono 01/08): reflete ATRIBUIÇÃO, não "vai responder agora".** Assistente
373
+ pausado no botão de pânico global continua contando como atribuído. Se o cliente perguntar "a IA está
374
+ respondendo?", essa condição NÃO responde isso — ela responde "a IA está ligada nesta conversa?".
375
+
345
376
  **SLA — operador `sla_check` (2026-06):** verifica se a conversa está dentro/fora do prazo de SLA. A regra NÃO usa `field`, só `value` (código fixo):
346
377
 
347
378
  | `value` | Verdadeiro quando |
@@ -813,6 +844,41 @@ continua servindo pra GRAVAR no contato). Teto: payload acima de 256KB não vira
813
844
  disparado por OUTRO gatilho (mensagem, card, data): `{{webhook.*}}` resolve vazio. O autocomplete
814
845
  do editor mostra o grupo WEBHOOK com os campos do último payload recebido.
815
846
 
847
+ **Variáveis do gatilho `{{trigger.*}}` (novo 2026-08-01):** todo flow — não importa o gatilho — passa
848
+ a saber O QUE o disparou. Antes só existia o tipo, e um flow com VÁRIOS gatilhos não tinha como se
849
+ ramificar por qual deles disparou. Agora dá: `{{trigger.type}}` num node `condition`.
850
+
851
+ Sempre presente:
852
+
853
+ | Variável | Conteúdo |
854
+ |---|---|
855
+ | `{{trigger.type}}` | chave do evento (`message_created`, `conversation_created`, `label_added`, `kanban_item_stage_changed`, `webhook_received`, `date_trigger`, `flow_campaign`, `manual`, `pagetrack_visited`…) |
856
+ | `{{trigger.name}}` | rótulo legível do MESMO evento ("Mensagem recebida", "Card mudou de etapa") — use este pra ESCREVER pro cliente, e o `type` pra COMPARAR em condição |
857
+ | `{{trigger.activated_at}}` | quando disparou (ISO 8601) |
858
+ | `{{trigger.kanban_item_id}}` / `{{trigger.funnel_id}}` | só quando veio do Kanban |
859
+
860
+ Presentes conforme o evento (ausente = resolve vazio, sem erro):
861
+
862
+ | Bloco | Campos |
863
+ |---|---|
864
+ | `{{trigger.message.*}}` | `id`, `content` (texto da mensagem, cortado em 2.000 caracteres), `type`, `created_at` |
865
+ | `{{trigger.conversation.*}}` | `id` (o display_id), `status`, `channel` (nome amigável: `Waha`, `Whatsapp`, `Instagram`), `inbox_id`, `inbox_name`, `created_at` |
866
+ | `{{trigger.contact.*}}` | `id`, `name`, `phone` |
867
+ | `{{trigger.ad.*}}` (anúncio/CTWA) | `id`, `name`, `creative_id`, `creative_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `source`, `headline`, `description`, `cta`, `url` |
868
+ | `{{trigger.attribute.*}}` | `name`, `previous_value`, `current_value`, `changed_at` — o valor ANTES e DEPOIS do atributo que disparou (só nos gatilhos de mudança de atributo) |
869
+
870
+ Dois atalhos: `{{trigger}}` devolve o contexto INTEIRO em JSON e `{{trigger.data}}` devolve o mesmo
871
+ sem os metadados — úteis pra jogar tudo dentro de um node `ai` ou `api`. Bloco (Hash/Array) inteiro
872
+ sai em JSON; valor simples sai cru.
873
+
874
+ **NÃO duplique o que já tem variável própria.** `{{contact.name}}`, `{{conversation.status}}`,
875
+ `{{inbox.name}}` e os atributos `ctwa_*` continuam existindo e são a forma canônica. O `{{trigger.*}}`
876
+ serve pra perguntar "o que veio COM o gatilho", não pra ser um segundo nome do mesmo dado.
877
+
878
+ **Variáveis do agente de IA `{{ai_agent.*}}` (novo 2026-08-01):** `{{ai_agent.name}}` e
879
+ `{{ai_agent.id}}` trazem o AI Agente atribuído à conversa (vazio se não houver). Prefixo `ai_agent` de
880
+ propósito — `agent` sozinho é o atendente HUMANO, e confundir os dois seria pior que não ter.
881
+
816
882
  ---
817
883
 
818
884
  ## 2-D. Exit conditions (saída automática do flow)
@@ -168,6 +168,23 @@ do rodízio).
168
168
  aceitam `supervisor_ids[]` além de `user_ids[]` — os IDs em `supervisor_ids` viram supervisores;
169
169
  os de `user_ids` viram membros comuns. A resposta traz `is_supervisor` (boolean) por agente.
170
170
 
171
+ ### Supervisor de TIME (novo 2026-07-30) — é outra marca, independente da de caixa
172
+
173
+ O time ganhou os mesmos dois papéis. Supervisor de time continua membro pra tudo (menção, e-mail de
174
+ automação, acesso a funil do Kanban, relatório, contagem, atribuição MANUAL) e fica fora da
175
+ distribuição automática do time — tanto de conversa quanto do rodízio de card do funil.
176
+
177
+ **Onde fica armazenado:** flag `team_members.auto_assignable` (espelho da de caixa).
178
+
179
+ **Como definir/ler via tools:** `lionchat_team_members_create` / `_update` com `supervisor_ids[]`.
180
+ **Diferença importante do contrato da caixa:** aqui `user_ids[]` é sempre a lista COMPLETA de membros
181
+ e `supervisor_ids[]` é um SUBCONJUNTO dela; omitir `supervisor_ids` não mexe em papel nenhum.
182
+ `lionchat_team_members_list` traz `is_supervisor`.
183
+
184
+ **As duas marcas NÃO se comunicam:** a da caixa protege da distribuição da caixa, a do time da
185
+ distribuição do time. Supervisor de caixa que é agente num time continua recebendo por aquele time —
186
+ comportamento intencional.
187
+
171
188
  ## KanbanItem (Card do CRM)
172
189
 
173
190
  ### Estrutura
@@ -115,7 +115,8 @@ Um card individual dentro de uma etapa.
115
115
  | `funnel_stage` | Chave da etapa (ex: `"novo_lead"`) |
116
116
  | `position` | Ordem dentro da etapa |
117
117
  | `stage_entered_at` | Quando entrou na etapa atual (pra métrica de tempo) |
118
- | `conversation_display_id` | Conversa principal vinculada |
118
+ | `conversation_display_id` | Conversa principal vinculada. **O sistema move este ponteiro sozinho (desde 04/08/2026)**: quando nasce uma conversa nova do mesmo contato naquela caixa, o card passa a apontar pra ela. O card é da PESSOA, não do atendimento. **NÃO trate este valor como estável** — releia antes de usar. Enviar `conversation_display_id` num UPDATE de card é ignorado de propósito (senão o reenvio do formulário regravaria o número antigo por cima). |
119
+ | `origin_conversation_display_id` | Conversa em que o card NASCEU (só preenchida se o vínculo já foi movido). É o que o relatório de origem de lead usa, pra uma venda não mudar de mês nem de anúncio quando o card anda. |
119
120
  | `linked_conversations` | jsonb array `[{display_id: 123}, {display_id: 456}]` — múltiplas conversas |
120
121
  | `item_details` | jsonb (ver abaixo) |
121
122
  | `custom_attributes` | jsonb — campos custom (igual contatos) |
@@ -489,6 +489,25 @@ viraram conversa). É um relatório de LEITURA puro — não cria nem altera nad
489
489
  **Perguntas que esse relatório responde:** "qual plataforma trouxe mais leads?", "qual campanha/
490
490
  conjunto converteu melhor?", "quantos leads únicos vieram esse mês e quantos fecharam?".
491
491
 
492
+ ### Atributos de anúncio (`ctwa_*`) — dois novos e nomes alinhados (2026-08-01)
493
+
494
+ Conversa que veio de anúncio "Clique para WhatsApp" carrega atributos `ctwa_*` (`ctwa_campaign_name`,
495
+ `ctwa_adset_name`, `ctwa_ad_name`, `ctwa_creative_name`, `ctwa_source_url`…). Eles são atributos de
496
+ conversa normais: dá pra filtrar (`lionchat_conversations_filter`), usar em público de campanha e ler
497
+ em flow (`{{trigger.ad.*}}`).
498
+
499
+ Dois deles apareciam no painel **sem cadastro nenhum** e por isso não dava pra filtrar por eles em
500
+ lugar algum. Foram criados em 01/08 e agora funcionam como os demais:
501
+
502
+ | Chave | Rótulo | Tipo |
503
+ |---|---|---|
504
+ | `ctwa_conversion_source` | Plataforma de Origem do Anúncio | texto |
505
+ | `ctwa_conversion_delay_seconds` | Tempo até a Conversão (s) | número |
506
+
507
+ Na mesma data os rótulos de exibição dos `ctwa_*` foram alinhados com os do painel. **As CHAVES não
508
+ mudaram** — flow, automação, campanha e filtro guardam a chave, nunca o rótulo, então nada que já
509
+ existia quebrou. Se o cliente disser que "o nome do campo mudou", é só o rótulo na tela.
510
+
492
511
  ## Quando o usuário pede algo MUITO específico que não existe num endpoint
493
512
 
494
513
  Se a pergunta requer cálculos custom (ex: "conversas por agente que duraram mais de X minutos"):