AI

Crie um agente de pesquisa de contas no Salesforce Agentforce com Bright Data

A pesquisa de contas antes de uma ligação com o cliente costuma levar de dez a quinze minutos por representante de vendas. Aprenda a usar o Bright Data com
37 min de leitura
Build an account research agent in Salesforce Agentforce with Bright Data

A pesquisa de conta antes de uma ligação com o cliente geralmente leva dez ou quinze minutos por representante de vendas. O fluxo de trabalho é majoritariamente manual: o representante sai do Salesforce, abre o Google, percorre várias abas e cola os resultados em um campo de notas. A maior parte do trabalho é busca e síntese.

O Web Unlocker da Bright Data retorna Markdown limpo da maioria das URLs públicas. Integrá-lo ao Salesforce Agentforce permite que um representante obtenha pesquisas de conta a partir de um prompt no chat, com fontes atribuídas, sem sair do Salesforce. Por baixo dos panos, a construção consiste em um subagente do Agentforce, três classes Apex e um pequeno proxy Cloudflare Worker.

TL;DR

  • Um subagente do Agentforce recebe um prompt em linguagem natural de um representante de vendas, chama o Bright Data Web Unlocker e retorna um briefing de conta com fontes atribuídas, tudo dentro do chat do Salesforce.
  • O cliente HTTP do Apex falha silenciosamente em respostas com transferência em chunks acima de alguns kilobytes (verificado na API v66.0), então a integração é roteada por um pequeno Cloudflare Worker que armazena em buffer e reserve com um cabeçalho Content-Length explícito.
  • A interface Canvas do Agentforce oculta o sinalizador YAML is_user_input: True necessário para que agentes orientados por chat recebam entradas extraídas do prompt. A correção está no modo Script.
  • O padrão atual de External Credential do Salesforce se divide em três objetos (External Credential, Named Credential, Permission Set), com uma caixa de seleção fácil de ignorar que retorna 401 quando não marcada.
  • O Agentforce redige URLs externas das respostas do agente por padrão. O agente as lê internamente, mas não as exibe a menos que o domínio esteja na lista de permissões de URLs Confiáveis.
  • O footprint total é de cerca de 6 KB de Apex, um Cloudflare Worker, três objetos de credencial do Salesforce e um subagente. Cada bloco de código foi testado em uma org Salesforce Developer Edition ativa.

Antes de começar

Você precisará de quatro contas e ferramentas, todas gratuitas para este tutorial:

  • Uma conta Bright Data com pelo menos uma zona Web Unlocker provisionada. Novas contas incluem créditos de avaliação gratuitos, e o volume de requisições do tutorial se encaixa confortavelmente dentro desses créditos.
  • Uma conta Cloudflare para o proxy Worker. Não é necessário cartão de crédito para o plano gratuito; você escolherá um subdomínio workers.dev no primeiro uso.
  • Uma org Salesforce Developer Edition com Agentforce habilitado. As orgs Developer Edition recentes já vêm com Agentforce, Data Cloud e o Agentforce Studio pré-habilitados. Verifique se o app Agentforce Studio aparece no seu App Launcher antes de continuar além da Parte 5; se não aparecer, você tem uma org sem Agentforce e as partes seguintes não funcionarão.
  • Uma forma de enviar uma requisição HTTP de teste. A Parte 2 inclui um comando curl para verificar o Worker. macOS, Linux e Windows 11 já vêm com curl. Se preferir usar uma interface gráfica, Postman ou Insomnia funcionam com os mesmos cabeçalhos e corpo. Se você não tiver nenhum desses e não quiser instalar, pode pular o teste standalone do Worker e validar de ponta a ponta pelo Salesforce na Parte 3.
  • Perfil de Administrador do Sistema (ou um com Author Apex, Modify All Data e Customize Application). Uma org Developer Edition recém-criada já concede isso automaticamente. Se você estiver trabalhando em um sandbox corporativo com perfil restrito, use uma org Developer Edition nova.

O que você vai construir

Você vai construir um agente Agentforce chamado Account Briefing Agent. Um representante digita uma pergunta em linguagem natural. O agente roteia o prompt para um subagente personalizado, escolhe as ferramentas certas, chama a Bright Data por meio de um proxy Cloudflare Worker simples, sintetiza um briefing de conta com fontes atribuídas e publica o briefing no chat. A arquitetura tem cinco componentes:

  • Bright Data Web Unlocker como o primitivo de dados web. É um único endpoint que aceita a maioria das URLs e retorna Markdown limpo.
  • Cloudflare Worker como proxy entre o Salesforce e a Bright Data. O plano gratuito cobre uma equipe pequena.
  • Salesforce External Credential + Named Credential + Permission Set como camada de autenticação.
  • Apex com três classes: um serviço compartilhado, mais dois wrappers usando @InvocableMethod (a anotação que os torna chamáveis pelo Agentforce, um por Agent Action).
  • Subagente Agentforce com duas ações anexadas, um bloco de instruções e uma descrição de classificação.

Visão geral da arquitetura

Aqui está o fluxo de requisição, do prompt do representante até o briefing:

Rep prompt in Agentforce
        │
        ▼
Agent Router  ──►  Account Web Intelligence subagent
                          │
                          ├─► Apex: BrightDataNewsAction
                          │     └─► Named Credential → Cloudflare Worker → Web Unlocker → Google News
                          │
                          └─► Apex: BrightDataFetchAction
                                └─► Named Credential → Cloudflare Worker → Web Unlocker → target URL
        │
        ▼
LLM synthesis  ──►  Briefing back to the rep

O Cloudflare Worker existe porque o Salesforce Apex não consegue consumir de forma confiável respostas HTTP/1.1 com transferência em chunks, que é o que a Bright Data retorna para qualquer payload não trivial. O Worker armazena a resposta em um único ArrayBuffer e a reserve com um cabeçalho Content-Length explícito. Sem ele, cada chamada do Apex retorna um status 200 e um corpo de zero bytes. A Parte 2 abaixo detalha o diagnóstico e a correção.

A Bright Data tem vários produtos adequados para esse tipo de construção: SERP API para resultados do Google analisados, scrapers dedicados para LinkedIn e Crunchbase, entre outros. Esta construção usa apenas o Web Unlocker porque funciona pelo mesmo endpoint para qualquer URL, o que mantém o lado Apex simples. O proxy Cloudflare Worker da Parte 2 cobre todos os endpoints da API da Bright Data igualmente, então substituir pelo SERP API ou um scraper dedicado posteriormente não altera a configuração do lado do Salesforce.

Parte 1: Configurar a Bright Data

Se você não tem uma conta Bright Data, crie uma na página de cadastro da Bright Data. A zona Web Unlocker que você usará está na seção Web Access API do painel.

Criar ou anotar a zona Web Unlocker

Abra o painel, vá para Web Access API na navegação à esquerda e confirme que existe uma zona Web Unlocker. Se sua conta não tiver uma, clique em Create API (canto superior direito) e escolha Unlocker API no menu suspenso. Dê qualquer nome (os nomes de zona não podem ser alterados após a criação, então escolha algo estável como agentforce_unlocker). Seja qual for o nome, anote-o. Você o inserirá na constante UNLOCKER_ZONE em BrightDataService.cls na Parte 4, e no teste curl da Parte 2.

Painel da Bright Data mostrando a lista de zonas da Web Access API

A zona Web Unlocker é o primitivo que ambas as ações do Agentforce utilizam.

Criar um token de API

Clique em Settings (canto inferior esquerdo) → aba Users and API keysAdd API key com permissão User. A chave é exibida uma única vez ao ser gerada e depois fica mascarada. Copie-a agora e guarde em um lugar seguro; você a colará no Salesforce na Parte 3.

Configurações de conta da Bright Data mostrando a seção de chaves de API com uma chave em status Ativo, permissão de Usuário e expiração Ilimitada.

Essa é toda a configuração da Bright Data.

Parte 2: Implantar o proxy Cloudflare Worker

Antes de configurar o Salesforce, você precisa de um proxy na frente da Bright Data. O motivo é uma limitação na forma como o Salesforce Apex lê respostas com transferência em chunks; qualquer desenvolvedor Apex que faça callouts HTTP não triviais provavelmente vai se deparar com isso.

O bug

O cliente Http do Salesforce Apex suporta HTTP padrão, com uma lacuna prática: ele não analisa de forma confiável respostas HTTP/1.1 que usam transferência em chunks sem cabeçalho Content-Length. Em uma resposta em chunks, o callout retorna Status Code = 200, Content-Type = null, Response Size = 0 bytes, sem exceção ou aviso. Tanto getBody() quanto getBodyAsBlob().toString() retornam strings vazias.

O endpoint /request da Bright Data usa transferência em chunks para respostas acima de alguns kilobytes. Uma chamada ao Web Unlocker em uma página de teste pequena (o welcome.txt da Bright Data) fica abaixo do limite e retorna uma resposta com content-length, que o Apex analisa corretamente. Mas uma página real (a página inicial de uma empresa, uma busca no Google News) ultrapassa o limite e usa chunks, e o Apex retorna um corpo vazio.

Duas coisas provam que o problema é do lado do Apex, não da rede: uma chamada curl para o mesmo endpoint com o mesmo payload retorna um corpo de 9 KB corretamente, e a mesma chamada via execução anônima do Apex retorna 0 bytes com Transfer-Encoding: chunked nos cabeçalhos de resposta.

A correção é estrutural, não uma mudança de configuração: coloque um proxy com buffer entre o Salesforce e a Bright Data. O proxy lê completamente o stream em chunks da Bright Data e depois reserve a resposta ao Salesforce com um cabeçalho Content-Length explícito. O Apex analisa essa resposta corretamente.

Um Cloudflare Worker é uma boa opção para hospedar esse proxy. É gratuito para baixo volume, implanta em minutos, roda na borda e o código inteiro cabe em uma tela de JavaScript.

Criar o Worker

Cadastre-se no painel da Cloudflare se ainda não tiver uma conta. No painel, encontre Workers (aparece em ComputeWorkers & Pages dependendo da versão do seu painel). Clique em Create application, depois escolha Hello World nos templates. No primeiro uso, a Cloudflare solicita que você escolha um subdomínio workers.dev; escolha qualquer um (é o seu subdomínio de desenvolvimento gratuito). Dê ao Worker um nome fácil de lembrar; esta construção usa bd-proxy. Após o placeholder ser implantado, clique em Edit code.

Selecione todo o código placeholder no editor e cole este em seu lugar:

/**
 * Bright Data to Salesforce Apex proxy.
 *
 * Salesforce Apex does not reliably consume HTTP/1.1 chunked-transfer
 * responses, which is what Bright Data returns for any non-trivial payload.
 * This Worker buffers the full response and re-serves it with an explicit
 * Content-Length header. Apex parses that response cleanly.
 *
 * Production deployments typically route external API calls through an
 * integration layer like MuleSoft or Heroku. This Worker is the minimal
 * stand-in for that role.
 */

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const bdUrl = 'https://api.brightdata.com' + url.pathname + url.search;

    // Strip Cloudflare-injected headers we shouldn't forward upstream.
    const forwardHeaders = new Headers(request.headers);
    forwardHeaders.delete('host');
    forwardHeaders.delete('cf-connecting-ip');
    forwardHeaders.delete('cf-ray');
    forwardHeaders.delete('cf-visitor');
    forwardHeaders.delete('x-forwarded-for');
    forwardHeaders.delete('x-forwarded-proto');
    forwardHeaders.delete('x-real-ip');

    try {
      const bdResponse = await fetch(bdUrl, {
        method: request.method,
        headers: forwardHeaders,
        body: ['GET', 'HEAD'].includes(request.method)
          ? undefined
          : await request.arrayBuffer(),
      });

      // Buffer the entire response into a single ArrayBuffer. This collapses
      // chunked transfer into a buffer of known length.
      const bodyBuffer = await bdResponse.arrayBuffer();

      const responseHeaders = new Headers();
      const ct = bdResponse.headers.get('Content-Type');
      if (ct) responseHeaders.set('Content-Type', ct);
      responseHeaders.set('Content-Length', bodyBuffer.byteLength.toString());
      const brdStatus = bdResponse.headers.get('x-brd-status-code');
      if (brdStatus) responseHeaders.set('X-Brd-Status-Code', brdStatus);

      return new Response(bodyBuffer, {
        status: bdResponse.status,
        headers: responseHeaders,
      });
    } catch (err) {
      return new Response(
        JSON.stringify({ error: 'Proxy error', message: err.message }),
        { status: 502, headers: { 'Content-Type': 'application/json' } }
      );
    }
  },
};

As duas linhas que corrigem a integração são await bdResponse.arrayBuffer() (que lê todo o stream em chunks para a memória) e o cabeçalho Content-Length explícito definido a partir de bodyBuffer.byteLength (o Apex analisa o corpo corretamente a partir disso). Todo o restante trata do encaminhamento de cabeçalhos: remove os cabeçalhos injetados pela Cloudflare nas requisições de entrada e preserva o código de status upstream nas respostas de saída.

Clique em Deploy (canto superior direito). A Cloudflare fornece uma URL como https://<worker-name>.<your-subdomain>.workers.dev. Copie-a; você precisará dessa URL para o Salesforce na Parte 3.

Editor de código do Cloudflare Worker mostrando o arquivo worker.js do bd-proxy, com a lógica de buffer (`const bodyBuffer = await bdResponse.arrayBuffer()`) e a reescrita do cabeçalho Content-Length visíveis. O indicador de status mostra que o Worker está Ativo

As linhas que importam: await bdResponse.arrayBuffer() lê todo o stream em chunks para a memória, e o cabeçalho Content-Length explícito no objeto de resposta significa que o Apex consegue analisar o corpo corretamente.

Verificar se o Worker funciona

No seu terminal local, execute um teste rápido contra o Worker. Substitua a URL pela sua e use seu próprio token de API da Bright Data:

curl -i https://<your-worker>.workers.dev/request \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your-bd-token>" \
  -d '{"zone":"mcp_unlocker","url":"https://www.salesforce.com","format":"raw","data_format":"markdown"}' \
  | head -20

Os cabeçalhos de resposta devem incluir um status 200 e um cabeçalho content-length: <algum-número>. Não devem incluir transfer-encoding: chunked. Essa é a prova de que o proxy está funcionando corretamente. Aqui estão as falhas comuns: 401 significa que seu token da Bright Data está errado (verifique novamente o cabeçalho Authorization: Bearer ...); 502 do Worker significa que seu código do Worker não foi implantado (verifique novamente a etapa Deploy); um cabeçalho transfer-encoding: chunked ainda aparecendo significa que você não incluiu as linhas arrayBuffer() + Content-Length no código-fonte do Worker.

Em uma implantação empresarial, este Worker seria substituído por uma camada de integração de nível de produção: MuleSoft rodando no Anypoint, um microsserviço no Heroku ou um API gateway personalizado com autenticação, observabilidade e limitação de taxa. O Worker é um substituto mínimo para esse papel, mas o mesmo padrão funciona nessas configurações de produção.

Parte 3: Configurar as credenciais do Salesforce

O padrão External Credential do Salesforce divide uma credencial de terceiros em três objetos: um External Credential (armazena o token), uma Named Credential (armazena o endpoint) e um Permission Set (concede aos usuários acesso ao principal do External Credential).

Criar o External Credential

Clique no ícone de engrenagem (canto superior direito de qualquer página do Salesforce) → Setup. No Setup, use a caixa Quick Find no topo do painel esquerdo e pesquise Named Credentials. Clique no resultado. Na página que carregar, clique na aba External Credentials e depois em New.

Preencha os seguintes campos:

  • Label: Bright Data Cred
  • Name: Bright_Data_Cred (preenchido automaticamente)
  • Authentication Protocol: Custom

Clique em Save.

Na página de detalhes, encontre a seção Principals e clique em New:

  • Parameter Name: BrightDataPrincipal
  • Sequence Number: 1
  • Identity Type: Named Principal

Na seção Authentication Parameters abaixo do principal, adicione:

  • Name: api_key
  • Value: cole seu token de API da Bright Data

Clique em Save.

De volta à página do External Credential, encontre a seção Custom Headers e clique em New:

  • Name: Authorization
  • Value: Bearer {!$Credential.Bright_Data_Cred.api_key}
  • Sequence Number: 1

⚠️ O campo de merge deve corresponder aos nomes que você definiu. Bright_Data_Cred na fórmula deve corresponder ao API Name do External Credential. api_key deve corresponder ao nome do Authentication Parameter que você definiu no Principal. Se você renomeou algum deles, edite a fórmula para corresponder.

Isso é crítico: marque a caixa de seleção Allow Formulas in HTTP Header neste cabeçalho personalizado. Para encontrá-la: após salvar a linha do cabeçalho, clique na linha para abrir sua visualização de detalhes. A caixa de seleção está nessa página de detalhes, não na página pai do External Credential. Se você pular essa etapa, o Salesforce enviará a string literal Bearer {!$Credential...} para a Bright Data, que retornará 401, e a mensagem de erro não indicará qual caixa de seleção você deixou de marcar. Clique em Save.

⚠️ Você encontrará uma caixa de seleção com o mesmo nome na próxima seção. “Allow Formulas in HTTP Header” aparece em dois lugares. A Caixa A é a que você acabou de marcar (na página de detalhes do Custom Header). A Caixa B está nas Callout Options da Named Credential. Ambas devem estar marcadas. Se apenas uma estiver marcada, o campo de merge será enviado como texto literal e a Bright Data retornará 401.

Setup do Salesforce mostrando a página de detalhes do External Credential Bright_Data_Cred com Authentication Protocol definido como Custom, um Principal chamado BrightDataPrincipal e um Custom Header com o valor 'Bearer {!$Credential.Bright_Data_Cred.api_key}' visível

O valor do campo de merge do Custom Header é a parte que resolve o token de API no momento da requisição. A caixa de seleção Allow Formulas in HTTP Header (não visível nesta profundidade; está na página de detalhes do cabeçalho) deve estar marcada, caso contrário o campo de merge é enviado como texto literal.

Criar a Named Credential

Na mesma seção Named Credentials, volte para a aba Named Credentials e clique em New:

  • Label: Bright Data API
  • Name: Bright_Data_API
  • URL: cole a URL do seu Cloudflare Worker (por exemplo https://bd-proxy.<your-subdomain>.workers.dev)
  • Enabled for Callouts: marcado
  • External Credential: selecione Bright Data Cred

Em Callout Options, defina o seguinte:

  • Generate Authorization Header: desmarcado (você está fornecendo o seu próprio via Custom Header)
  • Allow Formulas in HTTP Header: marcado (para que o campo de merge seja resolvido)
  • Allow Formulas in HTTP Body: marcado (para que corpos JSON dinâmicos funcionem)

Clique em Save.

Página de detalhes da Named Credential 'Bright Data API' do Salesforce mostrando a URL apontando para o Cloudflare Worker, o External Credential definido como Bright Data Cred, Generate Authorization Header desmarcado, Allow Formulas in HTTP Header marcado e Allow Formulas in HTTP Body marcado

A URL aponta para o Cloudflare Worker, não para api.brightdata.com diretamente. É assim que a correção da transferência em chunks funciona. Os três estados das caixas de seleção importam de forma independente; errar qualquer um deles quebra o callout sem aviso.

Criar o Permission Set

O Salesforce bloqueia até os Administradores do Sistema de usar o principal de um External Credential até que um Permission Set conceda acesso explicitamente. Se você pular esta etapa, o Apex retorna um erro INVALID_OPERATION sem diagnóstico útil.

No Setup, pesquise Permission Sets e clique em New:

  • Label: Bright Data Access
  • API Name: Bright_Data_Access
  • License: deixe em branco

Clique em Save.

Na página de detalhes, role até External Credential Principal Access e clique em Edit. Mova Bright_Data_Cred - BrightDataPrincipal da lista Disponível para a lista Habilitado. Clique em Save.

De volta à página de detalhes, clique em Manage Assignments no topo, depois em Add Assignments, selecione seu próprio usuário e conclua a atribuição.

Permission Set 'Bright Data Access' do Salesforce mostrando a seção External Credential Principal Access com Bright_Data_Cred - BrightDataPrincipal concedido, e o botão Manage Assignments visível no topo da página

O Permission Set é o mecanismo de controle que permite definir quais usuários podem executar código que chama a Bright Data. Em uma org empresarial, isso seria atribuído via Permission Set Group a usuários de serviço ou perfis específicos, não a administradores individuais.

Verificar a configuração antes de continuar

Clique no ícone de engrenagem (canto superior direito) → Developer Console. Ele abre em uma nova janela do navegador. Com essa janela em foco, abra o Apex Anônimo via Debug → Open Execute Anonymous Window. Cole o código abaixo e clique em Execute:

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Bright_Data_API/request');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"zone":"mcp_unlocker","url":"https://geo.brdtest.com/welcome.txt?product=unlocker&method=api","format":"raw"}');
req.setTimeout(60000);
HttpResponse res = new Http().send(req);
System.debug('STATUS: ' + res.getStatusCode());
System.debug('BODY: ' + res.getBody().left(500));

Após clicar em Execute, uma nova linha de log aparece no painel inferior do Developer Console. Clique duas vezes nessa linha para abrir o visualizador de log e marque Debug Only na parte inferior (ou digite USER_DEBUG na caixa de filtro). Você deve ver duas linhas exibindo seus valores de STATUS e BODY. Procure por STATUS: 200 e um corpo contendo o texto de boas-vindas da Bright Data. Se você ver 401, verifique novamente a caixa de seleção “Allow Formulas in HTTP Header” do Custom Header (a que está na página de detalhes do cabeçalho, e a que está nas Callout Options da Named Credential). Se você ver INVALID_OPERATION, verifique novamente a atribuição do Permission Set.

Parte 4: Escrever a camada Apex

O Apex registra apenas um @InvocableMethod por classe como uma ação chamável pelo Agentforce. É por isso que a integração usa três classes em vez de uma: um serviço compartilhado para o plumbing HTTP e uma classe por Agent Action.

Cole cada bloco como está. A principal linha que você pode querer alterar é private static final String UNLOCKER_ZONE = 'mcp_unlocker'; em BrightDataService.cls se sua zona da Bright Data tiver um nome diferente (Parte 1).

No Setup, pesquise Apex Classes no Quick Find e clique no resultado. Clique em New. O editor abre com uma classe placeholder como public class YourClassName {}. Clique na área de código (a caixa de texto grande, não o painel Version Settings à direita), selecione todo o texto placeholder, pressione Delete e cole o código abaixo. O nome da classe vem do código-fonte, então você não precisa preencher nenhum outro campo. Clique em Save.

Crie as três classes nesta ordem, porque BrightDataNewsAction e BrightDataFetchAction referenciam BrightDataService. O serviço deve ser salvo primeiro:

BrightDataService.cls: a camada compartilhada de HTTP e parsing

Esta classe contém o plumbing HTTP e os dois métodos auxiliares (searchNews e fetchUrlAsMarkdown) que ambas as Agent Actions chamam. Não há @InvocableMethod aqui; essa anotação está nas classes wrapper de ação abaixo. Aqui está a classe:

public with sharing class BrightDataService {

    private static final String BD_ENDPOINT      = 'callout:Bright_Data_API/request';
    private static final String UNLOCKER_ZONE    = 'mcp_unlocker';
    private static final Integer CALLOUT_TIMEOUT = 60000;
    private static final Integer MAX_RESPONSE_CHARS = 50000;

    /**
     * Fetches the Google News results page for `companyName` (past month) as
     * clean Markdown via Bright Data Web Unlocker. The LLM downstream is
     * responsible for extracting individual articles, sources, and dates.
     */
    public static String searchNews(String companyName) {
        String googleNewsUrl =
            'https://www.google.com/search?q='
            + EncodingUtil.urlEncode(companyName, 'UTF-8')
            + '&tbm=nws&tbs=qdr:m';

        Map<String, Object> body = new Map<String, Object>{
            'zone'        => UNLOCKER_ZONE,
            'url'         => googleNewsUrl,
            'format'      => 'raw',
            'data_format' => 'markdown'
        };

        HttpResponse res = sendRequest(JSON.serialize(body));

        if (res.getStatusCode() != 200) {
            return 'Bright Data returned status '
                + res.getStatusCode() + ': ' + res.getBody().left(300);
        }

        String content = res.getBody();
        if (String.isBlank(content)) {
            return 'No content returned for "' + companyName
                + '". The page may have been empty or blocked.';
        }
        if (content.length() > MAX_RESPONSE_CHARS) {
            content = content.left(MAX_RESPONSE_CHARS)
                + '\n\n[Content truncated at ' + MAX_RESPONSE_CHARS + ' characters]';
        }

        return 'Google News results for "' + companyName
            + '" (past month). Extract article titles, sources, '
            + 'publication dates, and URLs from the Markdown below:\n\n'
            + content;
    }

    /**
     * Fetches any URL via Bright Data Web Unlocker and returns the page as
     * clean Markdown.
     */
    public static String fetchUrlAsMarkdown(String url) {
        Map<String, Object> body = new Map<String, Object>{
            'zone'        => UNLOCKER_ZONE,
            'url'         => url,
            'format'      => 'raw',
            'data_format' => 'markdown'
        };

        HttpResponse res = sendRequest(JSON.serialize(body));

        if (res.getStatusCode() != 200) {
            return 'Web Unlocker returned status '
                + res.getStatusCode() + ': ' + res.getBody().left(300);
        }

        String content = res.getBody();
        if (content.length() > MAX_RESPONSE_CHARS) {
            content = content.left(MAX_RESPONSE_CHARS)
                + '\n\n[Content truncated at ' + MAX_RESPONSE_CHARS + ' characters]';
        }
        return content;
    }

    private static HttpResponse sendRequest(String jsonBody) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(BD_ENDPOINT);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(jsonBody);
        req.setTimeout(CALLOUT_TIMEOUT);
        return new Http().send(req);
    }
}

A classe não tem @InvocableMethod, por design. É a camada HTTP compartilhada que as duas classes de ação utilizam.

BrightDataNewsAction.cls: a ação de busca de notícias

O Agentforce chama este wrapper invocável simples quando o agente decide buscar notícias. Ele valida a entrada, delega o trabalho HTTP para BrightDataService.searchNews() e retorna o resultado no formato Response que o Agentforce espera. Aqui está a classe:

public with sharing class BrightDataNewsAction {

    public class Request {
        @InvocableVariable(
            required=true
            label='Company Name'
            description='The name of the company to search news about. E.g. "Salesforce" or "Acme Corp".')
        public String companyName;
    }

    public class Response {
        @InvocableVariable(
            label='News Results'
            description='Formatted summary of recent news with titles, sources, dates, URLs, and snippets.')
        public String newsResults;
    }

    @InvocableMethod(
        label='Search Recent Company News (Bright Data)'
        description='Searches Google News via Bright Data for recent (past month) articles about a specific named company. Use this whenever the user asks about recent news, announcements, press releases, funding rounds, acquisitions, leadership changes, or current events for a named company.'
        callout=true)
    public static List<Response> searchCompanyNews(List<Request> requests) {
        List<Response> responses = new List<Response>();
        for (Request req : requests) {
            Response resp = new Response();
            try {
                resp.newsResults = String.isBlank(req.companyName)
                    ? 'Error: A company name is required.'
                    : BrightDataService.searchNews(req.companyName);
            } catch (Exception e) {
                resp.newsResults = 'Error fetching news for ' + req.companyName + ': ' + e.getMessage();
            }
            responses.add(resp);
        }
        return responses;
    }
}

O mecanismo de raciocínio do Agentforce lê o campo description na anotação @InvocableMethod para decidir quando chamar esta ação.

BrightDataFetchAction.cls: a ação de busca de URL

O segundo wrapper invocável segue o mesmo padrão da ação de notícias, mas busca qualquer URL que o representante mencionar. O bloco de validação também rejeita entradas malformadas antes do callout. Aqui está a classe:

public with sharing class BrightDataFetchAction {

    public class Request {
        @InvocableVariable(
            required=true
            label='URL to Fetch'
            description='The full URL of a web page to retrieve. Must start with http:// or https://.')
        public String url;
    }

    public class Response {
        @InvocableVariable(
            label='Page Content'
            description='Clean Markdown representation of the page content.')
        public String pageContent;
    }

    @InvocableMethod(
        label='Fetch Web Page as Markdown (Bright Data)'
        description='Retrieves the content of any web URL via Bright Data Web Unlocker and returns it as clean Markdown. Use this when you need to read a specific URL: a company homepage, blog post, press release, or any link the user mentions.'
        callout=true)
    public static List<Response> fetchUrlAsMarkdown(List<Request> requests) {
        List<Response> responses = new List<Response>();
        for (Request req : requests) {
            Response resp = new Response();
            try {
                if (String.isBlank(req.url)
                    || (!req.url.startsWithIgnoreCase('http://')
                        && !req.url.startsWithIgnoreCase('https://'))) {
                    resp.pageContent = 'Error: A valid URL starting with http:// or https:// is required.';
                } else {
                    resp.pageContent = BrightDataService.fetchUrlAsMarkdown(req.url);
                }
            } catch (Exception e) {
                resp.pageContent = 'Error fetching ' + req.url + ': ' + e.getMessage();
            }
            responses.add(resp);
        }
        return responses;
    }
}

Após salvar as três classes, Setup → Apex Classes deve exibi-las como Ativas.

Lista de Apex Classes do Salesforce filtrada por 'Bright', mostrando BrightDataFetchAction (1.820 chars, Ativa), BrightDataNewsAction (1.787 chars, Ativa) e BrightDataService (2.820 chars, Ativa), todas na API Version 66.0

O footprint total de Apex é de cerca de 6,4 KB em três classes. O serviço compartilhado mais uma classe de ação por Agent Action é o padrão padrão do Salesforce quando mais de um invocable é necessário.

Testar as ações

Antes de conectar as ações ao Agentforce, confirme que funcionam de ponta a ponta. No Apex Anônimo:

BrightDataNewsAction.Request r = new BrightDataNewsAction.Request();
r.companyName = 'Salesforce';
List<BrightDataNewsAction.Response> out =
    BrightDataNewsAction.searchCompanyNews(new List<BrightDataNewsAction.Request>{ r });
System.debug('LENGTH: ' + out[0].newsResults.length());
System.debug('PREVIEW: ' + out[0].newsResults.left(800));

Espere que LENGTH esteja entre 5.000 e 10.000, com um preview que começa com o prefixo da classe de serviço e depois o Markdown do Google News. Se você ver LENGTH: 0 ou uma string de erro, volte à etapa de verificação da Parte 3.

Parte 5: Registrar as ações como Agentforce Assets

As classes Apex não são visíveis para o Agentforce por padrão. Cada @InvocableMethod precisa ser registrado como um Agent Action (rótulo mais recente: Agentforce Asset) antes que o agente possa chamá-lo.

No Setup, pesquise Agent Actions (ou Agentforce Assets, dependendo do rótulo que sua org usa) e clique em New Agent Action. Você fará isso duas vezes, uma para cada classe de ação.

Para a ação de notícias, preencha:

  • Reference Action Type: Apex
  • Reference Action Category: Invocable Methods
  • Reference Action: BrightDataNewsAction.searchCompanyNews

Na próxima tela, preencha estes campos:

  • Agent Action Label: mantenha o Search Recent Company News (Bright Data) preenchido automaticamente
  • Agent Action Description: mantenha a descrição preenchida automaticamente (vem da anotação @InvocableMethod)
  • Show loading text for this action: marcado
  • Loading Text: Searching recent news…

O Salesforce detecta automaticamente a entrada (companyName, obrigatório, String) e a saída (newsResults, String). Deixe o mapeamento detectado automaticamente. Clique em Finish.

Repita para a ação de busca:

  • Reference Action: BrightDataFetchAction.fetchUrlAsMarkdown
  • Loading Text: Fetching web page…
  • A entrada é url (obrigatório, String). A saída é pageContent (String).

Após ambas serem salvas, a lista de Agent Actions exibe as duas com status Ativo.

Biblioteca de Agentforce Assets do Salesforce mostrando a aba Actions com duas ações Bright Data personalizadas no topo da lista (Fetch Web Page as Markdown e Search Recent Company News, ambas com Source: Custom, Reference Type: Apex), intercaladas com ações padrão do template Employee

As ações personalizadas aparecem na mesma Asset Library que as ações padrão fornecidas com o template Employee. O mecanismo de raciocínio do Agentforce trata ambas igualmente; a coluna de origem é metadado, não um interruptor comportamental.

A divisão em duas camadas (classe Apex vs. Agent Action) é intencional. É um ponto de governança que permite a um administrador do Salesforce conceder ou revogar a capacidade de um agente sem modificar o Apex. Em uma org regulamentada, por exemplo, o Apex permanece inalterado entre versões; o registro do Agent Action é o que é auditado e controlado por versão.

Parte 6: Construir o agente

Com ambas as ações registradas, você pode conectá-las a um agente Agentforce funcional. Abra o App Launcher (a grade de nove pontos, canto superior esquerdo) e pesquise Agentforce Studio. Dentro do Agentforce Studio, clique em New Agent.

A interface Builder

O Salesforce Agentforce Builder tem dois caminhos iniciais: uma caixa de descrição em linguagem natural no topo ou um conjunto de templates pré-construídos abaixo. Use o caminho de templates para que as etapas abaixo correspondam à sua tela. Clique em Select no card Agentforce Employee Agent. Se sua org não mostrar esse card exato, escolha o template “Employee” ou “General” mais próximo; os quatro subagentes iniciais (Agent Router, General FAQ, Off Topic, Ambiguous Question) podem ter nomes ligeiramente diferentes, mas o trabalho abaixo (adicionar um subagente personalizado e conectar duas ações a ele) se aplica a qualquer template.

Preencha os seguintes campos: