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:
| # | Regra | Errado (comum em LLMs) | Certo |
|---|---|---|---|
| 1 | nextId em toda cadeia | Steps sequenciais sem nextId | Cada step (exceto end terminal) tem nextId → próximo step |
| 2 | parentId = id da branch, nunca do step anterior | "parentId": "uuid-do-email-anterior" | "parentId": "pending-check1" (= branches[].id) |
| 3 | Spine sem parentId | delay/condition do tronco com parentId | Tronco: email → delay → condition sem parentId |
| 4 | Condição de pagamento | order.status + PENDING/PAID | payment_status + pending/paid |
| 5 | Fechar ramos com end | último email pendente sem end | action.nextId → end com mesmo parentId da branch |
nextIdvsparentId:nextId= “próximo na sequência”.parentId= “pertence a esta saída de condição”. Nunca useparentIdpara encadear steps do tronco.
Índice
- Visão geral
- Dois formatos JSON
- Gatilhos
- Limites numéricos
- Tipos de step (formato JSON)
- Formato Copilot (
propose_flow_changes) - Condições
- Metatags
- Conexões e topologia (formato flat — CRÍTICO)
- O que é permitido vs proibido
- Validação em camadas
- Exemplos completos
- Anti-padrões
- 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
conditione 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
| Contexto | Quando usar | Referência |
|---|---|---|
| JSON para importação | LLM gera o arquivo; usuário importa no painel | Este manifesto §5 |
| Copilot (editor) | Assistente propose_flow_changes dentro do painel | Este 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:
| Valor | Label | Quando dispara | Orientação para a LLM |
|---|---|---|---|
payment.confirmed | Pagamento confirmado | Pedido pago com sucesso | Agradecimento, resumo, rastreio. Não enviar cobrança. |
payment.failed | Pagamento recusado | Falha na adquirente | Orientar nova tentativa; não tratar como pago. |
pix.generated | PIX gerado | Código PIX criado | Recuperação com delays + condição de pagamento antes de lembretes. |
boleto.generated | Boleto gerado | Boleto emitido | Idem PIX; use variáveis de boleto. |
cart.abandoned | Carrinho abandonado | Checkout abandonado | Reengajamento; {{cart.items}}, link checkout; sem vars exclusivas PIX. |
lead.created | Lead criado | Lead capturado no checkout | Nutrição leve; prefira lead.* e customer.*. |
tracking_code_updated | Rastreio atualizado | Código de rastreio atribuído | Transportadora, prazo, código de rastreio. |
Proibido: inventar gatilhos (order.shipped, subscription.renewed, etc.).
4. Limites numéricos
| Regra | Mínimo | Máximo | Padrão |
|---|---|---|---|
name | 3 chars | 100 chars | — |
steps (array raiz) | 1 | 50 | — |
delay.minutes | 1 | 10.080 (7 dias) | — |
retry.maxAttempts | 1 | 10 | 3 |
retry.delayBetweenAttempts | 0 min | 10.080 min | 0 |
loop.maxIterations | 1 | 100 | 10 |
SMS message / text | 1 char | 160 chars | — |
Email subject | 3 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 desconectadosparentId(flat): somente em filhos de branch (branches[].id); não use para encadear o tronco
5.1 delay
delay{
"type": "delay",
"minutes": 30
}| Campo | Tipo | Obrigatório | Regras |
|---|---|---|---|
minutes | integer | sim | 1–10.080 |
Boas práticas: em recuperação PIX/boleto/carrinho, coloque delay antes da condição que verifica pagamento.
5.2 condition
conditionFormato simples (legado — prefira payment_status em condições novas)
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
action{
"type": "action",
"name": "send_whatsapp",
"data": { },
"onResponse": [],
"onNext": [],
"onOpen": [],
"onClickLink": []
}Ações permitidas no motor
name | Executa? | Copilot pode propor? |
|---|---|---|
send_whatsapp | ✅ | ✅ |
send_email | ✅ | ✅ |
send_sms | ✅ | ✅ |
webhook | ❌ não implementado | ❌ |
create_coupon | ❌ não implementado | ❌ |
Saídas aninhadas (action)
| Campo API | Handle copilot | Canal | Quando executa |
|---|---|---|---|
onResponse | reply | WhatsApp, SMS | Cliente responde |
onNext | next | Todos mensagem | Após enviar |
onOpen | open | Email aberto | |
onClickLink | click_link | Link clicado | |
| — | no_reply | SMS | Sem resposta (editor) |
send_whatsapp — data
send_whatsapp — dataTexto (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:
messageoutextnão vazio - Mídia: URL do campo correspondente obrigatória
- Botões:
buttonActionsouchoicesnão vazio - OTP:
codeobrigatório
send_email — data
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"
}| Campo | Obrigatório | Regras |
|---|---|---|
remetente_nome | sim | Não vazio (aceita content.remetente_nome) |
remetente_email | sim | Email válido |
subject | sim | Mínimo 3 caracteres |
html ou body | sim | Pelo menos um preenchido |
send_sms — data
send_sms — data{
"message": "{{customer.name}}, seu pedido foi confirmado!"
}| Regra | Valor |
|---|---|
message | Obrigatório, máx. 160 caracteres |
| HTML | Proibido |
5.4 retry
retry{
"type": "retry",
"maxAttempts": 3,
"delayBetweenAttempts": 5,
"steps": [
{
"type": "action",
"name": "send_whatsapp",
"data": { "type": "text", "message": "Tentativa de envio..." }
}
]
}5.5 loop
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
end{
"type": "end"
}Encerra explicitamente o caminho. Recomendado no fim de ramos principais.
6. Formato Copilot (propose_flow_changes)
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": []
}| Campo | Regras |
|---|---|
confidence | high | medium | low |
confidence=low | operations: [] + disambiguationOptions preenchido |
confidence=high/medium + pedido de alteração | operations nunca vazio |
affectedNodeIds | Todos 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
create_step — âncoras de posicionamento{
"op": "create_step",
"afterNodeId": "delay-abc",
"step": { "type": "action", "actionType": "send_whatsapp", "config": { "content": { "text": "..." } } }
}| Cenário | Parâmetros |
|---|---|
| Primeira etapa do fluxo principal | omitir afterNodeId ou afterNodeId: "trigger-0" só na 1ª |
| Encadear no fluxo principal | afterNodeId = ID real do nó anterior |
| Início de ramo vazio | conditionNodeId + branchId |
| Meio/fim de ramo | conditionNodeId + branchId + afterNodeId |
| Saída onResponse | actionNodeId + actionOutputHandle: "reply" |
| Saída onNext | actionNodeId + actionOutputHandle: "next" |
| Email aberto | actionOutputHandle: "open" |
| Clique no link | actionOutputHandle: "click_link" |
Proibido: inventar IDs (action-1, delay-2). Use apenas IDs do contexto do fluxo.
Tipos de mensagem (copilot content.type)
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)
| Campo | Formato do valor | Exemplos | Runtime |
|---|---|---|---|
payment_status | minúsculas | paid, pending, failed | ✅ Use sempre em condições de pagamento |
order.payment_method / payment_method | minúsculas | pix, boleto, credit_card | ✅ |
order.total, amount | número | usar >, <, >=, <= | ✅ |
order.status | MAIÚSCULAS no catálogo do editor | PENDING, 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. Emconditions[]de branches, o motor só resolvepayment_status(via query ao pedido). Usarorder.statusem 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
- Nunca inventar — use o catálogo
GET /automation/metatags?triggerType=... - Cada gatilho expõe um subconjunto de metatags; variável fora do catálogo → erro
UNKNOWN_METATAG(modoblock) - Metatag ausente no contexto → substituída por
"" - Valores monetários → formatados
R$ X,XX - Datas →
DD/MM/YYYY HH:mm
Metatags frequentes
| Metatag | Uso |
|---|---|
{{customer.name}} | Nome do cliente |
{{customer.phone}} | Telefone (WhatsApp/SMS) |
{{customer.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}})
{{order.pix_qr_code}})Metatag HTML gerada automaticamente a partir de {{order.pix_code}} (EMV copia e cola ou link curto da adquirente).
| Regra | Detalhe |
|---|---|
| Canal | email apenas — não use em SMS/WhatsApp |
| Gatilho | pix.generated (com order.pix_code preenchido) |
| Formato | <img src="https://apiv3.usecorvex.com.br/public/pix-qr.png?d=...&s=180" ...> |
| Por quê HTTPS | Clientes de email (Gmail, Outlook) bloqueiam data:image/png;base64,... |
| MJML | Envolva em <mj-raw>{{order.pix_qr_code}}</mj-raw> |
| Valor | Use {{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
| Gatilho | Não use |
|---|---|
cart.abandoned | {{order.pix_code}}, vars exclusivas de pagamento confirmado |
payment.confirmed | mensagens 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
endsoltos, ramos cruzados.
Causa quase sempre: uso incorreto deparentId,nextIdebranches[].idno 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 semparentIdde 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 LLM | Sintoma | Correção |
|---|---|---|---|
| 1 | Omitir nextId em todos os steps | Editor sem linhas entre email → delay → condition | Encadear: "nextId": "id-do-proximo-step" em cada step não-terminal |
| 2 | parentId = id do step anterior (ex.: delay com parentId do email) | Nó órfão no editor | Tronco: sem parentId. Ramo: parentId = branches[].id |
| 3 | parentId em condition do tronco (ex.: condition com parentId do delay) | Condition deslocada / spine quebrado | type: "condition" nunca tem parentId |
| 4 | order.status + PENDING/PAID nas branches | Condição sempre falsa (fieldValue: null) | payment_status + pending/paid |
| 5 | Último ramo pendente sem end | Nó 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): semparentId; encadeados comnextId cond-1.nextId→ primeiro filho do ramo pago (ajuda o editor a desenhar a saída “pago”)- Filhos de
pending-1: todos comparentId: "pending-1"(inclusive o delay), não o id do email cond-2no spine lógico do ramo pendente: semparentId(conditions nunca têm)- Último email pendente →
endcom mesmoparentId
9.1 Dois mundos: spine vs ramo
| Mundo | Quem entra | Campos usados |
|---|---|---|
| Spine (principal) | Email inicial, delays e conditions antes de ramificar | Sem parentId; obrigatório nextId entre si |
| Ramo (branch) | Actions, delays e end após uma saída de condition | parentId = branches[].id; nextId só entre steps do mesmo parentId |
| Condition | Nó 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
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:
- Step do mesmo spine (ambos sem
parentIdde ramo), ou - Próximo step no mesmo ramo (mesmo
parentId)
9.3 Regra #2 — NUNCA misturar branch.id de condições diferentes
branch.id de condições diferentesCada 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ção | Prefixo sugerido | Exemplos de branch.id |
|---|---|---|
| Checagem 20 min | 20min- | 20min-pending, 20min-paid, 20min-failed |
| Checagem 3 h | 3h- | 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
end OU encadeia com nextId dentro do MESMO parentIdTodo 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
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
conditionaninhada comparentIdde ramo é válido no formato flat — ela é filha daquele caminho pendente.
9.5 Ordem do array steps[] (importante para o editor)
steps[] (importante para o editor)Ao serializar em formato flat, siga esta ordem:
- Spine até a condição (action → delay → condition)
- Ramo A — todos os steps com
parentIddo branch A, na ordem de execução - Ramo B — todos os steps com
parentIddo branch B - Ramo C — idem
- 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
parentIdcorreto
❌ 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)
| Campo | Onde usar | Não usar para |
|---|---|---|
nextId | Encadear spine; encadear steps no mesmo parentId | Apontar do spine para step com parentId de ramo |
parentId | Identificar ramo (branches[].id) | Substituir nextId no spine principal |
branches[].id | ID estável da saída da condição | Reutilizar em outra condição |
id | Identificador único do step | Inventar 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)
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)
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
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(semparentId) condition.nextId→ primeiro filho do ramo pago (endou action comparentId: "…-paid")- Ramo pendente: todos os steps com
parentId: "…-pending"enextIdinterno - Ramo pago:
end(ou confirmação) comparentId: "…-paid" - Nunca
parentIdemtype: "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
nextIdda condição para dentro de ramo) - Delay 3h está em
20min-pending, não em20min-paid - IDs
20min-*vs3h-*separados - Todo ramo termina em
endcom o mesmoparentId -
nextIdsó liga steps com o mesmoparentId(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 visual | Causa provável no JSON |
|---|---|
| Nenhuma linha entre steps sequenciais | Falta nextId em toda a cadeia (erro #1 de LLMs) |
| Delay/condition “grudados” no step errado | parentId = id do step anterior em vez de branches[].id |
end isolado à direita | end sem parentId no spine, sem nextId chegando |
| Delay longe do ramo pendente | parentId apontando para ramo errado (paid em vez de pending) |
| Condição flutuando após delay | condition com parentId do delay (conditions nunca têm parentId) |
| Cruzamento de linhas | nextId da condição apontando para step do spine sem parentId |
| Ramo pago com delay de recuperação | Lógica invertida — delay de cobrança no ramo paid |
| Múltiplos emails desconectados | Steps de ramos intercalados no array sem nextId/parentId corretos |
| Lembretes sempre ou nunca disparam | order.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)
- Todo step deve estar conectado ao fluxo — nenhum órfão
nextIdobrigatório em cada step não-terminal do encadeamentotrigger-0é o nó raiz — nunca remover- Cada
branches[].idtem steps ouend - Máximo 50 steps
- Spine usa
nextIdsemparentId - Ramos usam
parentId(= branch id) +nextIdinterno parentIdnunca é id de outro step — sóbranches[].idtype: "condition"nunca temparentId- Nunca misturar branch IDs entre condições (use prefixos:
check1-paid,check2-pending) - Sempre fechar ramos com
endou encadeamento completo no mesmoparentId - Condições de pagamento:
payment_status+pending/paid
10. O que é permitido vs proibido
✅ Permitido
| Categoria | Itens |
|---|---|
| Step types | delay, 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) |
| Gatilhos | 7 listados em §3 |
| Operadores | 12 listados em §7 |
| Metatags | Catálogo oficial por gatilho/canal |
| Aninhamento | branches, onResponse, onNext, onOpen, onClickLink, if_true/if_false |
❌ Proibido (conexões — causa nós "voando")
| Item | Motivo |
|---|---|
nextId no spine → step com parentId de ramo | Arestas conflitantes no editor |
parentId de branch de outra condição | Step órfão ou ligado à condição errada |
end sem parentId no meio do array | Nó Finalizar solto |
Ramo sem steps e sem end | Saída morta + merge visual incorreto |
| Intercalar steps de ramos diferentes no array | Editor reconstrói edges errado |
nextId na condition apontando para step de ramo | Spine e ramo colidem |
Delay de recuperação em ramo paid | Lógica de negócio invertida |
❌ Proibido (geral)
| Item | Motivo |
|---|---|
| Webhook / HTTP / API externa no copilot | Não existe no schema do agente |
create_coupon no copilot | Não implementado no motor |
IDs inventados (action-1, node-xyz) | Validação falha |
Metatags inventadas ({{cliente.nome}}, {{nome}}) | UNKNOWN_METATAG |
| Campos de condição inventados | Validação falha |
Remover trigger-0 | Quebra o fluxo |
| SMS > 160 chars | Rejeitado pela validação do painel |
| Email sem remetente/assunto/conteúdo | Rejeitado pela validação do painel |
| Gatilho fora do enum | 400 Bad Request |
| > 50 steps | 400 Bad Request |
| Delay 0 ou > 7 dias | 400 Bad Request |
| HTML em SMS | Rejeitado |
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
| Modo | Comportamento |
|---|---|
off | Ignora metatags |
warn | Retorna _validation na resposta, não bloqueia |
block | Bloqueia 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 omiteafterNodeIdpara 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ão | Problema | Correção |
|---|---|---|
Steps sem nextId | Editor 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 ramo | Tronco: sem parentId. Ramo: parentId = branches[].id |
parentId em condition | Spine quebrado | Conditions nunca têm parentId |
order.status em conditions[] | Motor retorna null — condição morta | payment_status + pending/paid |
| Condição antes do delay em recuperação | Verifica status imediato, antes do tempo passar | delay → condition → action |
payment_status = "PAID" (maiúsculo) | Condição nunca verdadeira | Use paid (minúsculas) |
| Múltiplos lembretes sem condição | Cliente que já pagou recebe cobrança | Condição payment_status = pending antes de cada lembrete |
| Ramo de condição vazio | Caminho silencioso | Adicionar action, delay ou end |
Último email pendente sem end | Nó final solto no editor | action.nextId → end com mesmo parentId |
{{nome}} ou {{cliente}} | Metatag inválida | {{customer.name}} do catálogo |
SMS com <p>texto</p> | Rejeitado | Texto puro, ≤160 chars |
Email só com body, sem subject | Rejeitado | subject + html |
webhook em fluxo ativo | Não executa | Remover ou substituir por mensagem |
| IDs fictícios no copilot | Validação falha | IDs do contexto |
operations: [] com confidence: high | Proposta vazia | Preencher operations ou baixar confidence |
nextId da condition → step do spine sem parentId | Arestas cruzadas no editor | condition.nextId → filho do ramo pago (parentId = branch paid) |
parentId: "paid-3h" em step antes da condição 3h | Órfão / ligação à condição errada | Usar só branch IDs da condição pai imediata |
Delay de recuperação com parentId do email anterior | Delay fora do ramo pendente | parentId = id da branch pending, não do action |
Delay 3h com parentId: paid-20min | Quem já pagou entra em espera | Delay de recuperação em pending-* |
end sem parentId no meio do array | Finalizar solto no editor | end com mesmo parentId do ramo |
| Steps de ramos intercalados no array | Editor agrupa errado | Agrupar todos os steps de cada ramo juntos |
Ramo paid sem mensagem nem end | Caminho vazio para quem pagou | action 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
typede 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),htmloubody - SMS ≤ 160 chars, sem HTML
- Delays entre 1 e 10.080 minutos
- Condições de pagamento usam
payment_statuscompending/paid(nãoorder.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)
-
operationspreenchido seconfidence≠lowe houve pedido de alteração
Conexões flat (evitar nós "voando" no editor)
- Todo step não-terminal tem
nextIdapontando para o próximo step da sequência - Steps do spine não têm
parentId -
type: "condition"nunca temparentId -
parentIdé semprebranches[].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.nextIdnão aponta para step do spine semparentId - Todo
parentIdébranches[].idde 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
endem ramo tem o mesmoparentIddo ramo -
nextIdsó liga steps do mesmoparentId(ou spine sem parent) - Ramo
paidtemend(não delay de recuperação) - Ramo
pendingde recuperação: delay comparentIdda branch → próxima condition
Apêndice A — Consulta de metatags
| Método | Rota | Uso |
|---|---|---|
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).