Fórmulas
Várias partes do Fácil123 aceitam uma fórmula no lugar de um valor fixo: critérios de execução, impressões e etiquetas personalizadas, campos personalizados preenchidos automaticamente, critérios de inscrição e de roteamento em automações e validações personalizadas.
A linguagem das fórmulas é o JSON Logic — um jeito de escrever regras como um pequeno JSON. Este artigo explica a sintaxe e lista todas as operações disponíveis, incluindo as que são específicas do Fácil123.
Onde você usa fórmulas
- Campos personalizados com preenchimento por fórmula
- Impressões personalizadas e etiquetas personalizadas em elementos do tipo Fórmula
- Validações personalizadas — tanto no critério de execução quanto na validação em si
- Automações — no critério de inscrição e em comandos como Se então
A sintaxe é a mesma em todos esses lugares. O que muda é qual é a entidade do contexto (uma venda, uma pessoa, uma produção…) e, portanto, quais nomes de atributo funcionam em attr.
Como uma fórmula funciona
Toda fórmula é um objeto JSON com uma única chave — o nome da operação — e um array com os argumentos. Esta fórmula compara dois valores e devolve true:
{ "==": [1, 1] }
Resultado: true.
Uma fórmula pode ser argumento de outra. É assim que você monta expressões maiores — cada operação devolve um valor, que vira entrada para a operação de fora:
{
"and": [
{ ">": [{ "attr": "invoice.total" }, 0] },
{ "==": [{ "attr": "invoice.status" }, "A"] }
]
}
Resultado: true quando o total da venda é maior que zero E a situação é "A" (autorizada); false caso contrário.
Nomes dos atributos
O primeiro argumento de attr é um caminho começando pela entidade do contexto:
- Em venda (invoice):
invoice.total,invoice.status,invoice.note,invoice.emitted_at,invoice.recipient.name,invoice.recipient.kind,invoice.recipient.cnpj,invoice.seller.name,invoice.licensee.name - Em pessoa (person):
person.name,person.kind,person.cpf,person.cnpj,person.act_as_customer - Em produção (production), produto (product), compra (purchase), transação (transaction): mesmo padrão, sempre começando pelo nome da entidade
Os editores do Fácil123 mostram a lista de atributos disponíveis quando você está montando a fórmula — use-a como referência ao invés de memorizar nomes.
Campos personalizados aparecem com o prefixo cf_. Por exemplo, um campo personalizado de venda com nome interno vendedor_indicador é acessado como invoice.cf_vendedor_indicador.
Navegando em campos do tipo cadastro
Um campo personalizado do tipo Cadastro aponta para outro registro (uma Pessoa, um Produto). Sozinho, o caminho devolve o id do registro vinculado:
{ "attr": "invoice.cf_profissional" }
Resultado: o id da pessoa vinculada (por exemplo, 4821).
Para ler um atributo desse registro (o nome, por exemplo), acrescente o atributo ao caminho:
{ "attr": "invoice.cf_profissional.name" }
Resultado: o nome da pessoa vinculada (por exemplo, "João Arquiteto").
Você pode navegar mais fundo, passando por associações do registro vinculado (o mesmo que faz em qualquer outro caminho de attr):
{ "attr": "invoice.cf_profissional.city.name" }
Resultado: o nome da cidade da pessoa vinculada.
Atenção: ler um atributo do registro vinculado funciona quando o campo aponta para Pessoa ou Produto. Para os demais cadastros (Conta, Usuário, Cidade, Categoria, e outros), o caminho continua devolvendo o id, e acrescentar um atributo (.name, etc.) resulta em vazio, não em erro.
Duas formas equivalentes
Em fórmulas prontas você vai encontrar tanto {"attr": "invoice.total"} (string) quanto {"attr": ["invoice.total"]} (array). As duas dão o mesmo resultado — a linguagem envolve um argumento solitário em array automaticamente. A forma em array é obrigatória quando a operação recebe dois ou mais argumentos (como values_of e format_mask_text).
Os exemplos deste artigo usam a forma em string sempre que a operação recebe um argumento só — fica mais enxuto de ler.
Operações padrão
Estas são as operações do JSON Logic e funcionam em qualquer lugar. A lista completa está em jsonlogic.com/operations — abaixo vão as mais usadas no Fácil123.
| Operação | O que faz |
|---|---|
==, != |
Comparação de igualdade |
>, >=, <, <= |
Comparação numérica ou de datas |
and, or, ! |
Lógica booleana |
if |
Condicional: {"if": [condição, então, senão]} |
in |
Verifica se um valor está em uma lista ou substring em texto |
var |
Lê um valor dos dados passados à fórmula |
cat |
Concatena textos |
+, -, *, /, % |
Aritmética |
min, max |
Menor/maior de uma lista |
merge |
Junta arrays em um só |
missing |
Lista quais variáveis de um conjunto não foram informadas |
all |
Verdadeiro se todos os itens do array satisfazem a condição |
some |
Verdadeiro se pelo menos um item satisfaz a condição |
none |
Verdadeiro se nenhum item satisfaz a condição |
reduce |
Reduz um array a um único valor, acumulando item a item |
Iterando sobre coleções
all, some, none e reduce são o que você usa para validar os itens de uma venda, as parcelas, os insumos de uma produção, etc. A coleção geralmente vem de um values_of.
Na venda, as coleções disponíveis são invoice.invoice_products (itens), invoice.invoice_bills (parcelas) e invoice.invoice_services (serviços).
Dentro da condição de all, some e none, cada item vira o contexto — você acessa o atributo do item atual com {"var": ".nome_do_atributo"} (repare no ponto inicial). O exemplo abaixo devolve true se todos os itens da venda têm quantidade maior que zero:
{
"all": [
{ "values_of": ["invoice.invoice_products", "quantity"] },
{ ">": [{ "var": ".quantity" }, 0] }
]
}
Resultado: true quando todo item tem quantity > 0; false se algum item tiver zero ou negativo.
Já o reduce leva três argumentos — coleção, expressão de acumulação e valor inicial — e expõe {"var": "accumulator"} (valor acumulado) e {"var": "current"} (item atual). Este exemplo conta quantas parcelas a venda tem:
{
"reduce": [
{ "values_of": ["invoice.invoice_bills", "expire_at"] },
{ "+": [1, { "var": "accumulator" }] },
0
]
}
Resultado: 3 numa venda com três parcelas; 0 numa venda sem parcelas.
Operações do Fácil123
Estas operações são adicionadas pelo Fácil123 e não existem no JSON Logic padrão.
Dados do documento
| Operação | O que faz |
|---|---|
attr |
Lê um atributo da entidade atual: {"attr": "invoice.total"}. O caminho pode navegar por associações: {"attr": "invoice.recipient.cnpj"} |
values_of |
Lê uma coleção — por exemplo, os valores de um atributo em cada item da venda: {"values_of": ["invoice.invoice_products", "quantity"]} |
memory |
Lê um valor que ficou em memória de um passo anterior da automação: {"memory": ["chave"]} |
current_user_id / more.current_user_id |
Devolve o id do usuário que disparou a ação |
more.current_weekday |
Devolve o dia da semana atual por extenso (ex.: "Monday") |
Verificação de valor
| Operação | O que faz |
|---|---|
is_empty |
Verdadeiro quando o valor é null: {"is_empty": [{"attr": "invoice.note"}]} |
is_filled |
Verdadeiro quando o valor está preenchido — inverso do is_empty |
contain |
Verdadeiro quando um texto contém outro: {"contain": ["abacaxi", "caxi"]} |
start_with |
Verdadeiro quando um texto começa com outro: {"start_with": [{"attr": "invoice.recipient.name"}, "SA "]} |
Texto
| Operação | O que faz |
|---|---|
cat |
Concatena: {"cat": ["Cliente: ", {"attr": "invoice.recipient.name"}]} (essa é padrão, mas vale lembrar) |
split |
Divide um texto por um separador: {"split": ["12/06/2026", "/"]} → ["12","06","2026"] |
array.at |
Pega o item na posição N de um array: {"array.at": [{"split": ["12/06/2026", "/"]}, 0]} → "12" |
rjust |
Preenche à esquerda até atingir um tamanho: {"rjust": [42, 5, "0"]} → "00042" |
fmt |
Formata número com N casas decimais: {"fmt": [3.1, 2]} → "3,10" |
format_mask_text |
Aplica uma máscara a um texto. Segundo argumento é a máscara (ex.: "##.###.###/####-##" para CNPJ) |
strftime |
Formata uma data no fuso do licenciado: {"strftime": [{"attr": "invoice.emitted_at"}, "%d/%m/%Y"]} |
Conversão
| Operação | O que faz |
|---|---|
as_time |
Converte um texto para data/hora: {"as_time": ["2026-04-16"]} |
as_decimal |
Converte um texto para número decimal: {"as_decimal": ["3.14"]} |
Tempo — valores
| Operação | O que faz |
|---|---|
time.now |
Agora: {"time.now": []} |
time.from_now |
Data futura, informando dias/hora/minuto: {"time.from_now": [7, 8, 0]} — daqui a 7 dias às 08h00 |
time.days_after_today / time.days_before_today |
O dia inteiro N dias depois/antes de hoje — útil para comparar com datas que não têm hora |
time.seconds_ago, time.seconds_from_now |
Um instante N segundos no passado/futuro |
time.minutes_ago, time.minutes_from_now |
Idem em minutos |
time.hours_ago, time.hours_from_now |
Idem em horas |
seconds |
Converte um número em uma duração em segundos, para somar em datas |
time.+ |
Soma uma duração a uma data: {"time.+": [{"time.now": []}, {"seconds": [3600]}]} |
time |
Atalho para intervalos do tipo "today", "yesterday", "tomorrow", "this_week", "last_week", "this_month", "last_month", "this_year", "last_year" |
Tempo — comparação
As comparações de tempo (time.equal, time.different, time.less_than, time.less_than_or_equal, time.greater_than, time.greater_than_or_equal) funcionam como os operadores comuns de comparação, mas aceitam intervalos no segundo argumento. Por exemplo, time.equal com um intervalo devolve verdadeiro quando a data cai dentro do intervalo — é o que permite comparar "a data é hoje" sem se importar com a hora.
{ "time.equal": [{ "attr": "invoice.emitted_at" }, { "time": ["today"] }] }
Resultado: true quando emitted_at caiu em qualquer hora do dia de hoje; false caso contrário.
Caracteres de controle (impressão térmica)
Usadas em elementos de Fórmula de impressões e etiquetas para enviar comandos à impressora.
| Operação | O que faz |
|---|---|
stx |
Caractere STX (início de texto, \x02) |
cr |
Caractere CR (retorno de carro, \x0D) |
lf |
Caractere LF (quebra de linha, \x0A) |
esc |
Caractere ESC (escape, \x1B) |
O que a fórmula deve devolver
O tipo de valor depende de onde a fórmula é usada:
- Validações personalizadas, critérios de execução, critérios de inscrição em automação, blocos "Se então": a fórmula devolve um booleano.
trueautoriza a ação (a validação passa, a automação inscreve, o fluxo segue pelo "então");falsebarra. - Campo personalizado por fórmula: a fórmula devolve o valor a ser gravado no campo (texto, número, data — conforme o tipo do campo).
- Elemento Fórmula de impressão/etiqueta: a fórmula devolve o texto a ser impresso.
Exemplos práticos
Concatenar nome e CNPJ formatado
Em uma etiqueta personalizada de venda, para imprimir "Indústria XYZ — 12.345.678/0001-90":
{
"cat": [
{ "attr": "invoice.recipient.name" },
" — ",
{ "format_mask_text": [{ "attr": "invoice.recipient.cnpj" }, "##.###.###/####-##"] }
]
}
Resultado: "Indústria XYZ — 12.345.678/0001-90".
Imprimir o nome do profissional vinculado por campo personalizado
A venda tem um campo personalizado do tipo Cadastro cf_profissional, apontando para a Pessoa que indicou a compra. Numa impressão personalizada de venda, para imprimir o nome dele em vez do id:
{ "attr": "invoice.cf_profissional.name" }
Resultado: o nome da pessoa vinculada. Sem o .name, a impressão sairia com o id do registro.
Obrigatoriedade do vendedor
Em uma validação personalizada de venda, barrar a gravação quando o vendedor não foi informado:
{ "is_filled": [{ "attr": "invoice.seller_id" }] }
Resultado: true (venda válida) quando seller_id está preenchido; false (barra a gravação) quando está vazio.
Restringir movimentação de etapa a usuários específicos
Só permitir que a venda seja salva se quem está salvando está em uma lista curta de usuários autorizados. O if com current_user_id == null é importante: jobs automáticos e rotinas de background não têm usuário — sem esse escape a validação barraria tudo que não é disparado manualmente.
{
"if": [
{ "==": [{ "current_user_id": [] }, null] },
true,
{ "in": [{ "current_user_id": [] }, [4748, 10069, 10067]] }
]
}
Resultado: true para rotinas automáticas ou para usuários cujo id está na lista; false para qualquer outro usuário.
Obrigar que a venda tenha pelo menos uma parcela
Usando reduce para contar as parcelas (invoice_bills) e comparando com zero:
{
">": [
{
"reduce": [
{ "values_of": ["invoice.invoice_bills", "expire_at"] },
{ "+": [1, { "var": "accumulator" }] },
0
]
},
0
]
}
Resultado: true quando há pelo menos uma parcela; false (barra) quando não há nenhuma.
Prazo de vencimento limitado pelo cadastro do cliente
Cada cliente tem um campo personalizado cf_prazo_maximo_dias com o prazo máximo que ele pode financiar. A validação barra a venda se qualquer parcela vencer depois desse limite, contado a partir da emissão:
{
"all": [
{ "values_of": ["invoice.invoice_bills", "expire_at"] },
{
"time.less_than_or_equal": [
{ "var": ".expire_at" },
{
"time.+": [
{ "attr": "invoice.emitted_at" },
{ "seconds": [{ "*": [{ "attr": "invoice.recipient.cf_prazo_maximo_dias" }, 86400] }] }
]
}
]
}
]
}
Resultado: true quando todas as parcelas vencem até o prazo máximo; false quando alguma parcela extrapola. O seconds com dias * 86400 converte dias em uma duração somável ao emitted_at.
Campo personalizado calculado: dia da semana da emissão
Preencher um campo do tipo Texto Curto em venda com o dia da semana em que ela foi emitida:
{ "strftime": [{ "attr": "invoice.emitted_at" }, "%A"] }
Resultado: "Monday", "Tuesday", etc. (o dia da semana da emissão).
Simulando um critério de execução com fórmula
Quando um critério simples de atributo-operador-valor (usado no campo Execução das validações personalizadas) não dá conta, por exemplo, quando você precisa navegar em coleções ou combinar várias condições, dá pra usar um truque: escolha um atributo qualquer da entidade (como Destinatário, em vendas) com operador igual a, e na fórmula do valor faça um if que devolve o próprio atributo quando a regra é verdadeira, ou um valor que nunca vai bater (como 0) quando é falsa.
Exemplo: uma validação que só deve rodar quando alguma parcela da venda é Boleto (suponha id 5398 do meio de pagamento).
- Atributo: Destinatário
- Operador: igual a
- Valor (fórmula):
{
"if": [
{
"some": [
{ "values_of": ["invoice.invoice_bills", "paymenttype_id"] },
{ "==": [{ "var": ".paymenttype_id" }, 5398] }
]
},
{ "attr": "invoice.recipient_id" },
0
]
}
Como funciona: quando tem parcela em Boleto, o if devolve o próprio recipient_id → a comparação vira recipient_id == recipient_id → verdadeira → a validação roda. Sem Boleto, o if devolve 0 → recipient_id == 0 → falsa → a validação é pulada.
Troque o 5398 pelo id real do meio de pagamento Boleto da licença (ele aparece na URL do registro em Engrenagem → Meios de pagamento). Se tem mais de um cadastro com nome Boleto, troque o == por in com a lista de ids:
{ "in": [{ "var": ".paymenttype_id" }, [5398, 5399, 5400]] }
Veja como configurar isso na tela de validação em Critério de execução com fórmula.
Erros comuns
Armadilhas que aparecem em fórmulas antigas e que quebram silenciosamente — sem erro visível, mas com resultado diferente do esperado.
Caminho de attr que não existe vira null
Se o caminho está errado, attr devolve null em vez de dar erro. O efeito é que a sua validação parece funcionar mas na verdade está comparando null com tudo.
Os escorregões mais comuns:
- Misturar português com inglês:
invoice.loteem vez deinvoice.lot(ou o nome real do campo). - Começar pela entidade errada:
seller.seller_idem vez deinvoice.seller_idquando o contexto é venda. - Tratar coleção como associação 1:1:
invoice.product.lotnão existe — uma venda teminvoice_products(coleção), nãoproduct. Para ler valores de itens usevalues_of.
Quando desconfiar, substitua a fórmula inteira por apenas o attr em questão e veja se o valor retornado faz sentido.
Escolher o validador no escopo errado (documento inteiro × cada item)
Antes de escrever a fórmula, decida o escopo da regra: ela fala do documento inteiro (a venda, a produção) ou de cada item dele (cada material, cada parcela, cada produto)? Essa decisão define qual validador/template usar — e o validador define quais caminhos de attr existem:
- Validador de documento inteiro (ex.: "Produção: Fórmula", "Venda: Fórmula"): o
attrenxerga só o documento —production.*,invoice.*. Caminhos de item (production_material.*,invoice_product.*) viramnullsilenciosamente. - Validador por item (ex.: "Produção: Fórmula para cada material da produção", "Venda: Fórmula para cada item da venda"): a fórmula roda uma vez por item, e o
attrcomeça pelo item —production_material.*,invoice_product.*.
O erro clássico é escrever uma regra "por item" dentro do validador de documento inteiro. Como o caminho do item vira null, ela barra (ou libera) sempre, não importa o que o usuário preencha.
Exemplo: "todo material da produção precisa ter o lote preenchido". No validador "Produção: Fórmula" (documento inteiro), production_material.lot é sempre null e a regra nunca funciona. O certo é o validador "Produção: Fórmula para cada material da produção", com a fórmula:
{ "is_filled": [{ "attr": "production_material.lot" }] }
Atenção: production.product é o produto final da produção, não os materiais. "Lote do produto" e "lote de cada material" são coisas diferentes (confira o escopo antes de escolher o validador).
Se precisar mesmo manter o validador de documento inteiro, dá pra varrer a coleção com all/some + values_of (veja Iterando sobre coleções) — mas, quando existe o validador "por item", ele é o caminho mais simples.
Usar values_of para um atributo único
values_of é para coleções (itens da venda, parcelas, materiais da produção). Para ler um atributo único — mesmo passando por associações — use attr.
Errado:
{ "==": [{ "values_of": ["invoice.seller_id"] }, null] }
Certo:
{ "is_empty": [{ "attr": "invoice.seller_id" }] }
or com vários != é sempre verdadeiro
Um valor não pode ser diferente de A e diferente de B e diferente de C ao mesmo tempo — algum desses != vai ser verdadeiro para qualquer combinação. Esta fórmula, que parece querer dizer "não é nenhuma dessas etapas", na prática devolve true para qualquer venda:
{
"or": [
{ "!=": [{ "attr": "invoice.invoicelane_id" }, 7773] },
{ "!=": [{ "attr": "invoice.invoicelane_id" }, 7774] },
{ "!=": [{ "attr": "invoice.invoicelane_id" }, 10170] }
]
}
O jeito correto de dizer "não está em nenhuma dessas" é usar in negado:
{ "!": { "in": [{ "attr": "invoice.invoicelane_id" }, [7773, 7774, 10170]] } }
Se a intenção era o oposto ("está em todas" — o que normalmente não faz sentido), troque or por and.
Perguntas e respostas
1 - Como descubro o nome do atributo para usar em attr?
R: O editor do Fácil123 mostra a lista de atributos da entidade atual e das entidades associadas — use essa lista como referência. Associações ficam acessíveis separando por ponto (ex.: invoice.recipient.cnpj). Para um campo personalizado, use o prefixo cf_ com o nome interno do campo (ex.: invoice.cf_vendedor_indicador).
2 - Minha fórmula está dando erro. Como eu depuro?
R: Comece isolando. Troque a fórmula por apenas {"attr": "invoice.total"} (ou o atributo em questão) para confirmar que o campo existe com o nome que você está usando. Depois monte a expressão de dentro pra fora, testando cada pedaço. Se uma subexpressão devolver null inesperado, provavelmente o caminho do attr está errado.
3 - Posso usar qualquer operação em qualquer lugar?
R: Quase. As operações de tempo que dependem do fuso do licenciado (strftime, time.from_now, time.days_after_today, etc.) precisam de um licenciado no contexto — praticamente sempre há um. Já memory só existe em automações, quando um passo anterior deixou valor em memória. E attr precisa de uma entidade no contexto, então não funciona em lugares totalmente genéricos.
4 - Qual a diferença entre == e time.equal?
R: == compara exatamente. time.equal sabe lidar com intervalos no segundo lado — por isso comparar invoice.emitted_at com {"time": ["today"]} usando time.equal funciona (o dia inteiro é um intervalo), enquanto == devolveria falso porque a hora não bate.