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

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.

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.

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. true autoriza a ação (a validação passa, a automação inscreve, o fluxo segue pelo "então"); false barra.
  • 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".

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.

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.lote em vez de invoice.lot (ou o nome real do campo).
  • Começar pela entidade errada: seller.seller_id em vez de invoice.seller_id quando o contexto é venda.
  • Tratar coleção como associação 1:1: invoice.product.lot não existe — uma venda tem invoice_products (coleção), não product. Para ler valores de itens use values_of.

Quando desconfiar, substitua a fórmula inteira por apenas o attr em questão e veja se o valor retornado faz sentido.

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.