artigo

client script é o último recurso: a hierarquia que evita o erro clássico do onChange

UI Policy, Data Policy e g_scratchpad resolvem a maioria dos casos antes de um Client Script — e os dois erros de onChange que estão em todo projeto.

5 min de leitura intermediário servicenow client-scripts ui-policy

Todo projeto ServiceNow chega num momento em que alguém precisa que um campo mude de comportamento em tempo real: esconder aqui, obrigar ali, calcular um valor derivado sem o usuário perceber. E a resposta mais rápida que vem à cabeça é sempre a mesma — "faz um Client Script". Às vezes é mesmo a resposta certa. Na maioria das vezes, não é.

a régua que ninguém consulta

Client Script roda no navegador do usuário. Isso é uma vantagem — reação instantânea, sem viagem ao servidor — e também é o motivo de virar fonte de comportamento inconsistente: um script que funciona lindamente no formulário clássico pode simplesmente não existir quando o mesmo registro chega por REST, import ou portal. Regra prática: use a ferramenta menos poderosa que resolve o problema, porque poder demais sem necessidade é o jeito mais eficiente de criar manutenção futura.

ferramentaonde rodaquando é a escolha certaonde ela falha
UI Policycliente, sem códigomostrar/esconder, obrigatório/opcional, somente leitura com condição simplesnão cobre lógica condicional complexa; não vale para API
Data Policyservidor + clienteobrigatoriedade ou somente-leitura que precisa valer em TODO ponto de entradasó cobre mandatory/read-only, não show/hide
Client Script onLoadcliente, JavaScriptestado inicial do form que UI Policy não expressaroda em todo carregamento — script pesado atrasa a abertura do form
Client Script onChangecliente, JavaScriptreagir em tempo real à mudança de um campo específicoprecisa tratar isLoading, senão duplica a execução
GlideAjaxassíncrono, cliente → servidorbuscar dado que não veio no form carregadonunca use a versão síncrona — trava o navegador inteiro

Repare que essa tabela responde uma pergunta diferente da que o artigo sobre precedência de campos responde. Lá o assunto é "quando duas regras conflitam, quem vence". Aqui o assunto é anterior a isso: qual ferramenta você deveria pegar primeiro, antes de qualquer conflito existir.

Na prática isso significa: se o pedido é só "esconder este campo quando o status for X", a resposta é UI Policy, zero linhas de JavaScript, sobrevive a upgrade sem revisão. Client Script entra quando o comportamento não é expressável de forma declarativa — cálculo, chamada a outro sistema, reação a um evento que UI Policy não cobre.

os dois erros que aparecem em todo projeto

Assumindo que Client Script é mesmo necessário, tem dois erros de onChange que eu já vi em praticamente toda instância que passei a mão. O primeiro é não tratar isLoading: o parâmetro existe justamente para dizer se a mudança de valor está acontecendo por causa do carregamento do form ou de uma ação real do usuário, e ignorá-lo faz a lógica rodar duas vezes — uma ao abrir o registro, outra quando o campo muda de verdade. O segundo é usar o GlideAjax de forma síncrona — ou seja, getXMLWait() —, que trava a interface inteira até a resposta do servidor voltar. É a variante que a ServiceNow já trata como legada, e que nem funciona no Service Portal. Vale procurar por esse nome de método no código da sua instância: onde ele aparecer, provavelmente há uma tela que congela sem ninguém saber por quê.

javascript
// ❌ os dois erros clássicos juntos
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  var ga = new GlideAjax('CategoryGroupMapper');
  ga.addParam('sysparm_name', 'getGroupForCategory');
  ga.addParam('sysparm_category', newValue);
  ga.getXMLWait(); // síncrono: bloqueia o navegador até a resposta chegar

  var xml = ga.getXMLDocument();
  var result = xml.getElementsByTagName('result')[0].textContent;
  g_form.setValue('assignment_group', result);
}
javascript
// �
 tratando isLoading/isTemplate e usando callback assíncrono
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || isTemplate || !newValue) return;

  var ga = new GlideAjax('CategoryGroupMapper');
  ga.addParam('sysparm_name', 'getGroupForCategory');
  ga.addParam('sysparm_category', newValue);
  ga.getXMLAnswer(function (answer) {
    // o usuário pode continuar usando o form enquanto espera
    g_form.setValue('assignment_group', answer || '');
  });
}

O isTemplate costuma passar batido — ele avisa quando o valor mudou porque um template foi aplicado, não porque o usuário digitou algo, e nem sempre você quer disparar a mesma lógica nos dois casos. Vale decidir isso explicitamente, não por acidente.

Do lado do servidor, o Script Include chamado via GlideAjax precisa herdar de AbstractAjaxProcessor e estar marcado como "Client callable". É tentador colocar a lógica direto ali dentro sem pensar duas vezes, mas o mesmo cuidado de sempre se aplica: parâmetro vindo do cliente é input não confiável, então valide antes de usar em qualquer query.

javascript
var CategoryGroupMapper = Class.create();
CategoryGroupMapper.prototype = Object.extendsObject(AbstractAjaxProcessor, {
  getGroupForCategory: function () {
    var category = this.getParameter('sysparm_category');
    if (!category || category.length > 100) return '';

    var gr = new GlideRecord('x_acme_itsm_category_routing');
    gr.addQuery('u_category', category);
    gr.addQuery('active', true);
    gr.setLimit(1);
    gr.query();
    return gr.next() ? gr.getValue('u_assignment_group') : '';
  },
  type: 'CategoryGroupMapper',
});

quando o GlideAjax nem precisa existir

Tem um atalho que resolve boa parte dos casos de "preciso de um dado do servidor assim que o form abre" sem nenhuma chamada assíncrona adicional: o g_scratchpad, populado por uma Business Rule de timing Display. A ideia é inverter a ordem — em vez do Client Script pedir o dado depois que o form já carregou, o servidor deposita esse dado no objeto antes do HTML sequer chegar ao navegador. É a diferença entre ligar para alguém no meio da reunião pedindo um número, e essa pessoa já ter deixado o número escrito na sua agenda antes da reunião começar.

javascript
// Business Rule "Populate Scratchpad" — timing Display — tabela incident
(function executeRule(current, previous) {
  g_scratchpad.is_change_manager = gs.hasRole('x_acme_itsm.change_manager');

  var svc = current.getValue('business_service');
  if (svc) {
    var gr = new GlideRecord('cmdb_ci_service');
    if (gr.get(svc)) {
      g_scratchpad.service_tier = gr.getValue('u_service_tier');
    }
  }
})(current, previous);
javascript
// Client Script onLoad — sem nenhum GlideAjax adicional
function onLoad() {
  if (g_scratchpad.is_change_manager) {
    g_form.setMandatory('u_change_rationale', true);
  }
  if (g_scratchpad.service_tier === '1') {
    g_form.setSectionDisplay('Escalation', true);
  }
}

e agora?

  • Se você tem um Client Script onChange em produção sem verificação de isLoading, esse é o teste de cinco minutos mais valioso da semana: abra o form, veja se a lógica dispara duas vezes no carregamento.
  • Antes do próximo GlideAjax síncrono, troque por getXMLAnswer() com callback — a mudança costuma ser pequena e o ganho de responsividade é imediato.
  • Quem quiser entender por que um campo "obrigatório" às vezes não obriga nada, o artigo sobre precedência de campos mostra a ordem de investigação entre ACL, Data Policy, UI Policy e Client Script.
LinkedIn X/Twitter WhatsApp ☕ me paga um café