Manifesto LLM — Automações

Regras exatas para LLMs gerarem JSON de automação válido: gatilhos, steps, metatags, topologia flat e anti-padrões.

Versão: 1.1 · Público: modelos de linguagem, copilot, integrações e agentes

Este documento define exatamente o que uma LLM pode e não pode gerar ao criar ou editar automações no Corvex. Qualquer JSON fora destas regras será rejeitado pelo validador ou não executará no motor.

Regras obrigatórias para LLMs (formato flat / API)

Antes de gerar steps[], memorize estas cinco regras — violações geram nós voando no editor ou condições que nunca disparam:

#RegraErrado (comum em LLMs)Certo
1nextId em toda cadeiaSteps sequenciais sem nextIdCada step (exceto end terminal) tem nextId → próximo step
2parentId = id da branch, nunca do step anterior"parentId": "uuid-do-email-anterior""parentId": "pending-check1" (= branches[].id)
3Spine sem parentIddelay/condition do tronco com parentIdTronco: email → delay → condition sem parentId
4Condição de pagamentoorder.status + PENDING/PAIDpayment_status + pending/paid
5Fechar ramos com endúltimo email pendente sem endaction.nextId → end com mesmo parentId da branch

nextId vs parentId: nextId = “próximo na sequência”. parentId = “pertence a esta saída de condição”. Nunca use parentId para encadear steps do tronco.


Índice

  1. Visão geral
  2. Dois formatos JSON
  3. Gatilhos
  4. Limites numéricos
  5. Tipos de step (formato JSON)
  6. Formato Copilot (propose_flow_changes)
  7. Condições
  8. Metatags
  9. Conexões e topologia (formato flat — CRÍTICO)
  10. O que é permitido vs proibido
  11. Validação em camadas
  12. Exemplos completos
  13. Anti-padrões
  14. Checklist pré-envio

1. Visão geral

Uma automação Corvex é:

{
  "name": "string (3–100 chars)",
  "trigger_event": "enum de gatilho",
  "active": true,
  "steps": [ /* 1–50 steps */ ]
}
  • Steps executam sequencialmente (com ramificações via condition e aninhamento).
  • O motor substitui metatags {{chave}} no momento do envio.
  • Automações inativas (active: false) não executam.
  • IDs de steps (id) são gerados automaticamente se omitidos; preserve IDs existentes ao editar.

Documentação complementar:


2. Dois formatos JSON

ContextoQuando usarReferência
JSON para importaçãoLLM gera o arquivo; usuário importa no painelEste manifesto §5
Copilot (editor)Assistente propose_flow_changes dentro do painelEste manifesto §6

O painel valida e salva o fluxo. JSON gerado por LLM para importação deve seguir §5; integrações com o copilot usam §6.


3. Gatilhos

Únicos valores aceitos em trigger_event / update_trigger.triggerEvent:

ValorLabelQuando disparaOrientação para a LLM
payment.confirmedPagamento confirmadoPedido pago com sucessoAgradecimento, resumo, rastreio. Não enviar cobrança.
payment.failedPagamento recusadoFalha na adquirenteOrientar nova tentativa; não tratar como pago.
pix.generatedPIX geradoCódigo PIX criadoRecuperação com delays + condição de pagamento antes de lembretes.
boleto.generatedBoleto geradoBoleto emitidoIdem PIX; use variáveis de boleto.
cart.abandonedCarrinho abandonadoCheckout abandonadoReengajamento; {{cart.items}}, link checkout; sem vars exclusivas PIX.
lead.createdLead criadoLead capturado no checkoutNutrição leve; prefira lead.* e customer.*.
tracking_code_updatedRastreio atualizadoCódigo de rastreio atribuídoTransportadora, prazo, código de rastreio.

Proibido: inventar gatilhos (order.shipped, subscription.renewed, etc.).


4. Limites numéricos

RegraMínimoMáximoPadrão
name3 chars100 chars—
steps (array raiz)150—
delay.minutes110.080 (7 dias)—
retry.maxAttempts1103
retry.delayBetweenAttempts0 min10.080 min0
loop.maxIterations110010
SMS message / text1 char160 chars—
Email subject3 chars——

No copilot, delay usa config.value + config.unit (minutes | hours | days); o backend persiste em minutes.


5. Tipos de step (formato JSON)

Tipos permitidos

delay | condition | action | retry | loop | end

Campos comuns (todos os steps)

{
  "type": "delay",
  "id": "uuid-opcional",
  "parentId": "uuid-opcional",
  "nextId": "uuid-opcional"
}
  • Formato aninhado: if_true[], if_false[], branches[].steps[], onResponse[], onNext[], onOpen[], onClickLink[]
  • Formato flat: parentId + nextId (usado pelo editor visual)
  • nextId (flat): obrigatório em cada step não-terminal — sem ele o editor visual fica com nós desconectados
  • parentId (flat): somente em filhos de branch (branches[].id); não use para encadear o tronco

5.1 delay

{
  "type": "delay",
  "minutes": 30
}
CampoTipoObrigatórioRegras
minutesintegersim1–10.080

Boas práticas: em recuperação PIX/boleto/carrinho, coloque delay antes da condição que verifica pagamento.


5.2 condition

Formato simples (legado — prefira payment_status em condições novas)

{
  "type": "condition",
  "field": "payment_status",
  "operator": "=",
  "value": "paid",
  "if_true": [],
  "if_false": []
}

Formato composto (AND/OR)

{
  "type": "condition",
  "operator": "AND",
  "conditions": [
    { "field": "payment_status", "operator": "=", "value": "paid" },
    { "field": "order.total", "operator": ">", "value": 100 }
  ],
  "if_true": [],
  "if_false": []
}

Formato branches (recomendado — editor)

{
  "type": "condition",
  "branches": [
    {
      "id": "branch-pending",
      "label": "Pendente",
      "operator": "AND",
      "conditions": [
        { "field": "payment_status", "operator": "=", "value": "pending" }
      ],
      "steps": [
        { "type": "action", "name": "send_whatsapp", "data": { "type": "text", "message": "..." } }
      ]
    },
    {
      "id": "branch-paid",
      "label": "Pago",
      "conditions": [
        { "field": "payment_status", "operator": "=", "value": "paid" }
      ],
      "steps": []
    }
  ]
}

Cada branch deve ter conditions com pelo menos 1 item.


5.3 action

{
  "type": "action",
  "name": "send_whatsapp",
  "data": { },
  "onResponse": [],
  "onNext": [],
  "onOpen": [],
  "onClickLink": []
}

Ações permitidas no motor

nameExecuta?Copilot pode propor?
send_whatsapp✅✅
send_email✅✅
send_sms✅✅
webhook❌ não implementado❌
create_coupon❌ não implementado❌

Saídas aninhadas (action)

Campo APIHandle copilotCanalQuando executa
onResponsereplyWhatsApp, SMSCliente responde
onNextnextTodos mensagemApós enviar
onOpenopenEmailEmail aberto
onClickLinkclick_linkEmailLink clicado
—no_replySMSSem resposta (editor)

send_whatsapp — data

Texto (padrão):

{
  "type": "text",
  "message": "Olá {{customer.name}}! Pedido {{order.id}} confirmado.",
  "text": "Olá {{customer.name}}! Pedido {{order.id}} confirmado."
}

Aceita message ou text (alias). type pode ser text ou whatsapp.

Imagem:

{
  "type": "image",
  "image": "https://exemplo.com/img.jpg",
  "message": "Legenda opcional"
}

Áudio / vídeo / documento:

{ "type": "audio", "audio": "https://exemplo.com/audio.mp3" }
{ "type": "video", "video": "https://exemplo.com/video.mp4", "message": "Legenda" }
{ "type": "document", "document": "https://exemplo.com/doc.pdf", "documentType": "application/pdf" }

Link preview:

{
  "type": "link",
  "linkUrl": "https://exemplo.com",
  "title": "Título",
  "linkDescription": "Descrição",
  "image": "https://exemplo.com/preview.jpg"
}

Botões de ação:

{
  "type": "button-actions",
  "message": "Escolha:",
  "footer": "Rodapé",
  "buttonActions": [
    { "id": "1", "type": "CALL", "phone": "5511999999999", "label": "Ligar" },
    { "id": "2", "type": "URL", "url": "https://exemplo.com", "label": "Site" }
  ]
}

Lista de botões:

{
  "type": "button-list",
  "message": "Escolha uma opção:",
  "buttonList": {
    "buttons": [
      { "id": "1", "label": "Opção 1" },
      { "id": "2", "label": "Opção 2" }
    ]
  }
}

OTP:

{ "type": "button-otp", "message": "Seu código:", "code": "123456" }

Validação obrigatória:

  • Texto: message ou text não vazio
  • Mídia: URL do campo correspondente obrigatória
  • Botões: buttonActions ou choices não vazio
  • OTP: code obrigatório

send_email — data

{
  "remetente_nome": "Minha Loja",
  "remetente_email": "[email protected]",
  "subject": "Pedido {{order.id}} confirmado",
  "html": "<p>Olá {{customer.name}}, seu pedido de {{order.total}} foi confirmado.</p>",
  "body": "Texto alternativo em plain text"
}
CampoObrigatórioRegras
remetente_nomesimNão vazio (aceita content.remetente_nome)
remetente_emailsimEmail válido
subjectsimMínimo 3 caracteres
html ou bodysimPelo menos um preenchido

send_sms — data

{
  "message": "{{customer.name}}, seu pedido foi confirmado!"
}
RegraValor
messageObrigatório, máx. 160 caracteres
HTMLProibido

5.4 retry

{
  "type": "retry",
  "maxAttempts": 3,
  "delayBetweenAttempts": 5,
  "steps": [
    {
      "type": "action",
      "name": "send_whatsapp",
      "data": { "type": "text", "message": "Tentativa de envio..." }
    }
  ]
}

5.5 loop

{
  "type": "loop",
  "maxIterations": 5,
  "stopOnError": true,
  "condition": {
    "field": "payment_status",
    "operator": "!=",
    "value": "paid"
  },
  "steps": [
    { "type": "delay", "minutes": 60 },
    { "type": "action", "name": "send_email", "data": { "subject": "Lembrete", "html": "<p>Pendente</p>", "remetente_nome": "Loja", "remetente_email": "[email protected]" } }
  ]
}

5.6 end

{
  "type": "end"
}

Encerra explicitamente o caminho. Recomendado no fim de ramos principais.


6. Formato Copilot (propose_flow_changes)

O copilot do editor usa um schema estruturado para propor alterações no fluxo.

Envelope da proposta

{
  "intent": "create_step",
  "summary": "Resumo técnico curto",
  "userSummary": "O que o usuário verá — sem IDs",
  "confidence": "high",
  "locationLabel": "Ramo principal, após 30 minutos",
  "expectedResult": "Cliente recebe lembrete se ainda pendente",
  "operations": [],
  "affectedNodeIds": [],
  "requiredSafetyChanges": [],
  "optionalSuggestions": [],
  "disambiguationOptions": [],
  "changesPreview": []
}
CampoRegras
confidencehigh | medium | low
confidence=lowoperations: [] + disambiguationOptions preenchido
confidence=high/medium + pedido de alteraçãooperations nunca vazio
affectedNodeIdsTodos os IDs tocados

Intents

explain_flow | update_message | create_step | remove_step | add_condition |
update_condition | reorganize | review_flow | update_trigger | clarify

Operações permitidas

create_step | update_step | add_branch | update_branch | remove_step |
update_trigger | insert_before | insert_after | insert_between |
move_step | move_to_branch | remove_condition | remove_branch |
rename_branch | finish_branch

Payload de step (copilot)

{
  "type": "action",
  "actionType": "send_whatsapp",
  "label": "WhatsApp lembrete PIX",
  "config": {
    "content": {
      "type": "text",
      "text": "Oi {{customer.name}}, seu PIX de {{order.total}} aguarda pagamento."
    }
  }
}
{
  "type": "delay",
  "config": { "value": 30, "unit": "minutes" }
}
{
  "type": "condition",
  "config": {
    "branches": [
      {
        "id": "branch-pending",
        "label": "Pendente",
        "operator": "AND",
        "conditions": [
          { "field": "payment_status", "operator": "=", "value": "pending" }
        ]
      }
    ]
  }
}

create_step — âncoras de posicionamento

{
  "op": "create_step",
  "afterNodeId": "delay-abc",
  "step": { "type": "action", "actionType": "send_whatsapp", "config": { "content": { "text": "..." } } }
}
CenárioParâmetros
Primeira etapa do fluxo principalomitir afterNodeId ou afterNodeId: "trigger-0" só na 1ª
Encadear no fluxo principalafterNodeId = ID real do nó anterior
Início de ramo vazioconditionNodeId + branchId
Meio/fim de ramoconditionNodeId + branchId + afterNodeId
Saída onResponseactionNodeId + actionOutputHandle: "reply"
Saída onNextactionNodeId + actionOutputHandle: "next"
Email abertoactionOutputHandle: "open"
Clique no linkactionOutputHandle: "click_link"

Proibido: inventar IDs (action-1, delay-2). Use apenas IDs do contexto do fluxo.

Tipos de mensagem (copilot content.type)

text | image | audio | video | document | text_image | text_audio |
email | menu | poll | carousel

Ações: somente send_whatsapp, send_email, send_sms.


7. Condições

Operadores aceitos pelo runtime

=  !=  in  not-in  empty  not-empty  >  <  >=  <=

O editor também expõe contains, startsWith, endsWith — prefira os operadores da lista acima para garantir compatibilidade com o backend.

Valores corretos (crítico — evita condição morta)

CampoFormato do valorExemplosRuntime
payment_statusminúsculaspaid, pending, failed✅ Use sempre em condições de pagamento
order.payment_method / payment_methodminúsculaspix, boleto, credit_card✅
order.total, amountnúmerousar >, <, >=, <=✅
order.statusMAIÚSCULAS no catálogo do editorPENDING, PAID, …❌ Não avalia no motor — campo não resolvido em condições

Distinção importante: {{order.status}} em templates (email/WhatsApp) funciona como metatag. Em conditions[] de branches, o motor só resolve payment_status (via query ao pedido). Usar order.status em condição = branch nunca casa → lembretes não disparam ou todos os caminhos falham.

Padrão obrigatório para PIX / boleto / recuperação:

{ "field": "payment_status", "operator": "=", "value": "pending" }
{ "field": "payment_status", "operator": "=", "value": "paid" }

Campos de condição

Use somente chaves de CONDITION_FIELDS (editor) / CONDITIONAL_FIELDS_DOCUMENTATION.md.

Principais categorias:

  • Evento: amount, payment_status, payment_method
  • Pedido: order.status, order.total, order.payment_method, order.pix_code, …
  • Cliente: customer.name, customer.email, customer.state, …
  • Lead: lead.isCompleted, lead.utmSource, lead.deviceType, …
  • Frete: shipping.name, shipping.carrier, …
  • Browser: browser.ip, browser.utms.source, …

Proibido: status, total, paid sem prefixo de namespace.

Padrão de recuperação (PIX / boleto / carrinho)

[trigger] → [mensagem inicial] → [delay] → [condition: ainda pendente?]
                                              ├─ sim → [lembrete]
                                              └─ não → [confirmação ou end]

Antes da 2ª mensagem de cobrança, inclua condição payment_status = pending (não use order.status em conditions[]).


8. Metatags

Formato

{{categoria.campo}}
  • Sem espaços dentro das chaves
  • Case-sensitive: {{customer.name}} ≠ {{Customer.Name}}

Regras

  1. Nunca inventar — use o catálogo GET /automation/metatags?triggerType=...
  2. Cada gatilho expõe um subconjunto de metatags; variável fora do catálogo → erro UNKNOWN_METATAG (modo block)
  3. Metatag ausente no contexto → substituída por ""
  4. Valores monetários → formatados R$ X,XX
  5. Datas → DD/MM/YYYY HH:mm

Metatags frequentes

MetatagUso
{{customer.name}}Nome do cliente
{{customer.phone}}Telefone (WhatsApp/SMS)
{{customer.email}}Email
{{order.id}}ID do pedido
{{order.total}}Valor formatado
{{order.status}}Status do pedido
{{order.pix_code}}Código PIX copia e cola (texto)
{{order.pix_qr_code}}QR Code PIX como <img> HTTPS (somente email, trigger pix.generated)
{{order.domain}}URL do checkout
{{order.items}}Lista de itens
{{cart.items}}Itens do carrinho (carrinho abandonado)
{{shipping.carrier}}Transportadora
{{lead.name}}Nome do lead

QR Code PIX no email ({{order.pix_qr_code}})

Metatag HTML gerada automaticamente a partir de {{order.pix_code}} (EMV copia e cola ou link curto da adquirente).

RegraDetalhe
Canalemail apenas — não use em SMS/WhatsApp
Gatilhopix.generated (com order.pix_code preenchido)
Formato<img src="https://apiv3.usecorvex.com.br/public/pix-qr.png?d=...&s=180" ...>
Por quê HTTPSClientes de email (Gmail, Outlook) bloqueiam data:image/png;base64,...
MJMLEnvolva em <mj-raw>{{order.pix_qr_code}}</mj-raw>
ValorUse {{order.total}} sem prefixar R$ — a metatag já retorna R$ 12,75

Exemplo MJML (PIX gerado):

<mj-raw>
  <div style="text-align:center;padding:16px 0;">
    {{order.pix_qr_code}}
    <p style="font-size:12px;color:#666;margin-top:8px;">Escaneie com o app do seu banco</p>
  </div>
</mj-raw>
<mj-text font-size="12px"><pre style="word-break:break-all;">{{order.pix_code}}</pre></mj-text>
<mj-button href="{{order.url_pix_gerado}}">Abrir PIX e pagar</mj-button>

Combinação recomendada: QR ({{order.pix_qr_code}}) + copia e cola ({{order.pix_code}}) + botão ({{order.url_pix_gerado}}).

Por gatilho — variáveis a evitar

GatilhoNão use
cart.abandoned{{order.pix_code}}, vars exclusivas de pagamento confirmado
payment.confirmedmensagens de cobrança, {{order.pix_code}} como CTA de pagamento
lead.created{{order.total}} se pedido ainda não existe

9. Conexões e topologia (formato flat — CRÍTICO)

Sintoma no editor: etapas "voando" sem linha de conexão, vários end soltos, ramos cruzados.
Causa quase sempre: uso incorreto de parentId, nextId e branches[].id no array plano de steps.

O editor Corvex reconstrói o fluxo visual a partir do array steps[] usando:

  • nextId — encadeia o spine (fluxo principal, steps sem parentId de ramo)
  • parentId — liga o step a uma saída de condição (branches[].id)
  • Ordem do array — steps do mesmo ramo são agrupados e encadeados na ordem em que aparecem

O editor visual reconstrói as conexões a partir de nextId, parentId e da ordem dos steps no array.


9.0 Armadilhas frequentes de LLMs (regressão real)

Caso real: automação PIX com 4 emails gerada por LLM — passou na validação do painel, mas no editor apareceram nós voando e as condições não funcionariam em runtime.

#Erro da LLMSintomaCorreção
1Omitir nextId em todos os stepsEditor sem linhas entre email → delay → conditionEncadear: "nextId": "id-do-proximo-step" em cada step não-terminal
2parentId = id do step anterior (ex.: delay com parentId do email)Nó órfão no editorTronco: sem parentId. Ramo: parentId = branches[].id
3parentId em condition do tronco (ex.: condition com parentId do delay)Condition deslocada / spine quebradotype: "condition" nunca tem parentId
4order.status + PENDING/PAID nas branchesCondição sempre falsa (fieldValue: null)payment_status + pending/paid
5Último ramo pendente sem endNó de email final “solto”email.nextId → end com mesmo parentId da branch

❌ Anti-exemplo (padrão que LLMs inventam — NÃO reproduzir)

[
  { "id": "email-1", "type": "action", "name": "send_email", "data": { "...": "..." } },
  { "id": "delay-1", "type": "delay", "minutes": 5, "parentId": "email-1" },
  { "id": "cond-1", "type": "condition", "parentId": "delay-1", "branches": [
      { "id": "pending-1", "conditions": [{ "field": "order.status", "value": "PENDING", "operator": "=" }] },
      { "id": "paid-1", "conditions": [{ "field": "order.status", "value": "PAID", "operator": "=" }] }
  ]},
  { "id": "email-2", "type": "action", "name": "send_email", "parentId": "pending-1", "data": { "...": "..." } },
  { "id": "delay-2", "type": "delay", "minutes": 5, "parentId": "email-2" }
]

Problemas: sem nextId; parentId usado como encadeamento; condition com parentId; campo de condição errado; delay do ramo pendente aponta para email em vez da branch.

✅ Padrão correto (mesmo funil, trecho)

[
  { "id": "email-1", "type": "action", "name": "send_email", "nextId": "delay-1", "data": { "...": "..." } },
  { "id": "delay-1", "type": "delay", "minutes": 5, "nextId": "cond-1" },
  {
    "id": "cond-1",
    "type": "condition",
    "nextId": "end-paid-1",
    "branches": [
      { "id": "pending-1", "label": "PIX pendente", "conditions": [{ "field": "payment_status", "operator": "=", "value": "pending" }] },
      { "id": "paid-1", "label": "PIX pago", "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }] }
    ]
  },
  { "id": "end-paid-1", "type": "end", "parentId": "paid-1" },
  { "id": "email-2", "type": "action", "name": "send_email", "parentId": "pending-1", "nextId": "delay-2", "data": { "...": "..." } },
  { "id": "delay-2", "type": "delay", "minutes": 5, "parentId": "pending-1", "nextId": "cond-2" },
  {
    "id": "cond-2",
    "type": "condition",
    "nextId": "end-paid-2",
    "branches": [
      { "id": "pending-2", "label": "PIX pendente", "conditions": [{ "field": "payment_status", "operator": "=", "value": "pending" }] },
      { "id": "paid-2", "label": "PIX pago", "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }] }
    ]
  },
  { "id": "end-paid-2", "type": "end", "parentId": "paid-2" },
  { "id": "email-last", "type": "action", "name": "send_email", "parentId": "pending-2", "nextId": "end-pending-2", "data": { "...": "..." } },
  { "id": "end-pending-2", "type": "end", "parentId": "pending-2" }
]

Checklist deste padrão:

  • Spine (email-1, delay-1, cond-1): sem parentId; encadeados com nextId
  • cond-1.nextId → primeiro filho do ramo pago (ajuda o editor a desenhar a saída “pago”)
  • Filhos de pending-1: todos com parentId: "pending-1" (inclusive o delay), não o id do email
  • cond-2 no spine lógico do ramo pendente: sem parentId (conditions nunca têm)
  • Último email pendente → end com mesmo parentId

9.1 Dois mundos: spine vs ramo

MundoQuem entraCampos usados
Spine (principal)Email inicial, delays e conditions antes de ramificarSem parentId; obrigatório nextId entre si
Ramo (branch)Actions, delays e end após uma saída de conditionparentId = branches[].id; nextId só entre steps do mesmo parentId
ConditionNó de decisão (spine ou após delay no ramo pendente)Nunca parentId; pode ter nextId → primeiro filho do ramo pago (para o editor)
SPINE                          RAMO (parentId = "pending-20min")
──────                         ─────────────────────────────────
trigger                        condition ──► [pending-20min] ──► action ──► delay ──► end
  └─ action                         │
  └─ delay                          ├──► [paid-20min] ──► action ──► end
  └─ condition (20min)                │
                                      └──► [failed-20min] ──► action ──► end

Regra de ouro: se o step tem parentId de ramo, ele pertence àquele ramo — não ao spine.


9.2 Regra #1 — NUNCA usar nextId no spine apontando para step com parentId de ramo

❌ PROIBIDO

{
  "type": "condition",
  "id": "cond-20min",
  "branches": [
    { "id": "pending-20min", "label": "Pendente", "conditions": [...] },
    { "id": "paid-20min", "label": "Pago", "conditions": [...] }
  ],
  "nextId": "delay-3h"
},
{
  "type": "delay",
  "minutes": 180,
  "id": "delay-3h",
  "parentId": "paid-20min"
}

Por que quebra: o nextId da condição diz ao editor que o spine continua em delay-3h, mas esse delay está no ramo paid-20min. Resultado: arestas duplicadas/conflitantes e nós visualmente deslocados.

✅ CORRETO

O delay de 3h pertence ao ramo pendente (quem ainda não pagou espera mais):

{
  "type": "condition",
  "id": "cond-20min",
  "branches": [...]
},
{
  "type": "action",
  "name": "send_whatsapp",
  "parentId": "pending-20min",
  "id": "wa-reminder-20",
  "nextId": "delay-3h"
},
{
  "type": "delay",
  "minutes": 180,
  "id": "delay-3h",
  "parentId": "pending-20min"
}

Regra: nextId só pode apontar para:

  1. Step do mesmo spine (ambos sem parentId de ramo), ou
  2. Próximo step no mesmo ramo (mesmo parentId)

9.3 Regra #2 — NUNCA misturar branch.id de condições diferentes

Cada condition define seu próprio namespace de branches[].id. IDs como paid-20min e paid-3h pertencem a condições distintas.

❌ PROIBIDO

{ "type": "condition", "id": "cond-20min", "branches": [{ "id": "paid-20min", ... }] },
{ "type": "delay", "parentId": "paid-20min", "minutes": 180 },
{ "type": "action", "parentId": "paid-3h", ... },
{ "type": "condition", "id": "cond-3h", "branches": [{ "id": "paid-3h", ... }] }

Por que quebra: paid-3h só existe na segunda condição, mas um action no meio do array já referencia esse ID — o editor associa o step à condição errada ou deixa o nó órfão.

✅ CORRETO — prefixar IDs por condição

CondiçãoPrefixo sugeridoExemplos de branch.id
Checagem 20 min20min-20min-pending, 20min-paid, 20min-failed
Checagem 3 h3h-3h-pending, 3h-paid, 3h-failed
{ "parentId": "20min-paid", "type": "action", "name": "send_whatsapp", "data": { "message": "Pagamento confirmado!" } },
{ "parentId": "20min-paid", "type": "end", "id": "end-paid-20" }

Steps da condição 3h usam somente 3h-*:

{ "parentId": "3h-pending", "type": "action", ... }

Regra: o parentId de um step deve ser branches[].id da condição imediatamente acima dele na árvore lógica, nunca de outra condição.


9.4 Regra #3 — Cada ramo termina em end OU encadeia com nextId dentro do MESMO parentId

Todo branches[].id deve ter um caminho completo:

condition → [branch-X] → step₁ → step₂ → … → end
                ou
              step₁ → (nextId) → step₂ → end

❌ PROIBIDO — ramo incompleto

{ "parentId": "20min-pending", "type": "action", "name": "send_whatsapp", "data": { "message": "Lembrete" } }
// sem nextId para próximo step, sem end — ramo morre ou "flutua"

❌ PROIBIDO — end no spine sem ligação ao ramo

{ "type": "end", "id": "end-orfao" }

Steps end sem parentId no meio do array viram nós soltos no editor (o painel tenta ligá-los ao próximo step de topo e falha).

✅ CORRETO — cada saída fecha ou continua

{ "parentId": "20min-paid", "type": "action", "name": "send_whatsapp", "data": { "message": "Obrigado!" } },
{ "parentId": "20min-paid", "type": "end", "id": "end-20min-paid" },

{ "parentId": "20min-failed", "type": "action", "name": "send_whatsapp", "data": { "message": "PIX expirou" } },
{ "parentId": "20min-failed", "type": "end", "id": "end-20min-failed" },

{ "parentId": "20min-pending", "type": "action", "id": "wa-20", "data": { "message": "Lembrete" }, "nextId": "delay-3h" },
{ "parentId": "20min-pending", "type": "delay", "id": "delay-3h", "minutes": 180, "nextId": "cond-3h" },
{ "parentId": "20min-pending", "type": "condition", "id": "cond-3h", "branches": [...] }

Nota: colocar uma condition aninhada com parentId de ramo é válido no formato flat — ela é filha daquele caminho pendente.


9.5 Ordem do array steps[] (importante para o editor)

Ao serializar em formato flat, siga esta ordem:

  1. Spine até a condição (action → delay → condition)
  2. Ramo A — todos os steps com parentId do branch A, na ordem de execução
  3. Ramo B — todos os steps com parentId do branch B
  4. Ramo C — idem
  5. Se um ramo contém sub-condição, os steps da sub-condição e seus sub-ramos vêm logo após os steps do ramo pai, ainda com parentId correto

❌ PROIBIDO — intercalar ramos de condições diferentes

spine action → spine delay → cond-20min → delay(parentId:paid-20min) → action(parentId:paid-3h) → end órfão → action(pending-20min) → cond-3h

Esse padrão (extraído de JSON gerado por LLM) produz nós "voando" no editor.

✅ CORRETO — agrupar por ramo

spine: wa-inicial → delay-20 → cond-20min
ramo 20min-pending: wa-lembrete → delay-3h → cond-3h
  sub-ramo 3h-pending: wa-final → end
  sub-ramo 3h-paid: wa-obrigado → end
  sub-ramo 3h-failed: wa-expirado → end
ramo 20min-paid: wa-confirmado → end
ramo 20min-failed: wa-falhou → end

9.6 O que cada campo faz (referência rápida)

CampoOnde usarNão usar para
nextIdEncadear spine; encadear steps no mesmo parentIdApontar do spine para step com parentId de ramo
parentIdIdentificar ramo (branches[].id)Substituir nextId no spine principal
branches[].idID estável da saída da condiçãoReutilizar em outra condição
idIdentificador único do stepInventar IDs sem consistência com nextId
onNext[] / onResponse[]Sub-fluxos de action (aninhado)Substituir parentId/nextId flat sem necessidade

Campos a NÃO enviar na API: _clientNodeId, caption duplicado em type: text (opcional, não quebra).


9.7 Condição no spine: nextId só para desenhar saída de ramo (editor)

Após uma condition, o motor executa apenas os steps cujo parentId corresponde ao ramo satisfeito. O spine não continua linearmente através dos ramos.

❌ ERRADO — nextId da condição apontando para step do spine (sem parentId)

{
  "type": "condition",
  "id": "cond-1",
  "nextId": "delay-proximo-no-spine"
}

Se delay-proximo-no-spine não tem parentId, o editor desenha aresta extra do nó condição → delay do spine além das saídas dos ramos → visual confuso.

❌ ERRADO — confundir parentId com encadeamento

{ "type": "delay", "id": "delay-1", "parentId": "email-anterior-id" }

parentId não é “step anterior”. Use nextId no email: "nextId": "delay-1".

✅ CORRETO — padrão editor-friendly

  • Spine: action.nextId → delay.nextId → condition (sem parentId)
  • condition.nextId → primeiro filho do ramo pago (end ou action com parentId: "…-paid")
  • Ramo pendente: todos os steps com parentId: "…-pending" e nextId interno
  • Ramo pago: end (ou confirmação) com parentId: "…-paid"
  • Nunca parentId em type: "condition"

9.8 Fluxo completo correto — recuperação PIX (20 min + 3 h)

Estrutura lógica:

WhatsApp inicial
  → delay 20 min
  → condição 20 min
       ├─ pending → lembrete → delay 3h → condição 3h
       │              ├─ pending → último lembrete → end
       │              ├─ paid → obrigado → end
       │              └─ failed → expirado → end
       ├─ paid → confirmação → end
       └─ failed → expirado → end

JSON flat (trecho representativo — mensagens abreviadas):

[
  { "type": "action", "name": "send_whatsapp", "id": "wa-0", "nextId": "delay-20", "data": { "type": "text", "message": "PIX gerado {{order.total}}" } },
  { "type": "delay", "minutes": 20, "id": "delay-20", "nextId": "cond-20" },
  {
    "type": "condition",
    "id": "cond-20",
    "nextId": "end-20-paid",
    "branches": [
      { "id": "20min-pending", "label": "Pendente", "conditions": [{ "field": "payment_status", "operator": "=", "value": "pending" }] },
      { "id": "20min-paid", "label": "Pago", "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }] },
      { "id": "20min-failed", "label": "Falhou", "conditions": [{ "field": "payment_status", "operator": "=", "value": "failed" }] }
    ]
  },
  { "type": "action", "name": "send_whatsapp", "id": "wa-20-pend", "parentId": "20min-pending", "nextId": "delay-3h", "data": { "type": "text", "message": "Lembrete 20 min" } },
  { "type": "delay", "minutes": 180, "id": "delay-3h", "parentId": "20min-pending", "nextId": "cond-3h" },
  {
    "type": "condition",
    "id": "cond-3h",
    "nextId": "end-3h-paid",
    "branches": [
      { "id": "3h-pending", "label": "Pendente", "conditions": [{ "field": "payment_status", "operator": "=", "value": "pending" }] },
      { "id": "3h-paid", "label": "Pago", "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }] },
      { "id": "3h-failed", "label": "Falhou", "conditions": [{ "field": "payment_status", "operator": "=", "value": "failed" }] }
    ]
  },
  { "type": "action", "name": "send_whatsapp", "parentId": "3h-pending", "nextId": "end-3h-pend", "data": { "type": "text", "message": "Último lembrete" } },
  { "type": "end", "id": "end-3h-pend", "parentId": "3h-pending" },
  { "type": "action", "name": "send_whatsapp", "parentId": "3h-paid", "nextId": "end-3h-paid", "data": { "type": "text", "message": "Obrigado!" } },
  { "type": "end", "id": "end-3h-paid", "parentId": "3h-paid" },
  { "type": "action", "name": "send_whatsapp", "parentId": "3h-failed", "nextId": "end-3h-fail", "data": { "type": "text", "message": "PIX expirou" } },
  { "type": "end", "id": "end-3h-fail", "parentId": "3h-failed" },
  { "type": "action", "name": "send_whatsapp", "parentId": "20min-paid", "nextId": "end-20-paid", "data": { "type": "text", "message": "Pagamento confirmado!" } },
  { "type": "end", "id": "end-20-paid", "parentId": "20min-paid" },
  { "type": "action", "name": "send_whatsapp", "parentId": "20min-failed", "nextId": "end-20-fail", "data": { "type": "text", "message": "Falhou em 20 min" } },
  { "type": "end", "id": "end-20-fail", "parentId": "20min-failed" }
]

Checklist deste exemplo:

  • Spine só tem: wa-0 → delay-20 → cond-20 (sem nextId da condição para dentro de ramo)
  • Delay 3h está em 20min-pending, não em 20min-paid
  • IDs 20min-* vs 3h-* separados
  • Todo ramo termina em end com o mesmo parentId
  • nextId só liga steps com o mesmo parentId (ou spine sem parent)

9.9 Alternativa: formato aninhado (mais seguro para LLM)

Se o flat é error-prone, prefira aninhamento — o backend aceita ambos:

{
  "type": "condition",
  "branches": [
    {
      "id": "20min-pending",
      "label": "Pendente",
      "conditions": [{ "field": "payment_status", "operator": "=", "value": "pending" }],
      "steps": [
        { "type": "action", "name": "send_whatsapp", "data": { "type": "text", "message": "Lembrete" } },
        { "type": "delay", "minutes": 180 },
        {
          "type": "condition",
          "branches": [
            {
              "id": "3h-paid",
              "label": "Pago",
              "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }],
              "steps": [
                { "type": "action", "name": "send_whatsapp", "data": { "message": "Obrigado!" } },
                { "type": "end" }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "20min-paid",
      "label": "Pago",
      "conditions": [{ "field": "payment_status", "operator": "=", "value": "paid" }],
      "steps": [
        { "type": "action", "name": "send_whatsapp", "data": { "message": "Confirmado!" } },
        { "type": "end" }
      ]
    }
  ]
}

Vantagem: impossível misturar parentId de condições diferentes — a hierarquia é explícita.


9.10 Handles de action (saídas onResponse / onNext)

reply      → onResponse   (WhatsApp/SMS — cliente respondeu)
next       → onNext       (após enviar, qualquer canal)
open       → onOpen       (email aberto)
click_link → onClickLink  (clique no link do email)
no_reply   → onNoReply    (SMS sem resposta)

Steps em onNext[] são aninhados dentro do action — não precisam de parentId flat.


9.11 Diagnóstico: "nós voando" no editor

Sintoma visualCausa provável no JSON
Nenhuma linha entre steps sequenciaisFalta nextId em toda a cadeia (erro #1 de LLMs)
Delay/condition “grudados” no step erradoparentId = id do step anterior em vez de branches[].id
end isolado à direitaend sem parentId no spine, sem nextId chegando
Delay longe do ramo pendenteparentId apontando para ramo errado (paid em vez de pending)
Condição flutuando após delaycondition com parentId do delay (conditions nunca têm parentId)
Cruzamento de linhasnextId da condição apontando para step do spine sem parentId
Ramo pago com delay de recuperaçãoLógica invertida — delay de cobrança no ramo paid
Múltiplos emails desconectadosSteps de ramos intercalados no array sem nextId/parentId corretos
Lembretes sempre ou nunca disparamorder.status em conditions[] em vez de payment_status

Antes de salvar: para cada step, pergunte: quem é meu pai (parentId ou spine)? quem é meu próximo (nextId)? meu ramo termina em end?


Regras estruturais (resumo)

  1. Todo step deve estar conectado ao fluxo — nenhum órfão
  2. nextId obrigatório em cada step não-terminal do encadeamento
  3. trigger-0 é o nó raiz — nunca remover
  4. Cada branches[].id tem steps ou end
  5. Máximo 50 steps
  6. Spine usa nextId sem parentId
  7. Ramos usam parentId (= branch id) + nextId interno
  8. parentId nunca é id de outro step — só branches[].id
  9. type: "condition" nunca tem parentId
  10. Nunca misturar branch IDs entre condições (use prefixos: check1-paid, check2-pending)
  11. Sempre fechar ramos com end ou encadeamento completo no mesmo parentId
  12. Condições de pagamento: payment_status + pending/paid

10. O que é permitido vs proibido

✅ Permitido

CategoriaItens
Step typesdelay, condition, action, retry, loop, end
Ações (copilot)send_whatsapp, send_email, send_sms
Ações (API schema)acima + webhook, create_coupon (schema aceita, motor não executa)
Gatilhos7 listados em §3
Operadores12 listados em §7
MetatagsCatálogo oficial por gatilho/canal
Aninhamentobranches, onResponse, onNext, onOpen, onClickLink, if_true/if_false

❌ Proibido (conexões — causa nós "voando")

ItemMotivo
nextId no spine → step com parentId de ramoArestas conflitantes no editor
parentId de branch de outra condiçãoStep órfão ou ligado à condição errada
end sem parentId no meio do arrayNó Finalizar solto
Ramo sem steps e sem endSaída morta + merge visual incorreto
Intercalar steps de ramos diferentes no arrayEditor reconstrói edges errado
nextId na condition apontando para step de ramoSpine e ramo colidem
Delay de recuperação em ramo paidLógica de negócio invertida

❌ Proibido (geral)

ItemMotivo
Webhook / HTTP / API externa no copilotNão existe no schema do agente
create_coupon no copilotNão implementado no motor
IDs inventados (action-1, node-xyz)Validação falha
Metatags inventadas ({{cliente.nome}}, {{nome}})UNKNOWN_METATAG
Campos de condição inventadosValidação falha
Remover trigger-0Quebra o fluxo
SMS > 160 charsRejeitado pela validação do painel
Email sem remetente/assunto/conteúdoRejeitado pela validação do painel
Gatilho fora do enum400 Bad Request
> 50 steps400 Bad Request
Delay 0 ou > 7 dias400 Bad Request
HTML em SMSRejeitado
confidence=high com operations: []Proposta incompleta
Prometer proposta futura ("aguarde…")UX quebrada

11. Validação em camadas

Camada 1 — Validação estrutural (sempre ativa)

Tipos de step, limites numéricos e conteúdo obrigatório por canal (WhatsApp, email, SMS).

Camada 2 — Metatags (feature flag)

AUTOMATION_TEMPLATE_VALIDATION_MODE=off|warn|block

ModoComportamento
offIgnora metatags
warnRetorna _validation na resposta, não bloqueia
blockBloqueia save/ativação se active=true e há errors

Errors que bloqueiam: UNKNOWN_METATAG, UNAVAILABLE_FOR_TRIGGER, UNAVAILABLE_FOR_CHANNEL, PLANNED_METATAG, INVALID_SYNTAX, UNIMPLEMENTED_ACTION

Warnings (nunca bloqueiam): LEGACY_METATAG, DEPRECATED_METATAG, SENSITIVE_METATAG, EMPTY_TEMPLATE

Camada 3 — Editor (copilot)

Validação de IDs do fluxo, âncoras de posicionamento, campos de condição e metatags permitidas para o gatilho.

Resposta de erro (metatags)

{
  "success": false,
  "code": "AUTOMATION_TEMPLATE_VALIDATION_FAILED",
  "message": "Existem variáveis inválidas nos templates da automação.",
  "validation": {
    "valid": false,
    "totalErrors": 1,
    "totalWarnings": 0,
    "steps": []
  }
}

12. Exemplos completos

12.1 Importação no painel — Recuperação PIX (mínimo válido)

{
  "name": "Lembrete PIX 30 minutos",
  "trigger_event": "pix.generated",
  "active": true,
  "steps": [
    {
      "type": "action",
      "name": "send_whatsapp",
      "data": {
        "type": "text",
        "message": "Oi {{customer.name}}! Seu PIX de {{order.total}} foi gerado. Pague em: {{order.domain}}"
      }
    },
    {
      "type": "delay",
      "minutes": 30
    },
    {
      "type": "condition",
      "branches": [
        {
          "id": "pending",
          "label": "Ainda pendente",
          "conditions": [
            { "field": "payment_status", "operator": "=", "value": "pending" }
          ],
          "steps": [
            {
              "type": "action",
              "name": "send_whatsapp",
              "data": {
                "type": "text",
                "message": "{{customer.name}}, seu PIX ainda aguarda pagamento de {{order.total}}."
              }
            }
          ]
        },
        {
          "id": "paid",
          "label": "Pago",
          "conditions": [
            { "field": "payment_status", "operator": "=", "value": "paid" }
          ],
          "steps": [
            {
              "type": "action",
              "name": "send_whatsapp",
              "data": {
                "type": "text",
                "message": "Pagamento confirmado, {{customer.name}}! Obrigado pela compra."
              }
            }
          ]
        }
      ]
    },
    {
      "type": "end"
    }
  ]
}

12.2 Importação no painel — Carrinho abandonado + email

{
  "name": "Recuperação carrinho 1h",
  "trigger_event": "cart.abandoned",
  "active": true,
  "steps": [
    {
      "type": "delay",
      "minutes": 60
    },
    {
      "type": "action",
      "name": "send_email",
      "data": {
        "remetente_nome": "Minha Loja",
        "remetente_email": "[email protected]",
        "subject": "Você esqueceu algo, {{customer.name}}?",
        "html": "<p>Seus itens ainda estão te esperando:</p><p>{{cart.items}}</p><p><a href=\"{{order.domain}}\">Finalizar compra</a></p>"
      }
    }
  ]
}

12.3 Copilot — Adicionar delay + condição

{
  "intent": "create_step",
  "summary": "Adiciona espera de 15 min e condição de pagamento pendente",
  "userSummary": "Vou esperar 15 minutos e só enviar lembrete se o pagamento ainda estiver pendente.",
  "confidence": "high",
  "locationLabel": "Fluxo principal, após o primeiro WhatsApp",
  "expectedResult": "Lembrete só para quem não pagou",
  "operations": [
    {
      "op": "create_step",
      "afterNodeId": "9d1f2a3b-0001-4a1a-8a01-000000000001",
      "step": {
        "type": "delay",
        "label": "Aguardar 15 min",
        "config": { "value": 15, "unit": "minutes" }
      }
    },
    {
      "op": "create_step",
      "afterNodeId": "__PREVIOUS_CREATED__",
      "step": {
        "type": "condition",
        "label": "Status do pagamento",
        "config": {
          "branches": [
            {
              "id": "branch-pending",
              "label": "Pendente",
              "conditions": [
                { "field": "payment_status", "operator": "=", "value": "pending" }
              ]
            },
            {
              "id": "branch-paid",
              "label": "Pago",
              "conditions": [
                { "field": "payment_status", "operator": "=", "value": "paid" }
              ]
            }
          ]
        }
      }
    },
    {
      "op": "create_step",
      "conditionNodeId": "__PREVIOUS_CREATED__",
      "branchId": "branch-pending",
      "step": {
        "type": "action",
        "actionType": "send_whatsapp",
        "label": "Lembrete pendente",
        "config": {
          "content": {
            "type": "text",
            "text": "{{customer.name}}, seu pagamento de {{order.total}} ainda está pendente."
          }
        }
      }
    }
  ],
  "affectedNodeIds": ["9d1f2a3b-0001-4a1a-8a01-000000000001"],
  "requiredSafetyChanges": []
}

Nota: __PREVIOUS_CREATED__ é ilustrativo — na prática o copilot encadeia com IDs reais do contexto ou omite afterNodeId para encadear automaticamente.

12.4 Copilot — Atualizar mensagem existente

{
  "intent": "update_message",
  "summary": "Atualiza texto do WhatsApp inicial",
  "userSummary": "Vou personalizar a mensagem de boas-vindas com o nome do cliente.",
  "confidence": "high",
  "operations": [
    {
      "op": "update_step",
      "nodeId": "9d1f2a3b-0001-4a1a-8a01-000000000001",
      "config": {
        "content": {
          "type": "text",
          "text": "Olá {{customer.name}}! Seu PIX de {{order.total}} está pronto. Finalize em minutos!"
        }
      }
    }
  ],
  "affectedNodeIds": ["9d1f2a3b-0001-4a1a-8a01-000000000001"],
  "changesPreview": [
    {
      "nodeId": "9d1f2a3b-0001-4a1a-8a01-000000000001",
      "humanLabel": "WhatsApp inicial",
      "before": "Oi! Seu PIX foi gerado.",
      "after": "Olá {{customer.name}}! Seu PIX de {{order.total}} está pronto."
    }
  ]
}

12.5 Copilot — Ambiguidade (não alterar fluxo)

{
  "intent": "clarify",
  "summary": "Preciso saber qual mensagem alterar",
  "userSummary": "Encontrei mais de uma mensagem de WhatsApp. Qual delas você quer editar?",
  "confidence": "low",
  "operations": [],
  "affectedNodeIds": [],
  "disambiguationOptions": [
    {
      "humanLabel": "WhatsApp enviado logo após o PIX ser gerado",
      "nodeId": "9d1f2a3b-0001-4a1a-8a01-000000000001",
      "description": "Primeira mensagem do fluxo"
    },
    {
      "humanLabel": "WhatsApp de lembrete (caminho pendente)",
      "nodeId": "9d1f2a3b-0007-4a1a-8a01-000000000007",
      "description": "Enviado só se ainda não pagou"
    }
  ]
}

13. Anti-padrões

Anti-padrãoProblemaCorreção
Steps sem nextIdEditor sem linhas; nós “voando”Encadear todos os steps não-terminais com nextId
parentId = id do step anteriorÓrfão no editor; filho não entra no ramoTronco: sem parentId. Ramo: parentId = branches[].id
parentId em conditionSpine quebradoConditions nunca têm parentId
order.status em conditions[]Motor retorna null — condição mortapayment_status + pending/paid
Condição antes do delay em recuperaçãoVerifica status imediato, antes do tempo passardelay → condition → action
payment_status = "PAID" (maiúsculo)Condição nunca verdadeiraUse paid (minúsculas)
Múltiplos lembretes sem condiçãoCliente que já pagou recebe cobrançaCondição payment_status = pending antes de cada lembrete
Ramo de condição vazioCaminho silenciosoAdicionar action, delay ou end
Último email pendente sem endNó final solto no editoraction.nextId → end com mesmo parentId
{{nome}} ou {{cliente}}Metatag inválida{{customer.name}} do catálogo
SMS com <p>texto</p>RejeitadoTexto puro, ≤160 chars
Email só com body, sem subjectRejeitadosubject + html
webhook em fluxo ativoNão executaRemover ou substituir por mensagem
IDs fictícios no copilotValidação falhaIDs do contexto
operations: [] com confidence: highProposta vaziaPreencher operations ou baixar confidence
nextId da condition → step do spine sem parentIdArestas cruzadas no editorcondition.nextId → filho do ramo pago (parentId = branch paid)
parentId: "paid-3h" em step antes da condição 3hÓrfão / ligação à condição erradaUsar só branch IDs da condição pai imediata
Delay de recuperação com parentId do email anteriorDelay fora do ramo pendenteparentId = id da branch pending, não do action
Delay 3h com parentId: paid-20minQuem já pagou entra em esperaDelay de recuperação em pending-*
end sem parentId no meio do arrayFinalizar solto no editorend com mesmo parentId do ramo
Steps de ramos intercalados no arrayEditor agrupa erradoAgrupar todos os steps de cada ramo juntos
Ramo paid sem mensagem nem endCaminho vazio para quem pagouaction de confirmação + end

14. Checklist pré-envio

Antes de retornar JSON, a LLM deve confirmar:

  • Gatilho é um dos 7 valores válidos
  • Nome tem 3–100 caracteres
  • 1–50 steps
  • Todos os type de step são permitidos
  • Ações são send_whatsapp | send_email | send_sms (copilot)
  • WhatsApp tem texto ou mídia conforme o type
  • Email tem remetente_nome, remetente_email, subject (≥3), html ou body
  • SMS ≤ 160 chars, sem HTML
  • Delays entre 1 e 10.080 minutos
  • Condições de pagamento usam payment_status com pending/paid (não order.status)
  • Metatags só do catálogo do gatilho
  • IDs só do contexto (copilot) ou omitidos (API nova)
  • Ramos de condição não ficam vazios sem intenção
  • Fluxos de recuperação têm checagem de pagamento antes de lembretes
  • Sem webhook/HTTP/cupom (copilot)
  • operations preenchido se confidence ≠ low e houve pedido de alteração

Conexões flat (evitar nós "voando" no editor)

  • Todo step não-terminal tem nextId apontando para o próximo step da sequência
  • Steps do spine não têm parentId
  • type: "condition" nunca tem parentId
  • parentId é sempre branches[].id — nunca id de outro step
  • Delays/actions no ramo pendente: parentId = branch pending (não id do email anterior)
  • condition.nextId → primeiro filho do ramo pago (opcional mas recomendado para o editor)
  • condition.nextId não aponta para step do spine sem parentId
  • Todo parentId é branches[].id de uma condição existente no fluxo
  • Branch IDs de condições diferentes têm prefixos distintos (ex: check1-paid, check2-pending)
  • Cada saída de condição tem caminho completo até end
  • Todo end em ramo tem o mesmo parentId do ramo
  • nextId só liga steps do mesmo parentId (ou spine sem parent)
  • Ramo paid tem end (não delay de recuperação)
  • Ramo pending de recuperação: delay com parentId da branch → próxima condition

Apêndice A — Consulta de metatags

MétodoRotaUso
GET/automation/metatags?triggerType=Catálogo de variáveis válidas por gatilho

Apêndice B — Tom e marca Corvex

  • Idioma: português BR
  • Tom: direto, profissional, sem exageros
  • Email: HTML válido; propostas do copilot usam <p>...</p> mínimo
  • WhatsApp: emojis com moderação
  • Não criar descontos/cupons sem pedido explícito do usuário

Última atualização: v1.2 — {{order.pix_qr_code}} via URL HTTPS (sem base64 inline em email).