artigo

Business Rules: o funcionário que não pode fazer tudo ao mesmo tempo

Os quatro timings de Business Rule, o padrão Script Include + orquestrador e as armadilhas de recursão e performance que aparecem só em produção.

6 min de leitura avançado servicenow desenvolvimento business-rules

#o funcionário dos bastidores

Todo restaurante tem um funcionário que trabalha nos bastidores. Assim que o pedido chega, ele confere se está certo, calcula o desconto do dia, avisa a cozinha. Business Rule é esse funcionário: um script server-side que dispara quando um registro é inserido, atualizado, deletado ou consultado.

O problema começa quando esse funcionário decide que, já que está ali mesmo, também vai ligar para o fornecedor e lavar a louça — tudo ao mesmo tempo, enquanto o cliente espera na mesa. Esse Business Rule que faz de tudo até tem nome: God Business Rule. Vamos deixar o anti-padrão completo para outro dia; hoje o assunto é como estruturar o funcionário para ele não virar isso.

#quatro timings, quatro propósitos

Um Business Rule pode rodar em quatro momentos diferentes do ciclo de vida do registro, e cada um existe para um trabalho específico:

timingquando executauso corretoarmadilha clássica
Beforeantes de gravar no banco, no mesmo thread do usuáriovalidação, cálculo de campo antes de salvarquery pesada ou chamada REST aqui trava o usuário no spinner
Afterdepois de gravar, ainda no thread do usuárioatualizar registro relacionado, disparar eventoBR After que atualiza outro registro que aciona outro BR After...
Asyncfila separada, processada por um workerREST, e-mail, qualquer coisa que pode demorar ou falhartratar current como estado atual — ele foi capturado no momento do trigger e pode estar desatualizado
Displayno carregamento do formulário (GET)popular g_scratchpad para o Client Script usar sem round-trip extratentar gravar dado aqui — não persiste, e query pesada atrasa o form inteiro

A parte que costuma passar batido: Before e After rodam no mesmo thread da requisição do usuário. Qualquer operação lenta ali soma direto ao tempo que ele espera para ver "Saved". Regra prática que vale memorizar: qualquer operação que dependa de rede (chamada REST, integração externa) ou de processamento pesado não tem lugar em Before/After síncrono — vai para Async. Chamada REST síncrona em Before ou After é, quase sempre, um erro de arquitetura — não um detalhe de implementação.

#quando o funcionário vira o restaurante inteiro

O erro mais comum de arquitetura em Business Rules não é escolher o timing errado — é empilhar toda a lógica de negócio dentro do script do BR. E dá para entender por quê: current e previous estão ali, disponíveis, tudo parece resolvido no mesmo lugar. Por que complicar?

Porque daqui a seis meses você vai precisar da mesma lógica de cálculo de prioridade num Flow, numa API REST e num script de carga em lote. E ela vai estar enterrada dentro de um Business Rule. Você copia e cola. Na próxima mudança de regra, você precisa lembrar de todos os lugares onde colou.

javascript
// ❌ God Business Rule: valida, calcula, roteia e chama API externa — tudo junto
(function executeRule(current, previous) {

    if (!current.short_description.toString()) {
        current.setAbortAction(true);
        gs.addErrorMessage("Short description is required");
        return;
    }

    var impact  = parseInt(current.getValue("impact")  || "3");
    var urgency = parseInt(current.getValue("urgency") || "3");
    var matrix = [[1,2,3],[2,3,4],[3,4,4]];
    current.setValue("priority", matrix[impact-1][urgency-1]);

    var grp = new GlideRecord("sys_user_group");
    grp.addQuery("u_category", current.getValue("category"));
    grp.setLimit(1); grp.query();
    if (grp.next()) current.setValue("assignment_group", grp.getUniqueValue());

    // chamada REST síncrona — o usuário espera o servidor externo responder
    var rm = new sn_ws.RESTMessageV2("MonitoringSystem", "createAlert");
    rm.setStringParameter("incident_id", current.number.toString());
    rm.execute();

})(current, previous);

O padrão documentado pela própria ServiceNow separa as responsabilidades: lógica de negócio vive em Script Include, Business Rule é orquestrador. O BR sabe o contexto — current, previous, tipo de operação — e chama o método certo. O Script Include contém a lógica de verdade, pode ser testado isoladamente e reaproveitado por qualquer artefato da plataforma.

javascript
// === Script Include: IncidentPriorityCalculator ===
var IncidentPriorityCalculator = Class.create();
IncidentPriorityCalculator.prototype = {
    initialize: function() {
        this._matrix = {
            "1_1": 1, "1_2": 2, "1_3": 3,
            "2_1": 2, "2_2": 3, "2_3": 4,
            "3_1": 3, "3_2": 4, "3_3": 4
        };
    },
    calculate: function(impact, urgency) {
        var key = parseInt(impact) + "_" + parseInt(urgency);
        return this._matrix[key] || 4;
    },
    type: "IncidentPriorityCalculator"
};

// === BR "Validate Incident" — Before — Insert ===
(function executeRule(current, previous) {
    if (!current.short_description.toString()) {
        current.setAbortAction(true);
        gs.addErrorMessage("Short description is required");
    }
})(current, previous);

// === BR "Set Priority and Route" — Before — Insert/Update ===
(function executeRule(current, previous) {
    if (!current.impact.changesTo() && !current.urgency.changesTo()) return;

    var calc = new IncidentPriorityCalculator();
    current.setValue("priority",
        calc.calculate(current.getValue("impact"), current.getValue("urgency")));
})(current, previous);

// === BR "Notify External Monitoring" — ASYNC — Insert ===
(function executeRule(current, previous) {
    if (current.getValue("priority") !== "1") return;
    try {
        var rm = new sn_ws.RESTMessageV2("MonitoringSystem", "createAlert");
        rm.setStringParameter("incident_id", current.number.toString());
        rm.setHttpTimeout(15000);
        rm.executeAsync(); // fire-and-forget, o usuário não espera
    } catch (e) {
        gs.error("MonitoringBR: Failed to notify: " + e);
    }
})(current, previous);

Três BRs, cada um com uma responsabilidade só, e a chamada REST foi para Async — que é onde ela sempre deveria ter estado.

#detalhes da API GlideRecord que não são pedantismo

Tem um punhado de comportamentos da API que parecem detalhe até o dia em que geram um bug em produção, com dado real, numa sexta às 17h:

  • getValue() vs getDisplayValue(): para Reference, getValue() devolve o sys_id; getDisplayValue() devolve o texto configurado. Para Boolean, getValue() devolve "true"/"false" como string — não o booleano.
  • setValue() em After não persiste sozinho: em Before, o valor é gravado automaticamente junto com o resto do registro. Em After, o save já aconteceu — sem gr.update() explícito, a mudança fica só na memória.
  • changesTo() e changesFrom(): current.state.changesTo("3") verifica se o campo mudou para aquele valor nesta operação específica. Use os dois juntos para pegar transições de estado exatas, e para evitar que o BR rode em todo update irrelevante.
  • isNewRecord(): distingue Insert de Update sem precisar de uma condição adicional em Before BR.
  • setAbortAction(true): cancela o save — mas só funciona em Before. Em After, o registro já foi gravado; não tem volta sem update manual.

#dois espelhos de frente um para o outro

Um Business Rule After que atualiza o próprio registro onde foi disparado pode entrar em loop. É como dois espelhos de frente um para o outro: a imagem se repete infinitamente até alguma proteção interromper. O ServiceNow tem proteção nativa para alguns cenários — não para todos. Proteção explícita é trabalho do desenvolvedor.

javascript
// �
 prevenção de recursão com flag de controle
// BR "Sync Parent Record" — After — Update — tabela x_acme_task
(function executeRule(current, previous) {

    var flagKey = "x_acme.sync.processing_" + current.sys_id;
    if (gs.getProperty(flagKey) === "true") return;

    if (!current.state.changesTo() && !current.assigned_to.changesTo()) return;

    var parentId = current.getValue("u_parent_task");
    if (!parentId) return;

    gs.setProperty(flagKey, "true"); // levanta a flag antes do update

    try {
        var parent = new GlideRecord("x_acme_task");
        if (parent.get(parentId)) {
            parent.setValue("u_last_child_update", new GlideDateTime());
            parent.update();
        }
    } finally {
        // sempre abaixa a flag — sem isso, um erro deixa a flag levantada para sempre
        gs.setProperty(flagKey, "false");
    }

})(current, previous);

O finally não é estética. Sem ele, um erro no meio do bloco deixa a flag travada em "true" para sempre, e o Business Rule para de rodar silenciosamente — o tipo de bug que ninguém percebe até alguém perguntar por que o registro pai parou de sincronizar semanas atrás.

#performance não é opcional aqui

Business Rule roda dentro da transação do usuário. Um BR lento não é "ficou devagar para mim" — é "ficou devagar para todo mundo que está usando esse formulário agora". Algumas regras sem exceção:

javascript
// �
 BR otimizado: sai cedo, e usa GlideAggregate para contar
(function executeRule(current, previous) {

    var relevantes = ["priority", "impact", "urgency", "business_service"];
    var mudou = relevantes.some(function(campo) {
        return current[campo].changesTo();
    });
    if (!mudou) return;

    var priority = current.getValue("priority");
    current.setValue("u_sla_required", (priority === "1" || priority === "2"));

    var ga = new GlideAggregate("contract_sla");
    ga.addQuery("task", current.sys_id.toString());
    ga.addQuery("active", true);
    ga.addAggregate("COUNT");
    ga.query(); ga.next();
    current.setValue("u_active_sla_count", parseInt(ga.getAggregate("COUNT")));

})(current, previous);

A saída rápida no início (if (!mudou) return;) é tão importante quanto o GlideAggregate no fim — sem ela, esse BR roda em todo save do registro, mesmo quando nenhum campo relevante mudou.

#e agora?

  • Na sua instância de dev, abra os Business Rules de alguma tabela custom movimentada e conte quantas responsabilidades cada um tem. Se algum faz validação, cálculo e integração no mesmo script, esse é seu primeiro candidato a virar Script Include + orquestrador.
  • Procure BRs Before ou After chamando RESTMessageV2 de forma síncrona — é o sintoma mais direto de chamada que deveria estar em Async.
  • Este artigo puxa o fio de modelo de dados: os mesmos campos que você modela certo ali são os que o changesTo() vai testar aqui.
LinkedIn X/Twitter WhatsApp ☕ me paga um café