Quem trabalha com Consumer Goods Cloud eventualmente se depara com uma pergunta que parece simples mas não é: como o aplicativo mobile funciona offline com dados que vêm do Salesforce? A resposta mora na arquitetura de 6 camadas do Modeler, uma stack de metadados declarativos que transforma contratos XML em um app completo, sem código gerado manualmente.
Neste artigo, vou destrinhar cada camada, explicar como elas se conectam, mostrar exemplos reais de contratos XML e compartilhar o que aprendi implementando essa arquitetura em projetos de campo. Se você é arquiteto Salesforce, desenvolvedor CGC ou administrador que quer entender o que acontece por baixo do capô do aplicativo mobile, este guia é pra você.
Por que a arquitetura importa
O aplicativo mobile do Consumer Goods Cloud não é um wrapper genérico sobre a API do Salesforce. Ele roda contra um banco SQLite local (appl/data/app.db3), sincroniza dados via Salesforce Mobile SDK e executa toda a lógica de negócio offline. Isso significa que cada decisão de arquitetura afeta diretamente o que o representante de campo consegue fazer sem conexão.
A stack de 6 camadas existe justamente para resolver esse problema: como manter um aplicativo offline funcional, com dados frescos do Salesforce, lógica de negócio complexa e uma interface responsiva, tudo a partir de contratos declarativos que o Modeler compila em artefatos de runtime.
Camada 1: Salesforce Cloud (a fonte da verdade)
Tudo começa no Salesforce. Os objetos como Account, Visit__c, Task, Order e Promotion são a fonte canônica dos dados. A configuração de sincronização define quais campos e registros são enviados ao dispositivo mobile, e essa escolha impacta diretamente o que estará disponível offline.
Um ponto que pega muita gente de surpresa: nem todos os campos do Salesforce chegam ao SQLite. Somente as colunas configuradas para sync ficam disponíveis no dispositivo. Se você tentar referenciar um campo que não foi incluído na sincronização, o Modeler vai compilar sem erro, mas o aplicativo vai falhar em runtime.
Camada 2: SQLite Local (o cache offline)
O banco SQLite local (app.db3) é a cópia sincronizada dos dados do Salesforce que reside no dispositivo. Ele alimenta todas as consultas do aplicativo. Quando o representante cria um pedido offline, os dados vão primeiro para esse banco local e depois são enviados ao Salesforce na próxima sincronização.
A regra de ouro aqui: antes de escrever qualquer contrato, verifique o schema do banco local. O comando PRAGMA table_info(Tabela) mostra exatamente quais colunas estão disponíveis. Isso evita o erro mais comum em projetos CGC, que é referenciar campos que existem no Salesforce mas não no SQLite.
Camada 3: DataSource (o mapeamento de dados)
A camada DataSource faz a ponte entre o banco SQLite e o aplicativo. Cada arquivo .datasource.xml mapeia tabelas e colunas do banco local para atributos que as camadas superiores consomem.
Existem dois tipos de DataSource:
- Declarativo (
external="false"): o Modeler gera automaticamente a query SQL com base nos atributos definidos. Ideal para consultas simples. - Scriptado (
external="true"): você escreve a query SQL manualmente. Necessário quando a lógica envolve JOINs complexos, subqueries ou agregações.
Exemplo de DataSource declarativo para um Business Object de visita:
<DataSource name="DsBoVisit_sf" backendSystem="sf"
businessObjectClass="BoVisit"
external="false" editableEntity="Visit__c"
schemaVersion="2.0">
<Attributes>
<Attribute name="pKey" table="Visit__c" column="Id" />
<Attribute name="name" table="Visit__c" column="Name" />
<Attribute name="status" table="Visit__c" column="Status__c" />
<Attribute name="plannedDate" table="Visit__c"
column="PlannedVisitDate__c" />
</Attributes>
<Entities>
<Entity name="Visit__c" alias="" idAttribute="Id" />
</Entities>
<QueryCondition>
<![CDATA[ Visit__c.Id = #pKey# ]]>
</QueryCondition>
<Parameters>
<Parameter name="pKey" type="TEXT" />
</Parameters>
</DataSource>Note os detalhes: o backendSystem="sf" indica que os dados vêm do sync do Salesforce. O editableEntity define qual tabela do SQLite é atualizada quando o usuário salva dados. O QueryCondition usa macros (#pKey#) que são substituídas em runtime pelo framework.
Camada 4: Business Object e ListObject (a lógica de negócio)
Aqui é onde a coisa fica interessante. O Business Object (BO) e o ListObject (LO) transformam os atributos crus do DataSource em entidades tipadas com regras de negócio.
O Business Object representa uma entidade única (uma visita, um pedido, um cliente). Ele define propriedades tipadas que se ligam aos atributos do DataSource, métodos customizados para lifecycle hooks, e objetos aninhados para composição.
<BusinessObject name="BoVisit" schemaVersion="1.1"
generateLoadMethod="true">
<DataSource name="DsBoVisit_sf"/>
<SimpleProperties>
<SimpleProperty name="pKey" type="DomPKey" id="true"
dataSourceProperty="pKey"/>
<SimpleProperty name="name" type="DomText" id="false"
dataSourceProperty="name"/>
<SimpleProperty name="status" type="DomText" id="false"
dataSourceProperty="status"/>
</SimpleProperties>
<ObjectLookups/>
<NestedObjects/>
<ListObjects/>
<Methods>
<Method name="beforeLoadAsync"/>
<Method name="afterLoadAsync"/>
</Methods>
</BusinessObject>Os métodos declarados no XML (como beforeLoadAsync e afterLoadAsync) correspondem a arquivos .bl.js no diretório Mv2/. Esses arquivos contêm a lógica de negócio customizada em JavaScript:
'use strict';
function afterLoadAsync(context) {
var me = this;
var promise = when.resolve(context);
// Lógica customizada aqui
return promise;
}O ListObject, por sua vez, representa coleções. Ele é usado para listas de visitas, pedidos, produtos, etc. O ListItem define a estrutura de cada item da coleção.
Camada 5: Process (a orquestração)
O Process é o hub central que conecta tudo. Ele coordena o carregamento de dados, a lógica de negócio, decisões condicionais e a navegação entre telas. Cada processo define um ProcessContext, que é o objeto de dados compartilhado entre o Process e a UI.
Um processo simples de diálogo pode ter apenas LOAD e END. Um wizard complexo pode ter múltiplos passos com DECISION, VALIDATION, CONFIRM e SAVE. A sintaxe usa :: como separador de namespace:
<Process name="Visit::LoadVisitProcess"
schemaVersion="0.0.0.5">
<Entry>
<ProcessContext>
<Variable name="currentVisit" type="BoVisit"/>
</ProcessContext>
<EntryActions>
<Action actionType="LOAD"
boReference="ProcessContext.currentVisit"/>
</EntryActions>
</Entry>
<Steps>
<Step name="ShowDetail">
<Actions>
<Action actionType="VIEW"
uiReference="Visit::LoadVisitUI"/>
</Actions>
</Step>
</Steps>
</Process>A regra fundamental: todo caminho de fluxo deve terminar em <Action actionType="END">. Processos sem END causam comportamento inesperado no runtime.
Camada 6: User Interface (a apresentação)
A camada UI é a que o representante de campo vê. Cada arquivo .userinterface.xml define layouts responsivos com variantes para Phone, Tablet e Default. Os controles se ligam ao ProcessContext via binding expressions:
binding="ProcessContext::currentVisit.status"Para listas, o dataSource termina com .Items[] (com “I” maiúsculo). Esse detalhe de sintaxe é uma fonte frequente de bugs: usar .items[] em minúsculo compila sem erro mas não funciona em runtime.
Como as camadas se conectam na prática
O fluxo completo de uma tela de detalhe de visita funciona assim:
- Sync: dados do Visit__c chegam ao SQLite via Salesforce Mobile SDK.
- DataSource: DsBoVisit_sf mapeia as colunas do SQLite para atributos.
- Business Object: BoVisit transforma atributos em propriedades tipadas e executa lifecycle hooks.
- Process: Visit_LoadVisitProcess carrega o BO, popula o ProcessContext e direciona para a UI.
- UI: Visit_LoadVisitUI liga os controles ao ProcessContext e renderiza a tela.
- Usuário: o representante vê os dados e interage offline.
Cada camada é independente e pode ser alterada sem afetar as outras, desde que os contratos de interface sejam respeitados. Isso permite que um arquiteto modifique a lógica de negócio no BO sem tocar na UI, ou ajuste a query no DataSource sem alterar o Process.
Erros comuns e como evitá-los
Depois de trabalhar com Modeler em diversos projetos, consolidei os erros que mais vejo acontecer:
Referenciar campos que não existem no SQLite. Sempre verifique o schema com PRAGMA antes de escrever contratos. É o erro mais básico e o mais comum.
Esquecer de declarar métodos no XML. Se você cria um arquivo BoVisit.StartVisit.bl.js, precisa declarar <Method name="startVisit"/> no XML do BO. Sem essa declaração, o método nunca é chamado.
Usar external=”true” desnecessariamente. DataSource scriptado dá mais controle, mas o Modeler não valida queries externas da mesma forma. Erros de SQL em DataSource scriptado só aparecem em runtime.
Não terminar processos com END. Processos sem ação END ficam pendurados e causam memory leaks no dispositivo.
Esquecer o “I” maiúsculo em Items[]. .items[] não funciona. O correto é .Items[]. Parece bobagem, mas já vi projetos inteiros travados por causa disso.
Considerações sobre performance
A arquitetura offline-first tem implicações diretas de performance. Quanto mais dados você sincroniza, mais lento fica o First Sync of the Day (FSOD). A recomendação é sincronizar apenas o que o representante precisa para as visitas planejadas.
DataSource scriptados com JOINs complexos também impactam a performance. Cada query roda contra o SQLite local, que tem limites de memória e processamento. Para coleções grandes, considere paginação no Process em vez de carregar tudo de uma vez.
O que mudou no Summer ’26
O release Summer ’26 trouxe melhorias relevantes para quem trabalha com a arquitetura de metadados. O Sync Management agora oferece configurações padrão mais inteligentes e logs detalhados em modo trace, o que facilita o diagnóstico de problemas de sincronização. Para arquitetos que lidam com ambientes de produção, isso reduz significativamente o tempo de troubleshooting.
Além disso, o recurso de Push Upgrades automáticos, disponível desde o Winter ’26, significa que os managed packages do Consumer Goods Cloud agora são atualizados automaticamente pela Salesforce. Isso afeta diretamente quem customiza contratos do Modeler, pois é preciso garantir que suas customizações sejam compatíveis com as versões mais recentes do framework.
Construindo sua própria extensão
Se você quer criar um módulo customizado do zero, a ordem de criação dos contratos importa:
- Crie o DataSource primeiro e valide contra o schema SQLite.
- Crie o Business Object ou ListObject com referência ao DataSource.
- Implemente os arquivos .bl.js para a lógica customizada.
- Crie o Process que orquestra o fluxo.
- Crie a UI que se liga ao ProcessContext.
Use os comandos do Modeler CLI para validar e compilar em cada etapa:
sf mdl build # Compila os contratos (~3 segundos)
sf mdl simulate # Abre o simulador localCode language: PHP (php)
O simulador permite testar a interface sem deployar para um dispositivo real. É a ferramenta mais útil do ciclo de desenvolvimento CGC.
Conclusão
A arquitetura de 6 camadas do Consumer Goods Cloud Modeler é, na essência, um sistema de metadados declarativos que transforma XML em aplicativo mobile funcional. Cada camada tem uma responsabilidade clara: DataSource mapeia dados, Business Object adiciona regra de negócio, Process orquestra o fluxo, UI apresenta a interface.
Para arquitetos Salesforce que estão entrando no mundo do CGC, entender essa stack é o primeiro passo para tomar decisões acertadas sobre customização. Para quem já trabalha com a plataforma, revisitar os fundamentos ajuda a evitar os erros que consomem tempo de suporte.
O Consumer Goods Cloud continua evoluindo a cada release. Mas a arquitetura de metadados permanece como o alicerce. Quem domina os contratos XML domina o aplicativo.

