Na Vetra, a distribuidora fictícia que mantenho como demonstração do portfólio, uma conta grande ficou trinta e dois dias sem recompra e ninguém percebeu. O dado estava lá, num campo de risco que o Salesforce calculava sozinho, mas ele só aparecia para quem abrisse aquela conta específica. O gerente da carteira não abriu, porque não tinha motivo para abrir uma conta que, na cabeça dele, estava bem. A informação existia e mesmo assim o cliente esfriou. O problema não era falta de dado, era o dado esperando alguém ir até ele.
Para enviar uma notificação proativa no Salesforce, você usa Messaging.CustomNotification
no Apex: define título, corpo, o CustomNotificationType e o setTargetId que leva o
clique ao registro, e chama send() com o conjunto de destinatários. Dispare só na transição de
estado para não virar spam, e agende o resumo periódico com uma classe Schedulable. A org deixa de
esperar e passa a avisar a pessoa certa no momento certo.
Por que o dado certo no lugar errado não muda decisão
O Salesforce é ótimo em calcular. Um campo de fórmula, um Flow agendado ou uma classe Apex derivam o status de risco de uma conta, o SLA estourado de um caso, a oportunidade que passou do prazo de fechamento, e gravam isso num campo limpo e correto. O buraco não é o cálculo, é a entrega. Esse campo vive dentro do registro, e o registro só se abre quando alguém tem motivo para abri-lo. Quem tem motivo para abrir uma conta que parece saudável? Ninguém, até ela deixar de estar saudável, e aí já passou o momento de agir.
A saída preguiçosa é montar um relatório com todas as contas em risco e torcer para alguém olhar toda manhã. Não olham. Relatório é consulta ativa: exige que a pessoa lembre de ir lá, filtre, interprete. A automação proativa inverte a direção. Em vez de esperar a pessoa ir ao dado, o dado vai à pessoa, no canal que ela já olha o dia inteiro, com um clique que abre o registro que precisa de atenção.
O mínimo que dispara o alerta: Messaging.CustomNotification
A ferramenta nativa para isso é a Custom Notification, a mesma que alimenta o sino no canto superior do Lightning e a notificação push no app mobile. Do Apex, ela cabe em poucas linhas. No rebuy-radar, o projeto de onde tirei este exemplo, a classe que avisa o dono da conta quando ela entra em risco crítico é esta:
public with sharing class RebuyRiskNotifier {
public static void avisar(Account conta, Id donoId) {
Messaging.CustomNotification n = new Messaging.CustomNotification();
n.setTitle('Conta em risco crítico: ' + conta.Name);
n.setBody('A recompra prevista venceu há mais de 30 dias. Abra e acione o cliente.');
n.setNotificationTypeId(tipoId()); // o CustomNotificationType
n.setSenderId(UserInfo.getUserId());
n.setTargetId(conta.Id); // o clique abre ESTA conta
n.send(new Set<String>{ donoId }); // destinatário: o dono da carteira
}
}
Cada linha carrega uma decisão. O setTitle e o setBody são o que a pessoa lê antes de
decidir se clica, então precisam dizer o que houve e o que fazer, não um genérico "atualização na conta". O
setTargetId é o detalhe que separa um alerta útil de um alerta irritante: ele define o registro
que abre no clique. Sem ele, a pessoa recebe o aviso, entende que algo aconteceu e ainda precisa caçar qual
conta era. Com ele, um toque já a coloca na tela certa. O send() recebe um Set de
Ids, e é bom que seja um conjunto: notificar em lote é o caso comum, e passar o Set inteiro de uma vez respeita
os limites de governança em vez de chamar send() num laço.
O CustomNotificationType é metadado, não configuração de tela
O setNotificationTypeId pede o Id de um CustomNotificationType, e aqui mora uma
escolha de arquitetura que muita gente erra. Dá para criar esse tipo pela interface, em Setup, apontando e
clicando. O problema é que aí ele vira uma configuração órfã que existe só naquela org: você faz o deploy da
classe Apex, ela procura o tipo pelo nome, não encontra na sandbox de destino e estoura em produção. O tipo de
notificação é metadado como qualquer outro, com o sufixo .notiftype-meta.xml, e o lugar dele é o
repositório, versionado junto com a classe que o usa.
No código, você nunca chumba o Id do tipo, porque ele muda de org para org. Busca pelo
DeveloperName, que é estável, e guarda o resultado numa variável estática para não repetir a query
a cada chamada dentro da mesma transação:
private static Id tipoCache;
private static Id tipoId() {
if (tipoCache == null) {
tipoCache = [SELECT Id FROM CustomNotificationType
WHERE DeveloperName = 'Rebuy_Risk_Alert' LIMIT 1].Id;
}
return tipoCache;
}
Esse cuidado de tratar a configuração como código, e não como clique numa tela, é o mesmo princípio que defendo no post sobre versionar relatório e dashboard no Git: o que sustenta uma automação de produção precisa de histórico, diff e deploy repetível, senão você descobre que quebrou só quando o cliente reclama.
Anti-fadiga: notificar na transição, não a cada salvamento
Aqui está o erro que transforma uma boa ideia num filtro de e-mail que todo mundo cria para mandar o alerta direto para a lixeira. Se você dispara a notificação sempre que o registro está na condição crítica, um mesmo caso que continua crítico por cinco dias gera cinco alertas, um a cada vez que qualquer campo da conta é salvo. No terceiro, a pessoa já parou de ler. O alerta que grita toda hora tem exatamente o mesmo efeito de não ter alerta nenhum.
A regra é notificar no evento, não no estado. O evento é a transição: o instante em que a conta passa de saudável para crítica, não o fato de ela estar crítica. Num trigger, isso é comparar o valor novo com o antigo e agir só quando a fronteira foi cruzada:
// trigger after update em Account
for (Account nova : Trigger.new) {
Account antiga = Trigger.oldMap.get(nova.Id);
Boolean entrouEmRisco = nova.Rebuy_Status__c == 'Critico'
&& antiga.Rebuy_Status__c != 'Critico';
if (entrouEmRisco) {
RebuyRiskNotifier.avisar(nova, nova.OwnerId);
}
}
A conta que já estava crítica e continua crítica não passa no entrouEmRisco, então não gera nada.
Só o cruzamento da fronteira dispara. Esse mesmo raciocínio de agir na mudança de status, e não na leitura
repetida, é o que uso para medir tempo em cada etapa no post sobre
como medir SLA e tempo em cada status: transição é o
evento que importa, o resto é ruído.
O digest semanal: quando o resumo vale mais que o alerta
Nem toda informação merece interromper. O dono da conta precisa saber na hora que ela virou crítica, porque cada dia conta. O gerente que supervisiona doze vendedores não precisa de doze interrupções ao longo da semana; precisa de uma foto no início da segunda-feira: quantas contas da equipe dele estão em risco agora e para quem cobrar. Esse é o trabalho de uma classe Schedulable, que roda no horário que você agenda e monta um resumo em vez de reagir a eventos soltos.
public with sharing class RebuyManagerDigest implements Schedulable {
public void execute(SchedulableContext sc) {
Map<Id, Integer> porGestor = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT Owner.ManagerId gestor, COUNT(Id) qtd
FROM Account
WHERE Rebuy_Status__c = 'Critico' AND Owner.ManagerId != null
GROUP BY Owner.ManagerId
]) {
porGestor.put((Id) ar.get('gestor'), (Integer) ar.get('qtd'));
}
for (Id gestorId : porGestor.keySet()) {
Messaging.CustomNotification n = new Messaging.CustomNotification();
n.setTitle('Resumo de risco: ' + porGestor.get(gestorId) + ' contas críticas');
n.setBody('Sua equipe tem contas paradas na recompra. Abra o painel e priorize a semana.');
n.setNotificationTypeId(tipoId());
n.setTargetId(gestorId);
n.send(new Set<String>{ gestorId });
}
}
}
O agendamento é uma linha, uma vez, no Developer Console ou num script de setup, com a expressão cron que define segunda às oito da manhã:
System.schedule('Rebuy Digest Semanal', '0 0 8 ? * MON', new RebuyManagerDigest());
Repare no contraste que faz a coisa funcionar: o dono recebe um alerta pontual, na transição, porque a urgência
é individual e imediata; o gestor recebe um agregado semanal, porque a decisão dele é de alocação, não de
pronto-atendimento. A mesma Custom Notification serve os dois, o que muda é a cadência. Errar isso, mandando
para o gestor cada transição de cada conta, é a receita mais rápida de treinar todo mundo a ignorar o sino. E
como o digest roda numa classe Schedulable que varre a org inteira, vale conhecer os
governor limits do Apex antes de soltar: um
AggregateResult bem escrito resolve em uma query o que um laço ingênuo estouraria.
Testar o que o send() não deixa consultar
Chega o momento de escrever o teste e você descobre o problema: o send() não grava um registro que
você possa buscar por SOQL depois. Não existe um objeto "Notificação Enviada" para você fazer
SELECT e afirmar "saiu para o dono certo". Em contexto de teste a chamada simplesmente não entrega
nada, e você fica sem como provar que a lógica está correta olhando a org. O instinto errado é desistir da
cobertura ou testar só que o método não estoura, o que não garante nada sobre quem seria notificado.
A saída é abrir um seam no código: uma variável estática marcada com @TestVisible que guarda o
conjunto de destinatários calculado logo antes de chamar send(). Em produção ela não faz
diferença; no teste, ela é o que você inspeciona.
public with sharing class RebuyRiskNotifier {
@TestVisible private static Set<String> ultimoEnvio;
public static void avisar(Account conta, Id donoId) {
Set<String> destinatarios = new Set<String>{ donoId };
ultimoEnvio = destinatarios; // seam: o teste lê isto
Messaging.CustomNotification n = new Messaging.CustomNotification();
n.setTitle('Conta em risco crítico: ' + conta.Name);
n.setNotificationTypeId(tipoId());
n.setTargetId(conta.Id);
n.send(destinatarios);
}
}
O teste então dispara a condição real, uma conta que cruza para crítica, e valida o que importa: que o dono certo entrou no conjunto e que uma conta que já estava crítica não gerou envio nenhum.
@IsTest
static void avisaSomenteNaTransicaoParaCritico() {
Account c = new Account(Name = 'Cliente Teste', Rebuy_Status__c = 'Saudavel');
insert c;
Test.startTest();
c.Rebuy_Status__c = 'Critico'; // cruza a fronteira
update c;
Test.stopTest();
System.assertEquals(1, RebuyRiskNotifier.ultimoEnvio.size(), 'deveria notificar 1 destinatario');
System.assert(RebuyRiskNotifier.ultimoEnvio.contains(c.OwnerId), 'deveria notificar o dono');
}
O seam não é uma gambiarra, é a forma honesta de testar um efeito colateral que a plataforma não expõe. O mesmo padrão aparece sempre que a API grava algo que você não pode ler de volta, e deixa o teste afirmando a regra de negócio real, não um detalhe de implementação.
Custom Notification ou Platform Event: qual mecanismo
Custom Notification não é o único jeito de a org avisar sobre algo, e escolher errado gera retrabalho. A pergunta que decide é simples: você quer avisar uma pessoa, ou desacoplar quem gera o fato de quem reage a ele?
| Você quer | Custom Notification | Platform Event |
|---|---|---|
| Alvo | uma pessoa (sino, mobile) | qualquer assinante |
| Clique abre registro | sim, via setTargetId | não por si só |
| Atualiza tela ao vivo | não | sim, LWC com empApi |
| Integra com sistema externo | não | sim, é um barramento |
| Esforço para o caso simples | baixo | médio |
A regra prática que sigo: se o objetivo é um humano tomar uma ação sobre um registro, Custom Notification resolve com menos peça móvel. Se o objetivo é um painel que reage sozinho, ou um segundo sistema que escuta o mesmo fato, o caminho é o dos alertas em tempo real com Platform Events, que desacoplam a geração da reação. Os dois convivem bem: no rebuy-radar, a transição para risco publica um Platform Event que acende o heatmap ao vivo no cockpit e, no mesmo ponto, dispara a Custom Notification que bate no sino do dono. Um serve à tela, o outro serve à pessoa.
Quando não vale automatizar o aviso
Honestidade antes de vender complexidade: nem todo dado pede notificação, e encher o sino de alertas de baixa relevância mata a confiança em todos eles, inclusive nos que importam. Se um fato não muda o que a pessoa faria nos próximos dias, ele não merece interromper. Uma conta que virou crítica e alguém precisa ligar hoje, merece. Uma mudança de campo que só interessa no fechamento do mês, não; essa vive melhor num relatório que a pessoa consulta quando for a hora, sem toque no sino.
Vale também medir o volume antes de montar tudo. Se a sua operação gera duas ou três situações críticas por semana, você não precisa de trigger, Schedulable e tipo de notificação versionado; um alerta de e-mail simples já resolve. A engenharia deste post se paga quando o volume é alto e a lógica de quando notificar é sutil, com transição, anti-fadiga e destinatário dinâmico. Comece pela dor real: qual é o aviso que, se chegasse na hora, teria evitado um problema no mês passado? Automatize esse primeiro e só então expanda. O objetivo não é notificar tudo, é fazer a org falar exatamente quando calar sairia caro.
Sua operação descobre o problema tarde demais, quando já virou cliente perdido?
Eu desenho a automação proativa da sua org: Custom Notification na transição que importa, digest agendado para quem supervisiona, tipo de notificação versionado e teste cobrindo a regra de quando avisar. Sua equipe recebe o alerta certo no canal que ela já olha. Comece com um diagnóstico gratuito de 45 minutos.
Falar no WhatsApp Ver serviçosPerguntas frequentes
Como enviar uma notificação proativa no Salesforce com Apex?
Você instancia Messaging.CustomNotification no Apex, define título e corpo com
setTitle e setBody, aponta o tipo com setNotificationTypeId (o Id de um
CustomNotificationType), define o registro de destino com setTargetId e chama
send() passando um Set com os Ids dos destinatários. A notificação aparece no sino do
Lightning e no app mobile, e o clique abre o registro que você marcou como alvo.
Qual a diferença entre Custom Notification e Platform Event no Salesforce?
Custom Notification entrega uma mensagem direta para pessoas específicas no sino e no mobile, com um alvo clicável. Platform Event é um barramento de mensagens entre sistemas e telas: você publica um evento e qualquer assinante reage, inclusive um LWC ao vivo. Use Custom Notification quando o objetivo é avisar um humano; use Platform Event quando quer desacoplar quem gera de quem reage, sem saber quem escuta.
Como testar uma Custom Notification se o send() não é consultável?
O send() não grava nada que você possa buscar por SOQL, então o teste não consegue afirmar que a
notificação saiu olhando a org. A saída é criar um seam: uma variável estática marcada com
@TestVisible que guarda o conjunto de destinatários calculado logo antes do send(). O
teste chama o método, lê essa variável e valida quem seria notificado e em qual condição, sem depender do envio
real.
Como evitar que a notificação proativa vire spam para o usuário?
Dispare no evento, não no estado. Notifique apenas na transição para a condição crítica, comparando
Trigger.new com Trigger.oldMap, para que um mesmo registro que continua em risco não
gere alerta a cada salvamento. Para o acompanhamento contínuo, troque o alerta repetido por um resumo periódico
com uma classe Schedulable, uma notificação por gestor por semana em vez de uma por registro por dia.
