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.
#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:
| timing | quando executa | uso correto | armadilha clássica |
|---|---|---|---|
| Before | antes de gravar no banco, no mesmo thread do usuário | validação, cálculo de campo antes de salvar | query pesada ou chamada REST aqui trava o usuário no spinner |
| After | depois de gravar, ainda no thread do usuário | atualizar registro relacionado, disparar evento | BR After que atualiza outro registro que aciona outro BR After... |
| Async | fila separada, processada por um worker | REST, e-mail, qualquer coisa que pode demorar ou falhar | tratar current como estado atual — ele foi capturado no momento do trigger e pode estar desatualizado |
| Display | no carregamento do formulário (GET) | popular g_scratchpad para o Client Script usar sem round-trip extra | tentar 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.
// ❌ 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.
// === 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()vsgetDisplayValue(): para Reference,getValue()devolve osys_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 — semgr.update()explícito, a mudança fica só na memória.changesTo()echangesFrom():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.
// �
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:
// �
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
RESTMessageV2de 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.