BEYABLE Product Ranking API - Salesforce Commerce Cloud Integration Guide
Overview
This guide provides step-by-step instructions for integrating the BEYABLE Product Ranking API with Salesforce Commerce Cloud (SFCC). The integration enables AI-powered product ranking to optimize your product listings across category pages, search results, and recommendation slots.
Table of Contents
- Prerequisites
- Architecture Overview
- Cartridge Structure
- Installation
- Configuration
- Metadata Validation
- Implementation
- Job Configuration
- Frontend Integration
- Testing
- Deployment
- Cartridge Maintenance & Updates
- Monitoring & Troubleshooting
- Performance Optimization
Prerequisites
System Requirements
- Salesforce Commerce Cloud (SFCC/B2C Commerce) - Compatibility Mode 19.10 or higher
- Business Manager access with Administrator privileges
- Code deployment access (via VS Code or command line)
- Node.js 14+ (for local development and testing)
- SFRA (Storefront Reference Architecture) or SiteGenesis
Required Information from BEYABLE
Obtain the following from your BEYABLE account manager:
- Account ID: Your 32-character UUID (without hyphens)
- Subscription Key: Your API authentication key
- Ranking Rule ID(s): UUID for each ranking rule
- Segmented Ranking ID(s): UUID for segmented rankings (if applicable)
- Site/Locale Mapping: Tenant configuration for your SFCC sites
SFCC Knowledge Required
- Understanding of SFCC cartridges and pipelines/controllers
- Experience with Business Manager configuration
- Familiarity with ISML templates
- Knowledge of SFCC jobs and site preferences
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ BEYABLE Platform │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Ranking Rules & │ │ Product Ranking API │ │
│ │ Segment Config │───────>│ api.eu1.beyable.com │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└───────────────────────────────────────┬─────────────────────────┘
│ CSV Response
│ (ProductId, Position)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Salesforce Commerce Cloud (SFCC) │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Job: Sync │ │ Custom Object: │ │
│ │ Rankings │───────>│ BEYABLERanking │ │
│ │ (Every 2hrs) │ │ (Store rankings) │ │
│ └──────────────────┘ └──────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Shopper visits │ │ │
│ │ Category/Search │ ▼ │
│ └────────┬─────────┘ ┌──────────────────────────────┐ │
│ │ │ Controller/Script: │ │
│ │ │ - Read segment cookie │ │
│ └─────────────────>│ - Apply rankings │ │
│ │ - Sort products │ │
│ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Optimized Product Listing │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Integration Flow:
- BEYABLE Cookie: BEYABLE Platform sets visitor segment cookie
- Scheduled Job: SFCC job fetches rankings from BEYABLE API (every 1-4 hours)
- Data Storage: Rankings stored in SFCC Custom Objects
- Runtime Application: Controllers read segment, apply rankings to product searches
- Frontend Display: Optimized product listings displayed to shoppers
Cartridge Structure
int_beyable_ranking/
├── cartridge/
│ ├── client/
│ │ └── default/
│ │ └── js/
│ │ └── beyable/
│ │ └── tracking.js
│ ├── controllers/
│ │ ├── BEYABLERanking.js
│ │ └── Product.js (decorator)
│ ├── experience/
│ │ └── components/
│ │ └── commerce_assets/
│ │ └── productTile.js
│ ├── models/
│ │ ├── beyable/
│ │ │ ├── rankingAPI.js
│ │ │ ├── rankingService.js
│ │ │ └── segmentHelper.js
│ │ └── product/
│ │ └── productSearch.js (decorator)
│ ├── scripts/
│ │ ├── jobs/
│ │ │ └── SyncRankings.js
│ │ ├── helpers/
│ │ │ ├── beyableHelper.js
│ │ │ └── rankingHelper.js
│ │ └── services/
│ │ └── BEYABLEService.js
│ ├── templates/
│ │ └── default/
│ │ ├── search/
│ │ │ └── productGrid.isml (decorator)
│ │ └── components/
│ │ └── beyable/
│ │ └── tracking.isml
│ ├── static/
│ │ └── default/
│ │ └── css/
│ │ └── beyable.css
│ └── int_beyable_ranking.properties
├── metadata/
│ ├── custom-objecttype-definitions.xml
│ ├── jobs.xml
│ ├── services.xml
│ └── site-preferences.xml
├── sites/
│ └── site_template/
│ ├── preferences.xml
│ └── services.xml
├── caches.json
├── package.json
├── VERSION.txt
└── CHANGELOG.md
Installation
Step 1: Create Cartridge
Create the cartridge directory:
cd your-sfcc-project/cartridges
mkdir -p int_beyable_ranking/cartridge
cd int_beyable_ranking
Step 2: Initialize Package
Create package.json:
{
"name": "int_beyable_ranking",
"version": "1.1.0",
"description": "BEYABLE Product Ranking API Integration for SFCC",
"main": "cartridge/scripts/init.js",
"caches": "./caches.json",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint cartridge/",
"upload": "sgmf-scripts --upload"
},
"keywords": [
"SFCC",
"BEYABLE",
"Product Ranking",
"Commerce Cloud"
],
"author": "Your Company",
"license": "PROPRIETARY",
"dependencies": {
"dw-api-types": "^23.2.0"
},
"devDependencies": {
"eslint": "^8.0.0",
"sgmf-scripts": "^2.0.0"
}
}
Note (v1.1): The
"caches": "./caches.json"entry is required for the custom cache used in Performance Optimization. See Step 4 below.
Step 3: Cartridge Properties
Create cartridge/int_beyable_ranking.properties:
## Cartridge Properties for BEYABLE Product Ranking Integration
demandware.cartridges.int_beyable_ranking.id=int_beyable_ranking
demandware.cartridges.int_beyable_ranking.multipleLanguageStorefront=true
Step 4: Cache Registration
Create caches.json at the cartridge root. This registers the custom cache used by getCachedRankings() — without this file, CacheMgr.getCache('BEYABLERankings') will throw at runtime:
{
"caches": [
{
"id": "BEYABLERankings",
"expireAfterSeconds": 7200
}
]
}
Configuration
Step 1: Custom Object Type Definition
Create metadata/custom-objecttype-definitions.xml:
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://www.demandware.com/xml/impex/metadata/2006-10-31">
<custom-type type-id="BEYABLERanking">
<display-name xml:lang="x-default">BEYABLE Product Ranking</display-name>
<description xml:lang="x-default">Stores BEYABLE product ranking data</description>
<staging-mode>no-staging</staging-mode>
<storage-scope>site</storage-scope>
<key-definition attribute-id="rankingKey">
<type>string</type>
<min-length>1</min-length>
<max-length>256</max-length>
</key-definition>
<attribute-definitions>
<attribute-definition attribute-id="productID">
<display-name xml:lang="x-default">Product ID</display-name>
<description xml:lang="x-default">SFCC Product ID or SKU</description>
<type>string</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>1</min-length>
<max-length>256</max-length>
</attribute-definition>
<attribute-definition attribute-id="position">
<display-name xml:lang="x-default">Ranking Position</display-name>
<description xml:lang="x-default">Position in ranking (1-based)</description>
<type>int</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
<attribute-definition attribute-id="abTestGroup">
<display-name xml:lang="x-default">A/B Test Group</display-name>
<description xml:lang="x-default">A/B test group identifier (optional)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>0</min-length>
<max-length>100</max-length>
</attribute-definition>
<attribute-definition attribute-id="segmentID">
<display-name xml:lang="x-default">Segment ID</display-name>
<description xml:lang="x-default">Visitor segment identifier</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>0</min-length>
<max-length>100</max-length>
</attribute-definition>
<attribute-definition attribute-id="ruleID">
<display-name xml:lang="x-default">Rule ID</display-name>
<description xml:lang="x-default">BEYABLE ranking rule ID</description>
<type>string</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>
<attribute-definition attribute-id="lastSyncDate">
<display-name xml:lang="x-default">Last Sync Date</display-name>
<description xml:lang="x-default">Timestamp of last synchronization</description>
<type>datetime</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
</attribute-definitions>
<group-definitions>
<attribute-group group-id="BEYABLERankingGroup">
<display-name xml:lang="x-default">BEYABLE Ranking Data</display-name>
<attribute attribute-id="productID"/>
<attribute attribute-id="position"/>
<attribute attribute-id="abTestGroup"/>
<attribute attribute-id="segmentID"/>
<attribute attribute-id="ruleID"/>
<attribute attribute-id="lastSyncDate"/>
</attribute-group>
</group-definitions>
</custom-type>
</metadata>
Changed in v1.1:
min-lengthon the key definition andproductIDraised from0to1(a zero-length key is never valid), andmax-lengthadded to all bounded string attributes. These missing constraints were the source of the import validation errors reported on v1.0.
Step 2: Site Preferences
Create metadata/site-preferences.xml:
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://www.demandware.com/xml/impex/metadata/2006-10-31">
<custom-preference-group-definition preference-group-id="BEYABLE">
<display-name xml:lang="x-default">BEYABLE Configuration</display-name>
<description xml:lang="x-default">BEYABLE Product Ranking Configuration</description>
<attribute-definitions>
<!-- General Settings -->
<attribute-definition attribute-id="beyableEnabled">
<display-name xml:lang="x-default">Enable BEYABLE Ranking</display-name>
<description xml:lang="x-default">Enable or disable BEYABLE product ranking</description>
<type>boolean</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>
<attribute-definition attribute-id="beyableAccountID">
<display-name xml:lang="x-default">Account ID</display-name>
<description xml:lang="x-default">BEYABLE account ID (32 characters, no hyphens)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>
<attribute-definition attribute-id="beyableSubscriptionKey">
<display-name xml:lang="x-default">Subscription Key</display-name>
<description xml:lang="x-default">BEYABLE API subscription key</description>
<type>password</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
<attribute-definition attribute-id="beyableAPIBaseURL">
<display-name xml:lang="x-default">API Base URL</display-name>
<description xml:lang="x-default">BEYABLE API base URL</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>https://api.eu1.beyable.com</default-value>
</attribute-definition>
<!-- Ranking Rules -->
<attribute-definition attribute-id="beyableDefaultRankingID">
<display-name xml:lang="x-default">Default Ranking ID</display-name>
<description xml:lang="x-default">Default ranking rule ID (32 characters)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>
<attribute-definition attribute-id="beyableSegmentedRankingID">
<display-name xml:lang="x-default">Segmented Ranking ID</display-name>
<description xml:lang="x-default">Segmented ranking rule ID (optional)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>
<attribute-definition attribute-id="beyableUseSegmentation">
<display-name xml:lang="x-default">Use Visitor Segmentation</display-name>
<description xml:lang="x-default">Enable visitor segmentation</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>
<attribute-definition attribute-id="beyableSegmentCookieName">
<display-name xml:lang="x-default">Segment Cookie Name</display-name>
<description xml:lang="x-default">Name of cookie containing visitor segment</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>b_segment</default-value>
</attribute-definition>
<attribute-definition attribute-id="beyableTenantID">
<display-name xml:lang="x-default">Tenant ID</display-name>
<description xml:lang="x-default">BEYABLE tenant identifier for this site</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
<!-- Application Settings -->
<attribute-definition attribute-id="beyableApplyToCategories">
<display-name xml:lang="x-default">Apply to Category Pages</display-name>
<description xml:lang="x-default">Enable ranking on category listing pages</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>
<attribute-definition attribute-id="beyableApplyToSearch">
<display-name xml:lang="x-default">Apply to Search Results</display-name>
<description xml:lang="x-default">Enable ranking on search result pages</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>
<!-- Sync Settings -->
<attribute-definition attribute-id="beyableSyncEnabled">
<display-name xml:lang="x-default">Enable Automatic Sync</display-name>
<description xml:lang="x-default">Enable automatic ranking synchronization</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>
<attribute-definition attribute-id="beyableAPITimeout">
<display-name xml:lang="x-default">API Timeout (seconds)</display-name>
<description xml:lang="x-default">Maximum time to wait for API response</description>
<type>int</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>30</default-value>
</attribute-definition>
<!-- Debug -->
<attribute-definition attribute-id="beyableDebugLogging">
<display-name xml:lang="x-default">Debug Logging</display-name>
<description xml:lang="x-default">Enable debug logging</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>
</attribute-definitions>
</custom-preference-group-definition>
</metadata>
Step 3: Service Configuration
Create metadata/services.xml:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://www.demandware.com/xml/impex/services/2014-09-26">
<service service-id="beyable.http.ranking">
<enabled-flag>true</enabled-flag>
<type>HTTP</type>
<url>https://api.eu1.beyable.com</url>
<timeout>30000</timeout>
<communication-log>
<type>text</type>
</communication-log>
<mode>production</mode>
<mock-mode-enabled>false</mock-mode-enabled>
</service>
</services>
Metadata Validation
(New in v1.1) Always validate metadata before importing into a live instance.
Pre-Import Checklist
□ XML is well-formed (no unclosed tags, valid nesting)
□ <key-definition> is present in the custom type
□ Bounded string attributes have both <min-length> and <max-length>
□ Key attribute min-length is at least 1
□ Attribute types use valid SFCC values (string, int, boolean, date, datetime, password, ...)
□ No duplicate attribute-id values
□ Job module paths match actual cartridge paths
□ Service ID in services.xml matches the ID used in LocalServiceRegistry.createService()
Import with Validation
- Package metadata:
zip -r beyable_metadata.zip metadata/ - Navigate to Administration > Site Development > Import & Export
- Upload the ZIP
- Under Meta Data, select the file and click Validate first — do not import directly
- Review the validation report:
- Errors must be fixed before import
- Warnings should be reviewed
- Only after a clean validation, run Import
Common Validation Errors
| Error | Cause | Fix |
|---|---|---|
| Invalid key definition | min-length of 0 on key, or missing constraints | Set min-length ≥ 1 and add max-length |
| Attribute constraint missing | Bounded string without max-length | Add <max-length> |
| Unknown attribute type | Typo or unsupported type | Use a valid SFCC metadata type |
| Duplicate attribute | Same attribute-id declared twice | Rename or remove duplicate |
Implementation
Step 1: Service Definition
Create cartridge/scripts/services/BEYABLEService.js:
'use strict';
/**
* BEYABLE API Service
*/
var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var Site = require('dw/system/Site');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'BEYABLEService');
/**
* Create BEYABLE HTTP service
* @returns {dw.svc.Service} Service instance
*/
function getService() {
return LocalServiceRegistry.createService('beyable.http.ranking', {
/**
* Create request
* @param {dw.svc.Service} svc Service instance
* @param {Object} params Request parameters
* @returns {string} Request body
*/
createRequest: function (svc, params) {
var currentSite = Site.getCurrent();
var subscriptionKey = currentSite.getCustomPreferenceValue('beyableSubscriptionKey');
var baseURL = currentSite.getCustomPreferenceValue('beyableAPIBaseURL') || 'https://api.eu1.beyable.com';
// Set headers
svc.addHeader('Subscription-Key', subscriptionKey);
svc.addHeader('Accept', 'text/csv');
// Build URL
var url = baseURL + params.endpoint;
svc.setURL(url);
svc.setRequestMethod('GET');
// Log request if debug is enabled
if (currentSite.getCustomPreferenceValue('beyableDebugLogging')) {
Logger.debug('BEYABLE API Request: {0}', url);
}
return null;
},
/**
* Parse response
* @param {dw.svc.Service} svc Service instance
* @param {dw.net.HTTPClient} client HTTP client
* @returns {Object} Parsed response
*/
parseResponse: function (svc, client) {
var currentSite = Site.getCurrent();
var statusCode = client.statusCode;
var responseText = client.text;
if (statusCode !== 200) {
Logger.error('BEYABLE API Error: Status {0}, Response: {1}', statusCode, responseText);
throw new Error('BEYABLE API returned status ' + statusCode);
}
// Parse CSV
var rankings = parseCSV(responseText);
if (currentSite.getCustomPreferenceValue('beyableDebugLogging')) {
Logger.debug('BEYABLE API Response: {0} rankings retrieved', rankings.length);
}
return {
success: true,
rankings: rankings
};
},
/**
* Filter log message
* @param {string} msg Log message
* @returns {string} Filtered message
*/
filterLogMessage: function (msg) {
// Remove sensitive data from logs
return msg.replace(/Subscription-Key: [^\r\n]+/, 'Subscription-Key: [REDACTED]');
}
});
}
/**
* Parse CSV response
* @param {string} csvText CSV content
* @returns {Array} Array of ranking objects
*/
function parseCSV(csvText) {
var rankings = [];
var lines = csvText.split('\n');
// Skip header row
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
// Parse CSV line (handle quoted values)
var values = parseCSVLine(line);
if (values.length >= 3) {
rankings.push({
productID: values[0],
abTestGroup: values[1] || null,
position: parseInt(values[2], 10)
});
}
}
return rankings;
}
/**
* Parse CSV line handling quoted values
* @param {string} line CSV line
* @returns {Array} Array of values
*/
function parseCSVLine(line) {
var values = [];
var current = '';
var inQuotes = false;
for (var i = 0; i < line.length; i++) {
var char = line.charAt(i);
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
values.push(current);
current = '';
} else {
current += char;
}
}
values.push(current);
return values;
}
/**
* Fetch rankings from BEYABLE API
* @param {string} ruleID Ranking rule ID
* @param {string} tenantID Tenant ID (optional)
* @param {string} segmentID Segment ID (optional)
* @returns {Object} Service response
*/
function fetchRankings(ruleID, tenantID, segmentID) {
var currentSite = Site.getCurrent();
var accountID = currentSite.getCustomPreferenceValue('beyableAccountID');
if (!accountID || !ruleID) {
throw new Error('BEYABLE account ID and ranking ID are required');
}
// Build endpoint
var endpoint;
if (segmentID && tenantID) {
endpoint = '/segmented-productranking/' + accountID + '/' + ruleID + '/' + segmentID + '/' + tenantID;
} else if (segmentID) {
endpoint = '/segmented-productranking/' + accountID + '/' + ruleID + '/' + segmentID;
} else if (tenantID) {
endpoint = '/productranking/' + accountID + '/' + ruleID + '/' + tenantID;
} else {
endpoint = '/productranking/' + accountID + '/' + ruleID;
}
var service = getService();
var result = service.call({
endpoint: endpoint
});
if (result.status === 'OK') {
return result.object;
} else {
Logger.error('BEYABLE API call failed: {0}', result.errorMessage);
throw new Error('Failed to fetch rankings: ' + result.errorMessage);
}
}
module.exports = {
fetchRankings: fetchRankings,
getService: getService
};
Step 2: Ranking Helper
Create cartridge/scripts/helpers/rankingHelper.js:
'use strict';
/**
* BEYABLE Ranking Helper
*/
var CustomObjectMgr = require('dw/object/CustomObjectMgr');
var Transaction = require('dw/system/Transaction');
var Site = require('dw/system/Site');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'rankingHelper');
/**
* Get ranking key
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @param {string} productID Product ID
* @returns {string} Ranking key
*/
function getRankingKey(ruleID, segmentID, productID) {
var key = ruleID + '-';
if (segmentID) {
key += segmentID + '-';
} else {
key += 'default-';
}
key += productID;
return key;
}
/**
* Store rankings in custom objects
* @param {Array} rankings Array of ranking data
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {number} Number of rankings stored
*/
function storeRankings(rankings, ruleID, segmentID) {
var stored = 0;
Transaction.wrap(function () {
// Clear existing rankings for this rule/segment
clearRankings(ruleID, segmentID);
// Store new rankings
rankings.forEach(function (ranking) {
var key = getRankingKey(ruleID, segmentID, ranking.productID);
try {
var customObj = CustomObjectMgr.createCustomObject('BEYABLERanking', key);
customObj.custom.productID = ranking.productID;
customObj.custom.position = ranking.position;
customObj.custom.abTestGroup = ranking.abTestGroup;
customObj.custom.segmentID = segmentID || null;
customObj.custom.ruleID = ruleID;
customObj.custom.lastSyncDate = new Date();
stored++;
} catch (e) {
Logger.error('Failed to store ranking for product {0}: {1}', ranking.productID, e.message);
}
});
});
Logger.info('Stored {0} rankings for rule {1}, segment {2}', stored, ruleID, segmentID || 'default');
return stored;
}
/**
* Clear rankings for rule/segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
*/
function clearRankings(ruleID, segmentID) {
var rankings;
// Query arguments are passed as varargs after the sort string.
// The {0}/{1} placeholders are bound to these arguments.
if (segmentID) {
rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
null,
ruleID,
segmentID
);
} else {
rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = NULL',
null,
ruleID
);
}
var count = 0;
while (rankings.hasNext()) {
var ranking = rankings.next();
CustomObjectMgr.remove(ranking);
count++;
}
rankings.close();
Logger.debug('Cleared {0} existing rankings', count);
}
/**
* Get ranking for product
* @param {string} productID Product ID
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {dw.object.CustomObject|null} Ranking object
*/
function getRanking(productID, ruleID, segmentID) {
var key = getRankingKey(ruleID, segmentID, productID);
return CustomObjectMgr.getCustomObject('BEYABLERanking', key);
}
/**
* Get all rankings for rule/segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {dw.util.SeekableIterator} Iterator of rankings
*/
function getAllRankings(ruleID, segmentID) {
var sortString = 'custom.position asc';
if (segmentID) {
return CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
sortString,
ruleID,
segmentID
);
}
return CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = NULL',
sortString,
ruleID
);
}
/**
* Apply rankings to product hits
* @param {dw.util.Iterator} productHits Product search hits
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Array} Sorted array of product IDs
*/
function applyRankings(productHits, ruleID, segmentID) {
var currentSite = Site.getCurrent();
if (!currentSite.getCustomPreferenceValue('beyableEnabled')) {
return null;
}
// Get rankings map
var rankingsMap = {};
var rankings = getAllRankings(ruleID, segmentID);
while (rankings.hasNext()) {
var ranking = rankings.next();
rankingsMap[ranking.custom.productID] = ranking.custom.position;
}
rankings.close();
// Create array of products with rankings
var productsWithRankings = [];
var productsWithoutRankings = [];
while (productHits.hasNext()) {
var hit = productHits.next();
var productID = hit.productID;
if (rankingsMap[productID]) {
productsWithRankings.push({
productID: productID,
position: rankingsMap[productID],
hit: hit
});
} else {
productsWithoutRankings.push({
productID: productID,
hit: hit
});
}
}
// Sort ranked products
productsWithRankings.sort(function (a, b) {
return a.position - b.position;
});
// Combine: ranked first, then unranked
var sorted = productsWithRankings.concat(productsWithoutRankings);
return sorted.map(function (item) {
return item.productID;
});
}
module.exports = {
storeRankings: storeRankings,
clearRankings: clearRankings,
getRanking: getRanking,
getAllRankings: getAllRankings,
applyRankings: applyRankings,
getRankingKey: getRankingKey
};
Changed in v1.1:
clearRankings()andgetAllRankings()now pass the query arguments (ruleID,segmentID) toqueryCustomObjects(). In v1.0 the{0}/{1}placeholders were declared but the arguments were never passed, so the queries failed at runtime. This was the main cause of the "referenced methods do not exist" feedback.
Step 3: Segment Helper
Create cartridge/scripts/helpers/beyableHelper.js:
'use strict';
/**
* BEYABLE Helper Functions
*/
var Site = require('dw/system/Site');
/**
* Check if BEYABLE is enabled
* @returns {boolean} True if enabled
*/
function isEnabled() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableEnabled') === true;
}
/**
* Get visitor segment from cookie
* @param {dw.web.Cookies} cookies Request cookies
* @returns {string|null} Segment ID
*/
function getVisitorSegment(cookies) {
var currentSite = Site.getCurrent();
if (!currentSite.getCustomPreferenceValue('beyableUseSegmentation')) {
return null;
}
if (!cookies) {
return null;
}
var cookieName = currentSite.getCustomPreferenceValue('beyableSegmentCookieName') || 'b_segment';
var segmentCookie = cookies[cookieName];
if (segmentCookie && segmentCookie.value) {
return segmentCookie.value;
}
return null;
}
/**
* Get tenant ID for current site
* @returns {string|null} Tenant ID
*/
function getTenantID() {
var currentSite = Site.getCurrent();
var tenantID = currentSite.getCustomPreferenceValue('beyableTenantID');
if (!tenantID) {
// Fallback to site ID
tenantID = currentSite.getID();
}
return tenantID;
}
/**
* Get default ranking ID
* @returns {string|null} Ranking ID
*/
function getDefaultRankingID() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableDefaultRankingID');
}
/**
* Get segmented ranking ID
* @returns {string|null} Segmented ranking ID
*/
function getSegmentedRankingID() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableSegmentedRankingID');
}
/**
* Should apply to categories
* @returns {boolean} True if should apply
*/
function shouldApplyToCategories() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableApplyToCategories') === true;
}
/**
* Should apply to search
* @returns {boolean} True if should apply
*/
function shouldApplyToSearch() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableApplyToSearch') === true;
}
module.exports = {
isEnabled: isEnabled,
getVisitorSegment: getVisitorSegment,
getTenantID: getTenantID,
getDefaultRankingID: getDefaultRankingID,
getSegmentedRankingID: getSegmentedRankingID,
shouldApplyToCategories: shouldApplyToCategories,
shouldApplyToSearch: shouldApplyToSearch
};
Changed in v1.1: Removed the unused
dw/web/Cookieimport and added a null guard oncookies. Thecookies[cookieName]access pattern itself was already correct in v1.0.
Step 4: Job Script
Create cartridge/scripts/jobs/SyncRankings.js:
'use strict';
/**
* BEYABLE Ranking Sync Job
*
* This job fetches product rankings from the BEYABLE API
* and stores them in SFCC Custom Objects
*/
var Status = require('dw/system/Status');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'SyncRankings');
var Site = require('dw/system/Site');
var BEYABLEService = require('*/cartridge/scripts/services/BEYABLEService');
var rankingHelper = require('*/cartridge/scripts/helpers/rankingHelper');
var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');
/**
* Sync rankings for a specific rule and segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Object} Sync result
*/
function syncRankingsForRule(ruleID, segmentID) {
var tenantID = beyableHelper.getTenantID();
Logger.info('Syncing rankings for rule: {0}, segment: {1}, tenant: {2}',
ruleID, segmentID || 'default', tenantID);
try {
// Fetch rankings from API
var response = BEYABLEService.fetchRankings(ruleID, tenantID, segmentID);
if (!response.success) {
throw new Error('API call failed');
}
var rankings = response.rankings;
Logger.info('Fetched {0} rankings from BEYABLE API', rankings.length);
// Optionally filter to active products only (see Performance Optimization)
// rankings = filterActiveProducts(rankings);
// Store rankings
var stored = rankingHelper.storeRankings(rankings, ruleID, segmentID);
return {
success: true,
count: stored
};
} catch (e) {
Logger.error('Error syncing rankings: {0}', e.message);
return {
success: false,
error: e.message
};
}
}
/**
* Main job execution function
* @returns {dw.system.Status} Job status
*/
function execute() {
var currentSite = Site.getCurrent();
Logger.info('Starting BEYABLE ranking synchronization for site: {0}', currentSite.getID());
// Check if BEYABLE is enabled
if (!beyableHelper.isEnabled()) {
Logger.warn('BEYABLE is not enabled for this site');
return new Status(Status.OK, 'DISABLED', 'BEYABLE ranking is not enabled');
}
// Check if sync is enabled
if (!currentSite.getCustomPreferenceValue('beyableSyncEnabled')) {
Logger.warn('BEYABLE sync is disabled');
return new Status(Status.OK, 'DISABLED', 'BEYABLE sync is disabled');
}
var totalSynced = 0;
var errors = [];
try {
// Sync default rankings
var defaultRankingID = beyableHelper.getDefaultRankingID();
if (defaultRankingID) {
var result = syncRankingsForRule(defaultRankingID, null);
if (result.success) {
totalSynced += result.count;
} else {
errors.push('Default ranking: ' + result.error);
}
}
// Sync segmented rankings if enabled
if (currentSite.getCustomPreferenceValue('beyableUseSegmentation')) {
var segmentedRankingID = beyableHelper.getSegmentedRankingID();
if (segmentedRankingID) {
// Note: In production, you might want to sync multiple segments
// For now, we'll sync with a placeholder. Customize based on your segments.
var segments = getSegmentsToSync(); // Implement this based on your needs
segments.forEach(function (segmentID) {
var result = syncRankingsForRule(segmentedRankingID, segmentID);
if (result.success) {
totalSynced += result.count;
} else {
errors.push('Segment ' + segmentID + ': ' + result.error);
}
});
}
}
Logger.info('BEYABLE sync completed. Total rankings synced: {0}', totalSynced);
if (errors.length > 0) {
Logger.error('Sync completed with errors: {0}', errors.join('; '));
return new Status(Status.ERROR, 'ERROR', 'Sync completed with errors: ' + errors.join('; '));
}
return new Status(Status.OK, 'OK', 'Successfully synced ' + totalSynced + ' rankings');
} catch (e) {
Logger.error('Fatal error during sync: {0}', e.message);
return new Status(Status.ERROR, 'ERROR', 'Fatal error: ' + e.message);
}
}
/**
* Get list of segments to sync
* Customize this function based on your segmentation strategy
* @returns {Array} Array of segment IDs
*/
function getSegmentsToSync() {
// Example: Return predefined segments
// In production, you might fetch this from a custom object or site preference
return ['premium', 'standard', 'vip'];
}
module.exports = {
execute: execute
};
Step 5: Product Search Decorator
Create cartridge/models/product/productSearch.js:
'use strict';
var base = module.superModule;
var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');
var rankingHelper = require('*/cartridge/scripts/helpers/rankingHelper');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'productSearch');
/**
* Apply BEYABLE ranking to search results
* @param {dw.catalog.ProductSearchModel} productSearch Product search model
* @param {Object} httpParameterMap HTTP parameter map
* @returns {Array} Ranked product IDs
*/
function applyBEYABLERanking(productSearch, httpParameterMap) {
if (!beyableHelper.isEnabled()) {
return null;
}
// Determine if we should apply ranking based on context
var cgid = httpParameterMap.cgid ? httpParameterMap.cgid.stringValue : null;
var searchQuery = httpParameterMap.q ? httpParameterMap.q.stringValue : null;
var shouldApply = false;
if (cgid && beyableHelper.shouldApplyToCategories()) {
shouldApply = true;
} else if (searchQuery && beyableHelper.shouldApplyToSearch()) {
shouldApply = true;
}
if (!shouldApply) {
return null;
}
// Get visitor segment
// Note: `request` is a global variable in SFCC script/controller context.
// Do NOT use require('dw/system/Request').getCurrent() — that method does not exist.
var segmentID = beyableHelper.getVisitorSegment(request.httpCookies);
// Get appropriate ranking ID
var ruleID;
if (segmentID && beyableHelper.getSegmentedRankingID()) {
ruleID = beyableHelper.getSegmentedRankingID();
} else {
ruleID = beyableHelper.getDefaultRankingID();
}
if (!ruleID) {
Logger.warn('No ranking rule ID configured');
return null;
}
// Apply rankings
var productHits = productSearch.getProductSearchHits();
var rankedProductIDs = rankingHelper.applyRankings(productHits, ruleID, segmentID);
return rankedProductIDs;
}
/**
* Extends the base product search model
* @param {dw.catalog.ProductSearchModel} productSearch Product search model
* @param {Object} httpParameterMap HTTP parameter map
* @param {string} sortingRule Sorting rule
* @param {Array} selectedFilters Selected refinements
* @param {dw.catalog.Category} category Category
* @constructor
*/
function ProductSearch(productSearch, httpParameterMap, sortingRule, selectedFilters, category) {
base.call(this, productSearch, httpParameterMap, sortingRule, selectedFilters, category);
// Apply BEYABLE ranking
var rankedIDs = applyBEYABLERanking(productSearch, httpParameterMap);
if (rankedIDs && rankedIDs.length > 0) {
this.beyableRanked = true;
// Re-sort product hits based on ranked IDs.
// getProductSearchHits() returns a fresh iterator on each call,
// so we use the productSearch parameter directly (the base model
// does not expose it on `this`).
var originalHits = productSearch.getProductSearchHits();
var rankedHits = [];
var hitMap = {};
// Create map of product ID to hit
while (originalHits.hasNext()) {
var hit = originalHits.next();
hitMap[hit.productID] = hit;
}
// Reorder hits based on ranking
rankedIDs.forEach(function (productID) {
if (hitMap[productID]) {
rankedHits.push(hitMap[productID]);
}
});
this.productIds = rankedHits.map(function (hit) {
return hit.productID;
});
} else {
this.beyableRanked = false;
}
}
module.exports = ProductSearch;
Changed in v1.1: The global
requestvariable replacesrequire('dw/system/Request').getCurrent()(which does not exist in the SFCC API), and the hit re-sorting block uses theproductSearchparameter instead ofthis.productSearch(not exposed by the SFRA base model).
Job Configuration
Step 1: Job Definition
Create metadata/jobs.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jobs xmlns="http://www.demandware.com/xml/impex/jobs/2015-07-01">
<job job-id="BEYABLE-SyncRankings">
<description>Synchronize product rankings from BEYABLE API</description>
<is-disabled>false</is-disabled>
<run-mode>parallel</run-mode>
<flow>
<step step-id="SyncRankings" description="Sync BEYABLE product rankings">
<script-module-step>
<module-name>int_beyable_ranking/cartridge/scripts/jobs/SyncRankings.js</module-name>
<function-name>execute</function-name>
<transactional>false</transactional>
<timeout-in-seconds>3600</timeout-in-seconds>
</script-module-step>
</step>
</flow>
<retry-count>3</retry-count>
<retry-delay-in-ms>30000</retry-delay-in-ms>
</job>
</jobs>
Step 2: Schedule Job in Business Manager
- Navigate to Administration > Operations > Jobs
- Find job BEYABLE-SyncRankings
- Click Schedule and Run
- Configure schedule:
- Recurrence: Recurring
- Interval: Every 2 hours (recommended)
- Start Time: Choose appropriate time
- Click OK
Frontend Integration
Step 1: Tracking Script Template
Create cartridge/templates/default/components/beyable/tracking.isml:
<iscomment>
BEYABLE Tracking Script
This template should be included in the page footer
</iscomment>
<isset name="beyableEnabled" value="${dw.system.Site.current.getCustomPreferenceValue('beyableEnabled')}" scope="page" />
<isif condition="${beyableEnabled}">
<isset name="segmentCookieName" value="${dw.system.Site.current.getCustomPreferenceValue('beyableSegmentCookieName')}" scope="page" />
<script type="text/javascript">
// BEYABLE integration - segment detection
(function() {
var segmentCookieName = '${segmentCookieName}';
// Helper to read cookie
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Log segment for debugging (if debug mode is enabled)
var segment = getCookie(segmentCookieName);
if (segment && console && console.log) {
console.log('BEYABLE Segment:', segment);
}
})();
</script>
</isif>
Step 2: Include Tracking in Footer
Modify your footer template to include:
<isinclude template="components/beyable/tracking" />
Testing
Step 1: Configuration Test
Business Manager Configuration:
- Navigate to Merchant Tools > Site Preferences > Custom Preferences > BEYABLE
- Enable BEYABLE Ranking
- Enter Account ID and Subscription Key
- Configure Ranking IDs
- Save
Verify Service:
- Navigate to Administration > Operations > Services
- Find
beyable.http.ranking - Click Credentials
- Verify configuration
Step 2: Manual Job Test
- Navigate to Administration > Operations > Jobs
- Find BEYABLE-SyncRankings
- Click Run
- Monitor execution in job logs
- Check for errors
Step 3: Verify Data Storage
- Navigate to Merchant Tools > Custom Objects > BEYABLERanking
- Verify rankings are stored
- Check product IDs and positions
- Verify last sync date
Step 4: Frontend Test
Clear Cache:
- Navigate to Administration > Site Development > Cache Management
- Clear all caches
Test Category Page:
- Visit a category page on storefront
- Verify products are ordered according to rankings
- Use browser dev tools to verify segment cookie
Test Search:
- Perform a search
- Verify ranked results
Step 5: Segment Test
Set Test Cookie:
document.cookie = "b_segment=premium; path=/";Reload Page:
- Verify different rankings apply
Clear Cookie:
document.cookie = "b_segment=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
Deployment
Production Deployment Checklist
Pre-Deployment
Code Review:
- Review all custom code
- Ensure error handling is robust
- Verify logging is appropriate
Backup:
- Export site preferences
- Export custom objects
- Document current configuration
Prepare Metadata:
# Package metadata
zip -r beyable_metadata.zip metadata/
Deployment Steps
Upload Cartridge:
# Using sgmf-scripts
npm run upload
# Or manually via Business Manager
# Administration > Site Development > Code DeploymentImport Metadata:
- Navigate to Administration > Site Development > Import & Export
- Upload
beyable_metadata.zip - Validate first (see Metadata Validation)
- Select all components
- Click Import
Configure Cartridge Path:
- Navigate to Administration > Sites > Manage Sites > [Your Site]
- Add
int_beyable_rankingto cartridge path - Position before
app_storefront_base - Example:
int_beyable_ranking:app_storefront_base
Configure Site Preferences:
- Navigate to Merchant Tools > Site Preferences > Custom Preferences > BEYABLE
- Configure all settings
- Save
Schedule Job:
- Navigate to Administration > Operations > Jobs
- Configure BEYABLE-SyncRankings schedule
- Run initial sync
Clear Caches:
- Navigate to Administration > Site Development > Cache Management
- Clear all caches
Post-Deployment
Verify:
- Check storefront functionality
- Verify rankings are applied
- Test segment detection
- Review logs for errors
Monitor:
- Monitor job execution logs
- Check custom object counts
- Monitor page performance
- Track error rates
Documentation:
- Document configuration
- Create runbook
- Train support team
Cartridge Maintenance & Updates
(New in v1.1) Since this cartridge is built and maintained by your team (BEYABLE does not currently distribute an official maintained SFCC cartridge), this section defines how to version, update, and roll back the cartridge.
Versioning
Use Semantic Versioning (MAJOR.MINOR.PATCH) in package.json, plus VERSION.txt and CHANGELOG.md at the cartridge root:
| Version bump | When | Example |
|---|---|---|
| PATCH | Bug fix, no behavior change | 1.1.0 → 1.1.1 |
| MINOR | New feature, backward compatible | 1.1.0 → 1.2.0 |
| MAJOR | Breaking change (metadata schema change, configuration change, removed behavior) | 1.x → 2.0.0 |
Update Procedure
Each update follows the same sequence regardless of size; the depth of testing scales with the version bump:
- Branch & implement — develop on a feature/bugfix branch in your Git repository
- Update version — bump
package.json,VERSION.txt, and add aCHANGELOG.mdentry - Test on sandbox — upload cartridge to a sandbox, run the job, verify storefront behavior. For MINOR/MAJOR: full regression on category, search, and segmentation. For MAJOR with metadata changes: validate the metadata import on sandbox first
- Backup before deployment — keep the previous cartridge version as a code version in Business Manager (Code Deployment keeps multiple code versions; do not overwrite the active one)
- Deploy — upload as a new code version and activate it. This is the key SFCC mechanism: activation is instant, and so is rollback
- Re-import metadata — only if metadata changed (MAJOR updates). Validate first
- Run initial sync & verify — manual job run, check storefront and logs
Rollback Procedure
SFCC code versions make rollback fast:
Code-only rollback (~5 minutes):
- Administration > Site Development > Code Deployment
- Activate the previous code version
- Clear caches
- Verify storefront
Rollback with metadata changes (~1 hour):
- Re-activate previous code version (as above)
- Re-import the previous metadata export (this is why pre-deployment backup of site preferences and custom object definitions matters)
- Run a full re-sync to repopulate custom objects in the previous schema
- Clear caches and verify
Release Notes
For each release, record in CHANGELOG.md: Added / Changed / Fixed / Breaking Changes, plus a migration note for any MAJOR release. If multiple teams or sites consume the cartridge, distribute release notes before deployment with: the version, the change summary, whether metadata re-import is required, and the rollback plan.
Monitoring & Troubleshooting
Log Files
Monitor these log files:
Custom Logs (BEYABLE-*.log):
- BEYABLE-BEYABLEService.log
- BEYABLE-rankingHelper.log
- BEYABLE-SyncRankings.log
System Logs:
- customdebug.log
- customerror.log
- customwarn.log
Common Issues
1. API Authentication Failures
Symptom: HTTP 401 errors in service logs
Solution:
- Verify Subscription-Key in Business Manager
- Test API manually:
curl -H "Subscription-Key: YOUR_KEY" \
"https://api.eu1.beyable.com/productranking/ACCOUNT_ID/RANKING_ID/TENANT" - Check service configuration in Business Manager
2. Rankings Not Applied
Symptom: Products not sorted by BEYABLE ranking
Solution:
- Verify BEYABLE is enabled in site preferences
- Check custom object count:
- Navigate to Merchant Tools > Custom Objects > BEYABLERanking
- Verify job execution:
- Administration > Operations > Jobs > Job History
- Enable debug logging
- Clear caches
3. Job Failures
Symptom: Sync job shows error status
Solution:
- Review job execution log
- Check service call logs
- Verify API credentials
- Test service manually in Business Manager
- Check for quota limits
4. Performance Issues
Symptom: Slow category/search pages
Solution:
- Enable page caching for category pages
- Index custom objects
- Reduce ranking object count (filter by active products)
- Consider CDN for static assets
5. Segment Not Detected
Symptom: Segmented rankings not working
Solution:
- Verify cookie name matches configuration
- Check browser cookies in dev tools
- Test with manual cookie
- Verify BEYABLE tracking is loaded
- Check cookie domain settings
Debug Commands
Test Service Connection:
Create a test controller cartridge/controllers/BEYABLERanking.js:
'use strict';
var server = require('server');
var BEYABLEService = require('*/cartridge/scripts/services/BEYABLEService');
var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');
server.get('TestConnection', function (req, res, next) {
var ruleID = beyableHelper.getDefaultRankingID();
var tenantID = beyableHelper.getTenantID();
try {
var result = BEYABLEService.fetchRankings(ruleID, tenantID, null);
res.json({
success: true,
count: result.rankings.length,
sample: result.rankings.slice(0, 5)
});
} catch (e) {
res.json({
success: false,
error: e.message
});
}
next();
});
module.exports = server.exports();
Access: https://your-site.com/on/demandware.store/Sites-YourSite-Site/en_US/BEYABLERanking-TestConnection
Security note: Restrict or remove this endpoint in production (e.g. guard with
server.middleware.httpsand an allowlist, or only deploy it on sandbox/staging code versions).
Performance Optimization
1. Custom Object Indexing
While SFCC doesn't support custom indexes on custom objects, optimize queries:
// Use specific queries instead of iterating all objects
var rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
'custom.position asc',
ruleID,
segmentID
);
2. Caching Strategy
Implement application-level caching using the custom cache registered in caches.json (see Installation Step 4):
var CacheMgr = require('dw/system/CacheMgr');
/**
* Get rankings with caching
* Note: cache plain arrays, not SeekableIterators — iterators are
* stateful objects and cannot be stored in the cache.
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Array} Array of {productID, position} entries
*/
function getCachedRankings(ruleID, segmentID) {
var cache = CacheMgr.getCache('BEYABLERankings');
var cacheKey = ruleID + '-' + (segmentID || 'default');
return cache.get(cacheKey, function () {
// Loader function: executed only on cache miss
var result = [];
var rankings = getAllRankings(ruleID, segmentID);
while (rankings.hasNext()) {
var ranking = rankings.next();
result.push({
productID: ranking.custom.productID,
position: ranking.custom.position
});
}
rankings.close();
return result;
});
}
Changed in v1.1: v1.0 cached the
SeekableIteratorreturned bygetAllRankings()directly and usedcache.put()with a TTL argument. Iterators cannot be cached (they are consumed on first read), and entry TTL is defined incaches.json(expireAfterSeconds), not perput()call. The loader-function form ofcache.get()is the recommended SFCC pattern.
3. Page Caching
Enable page caching for category pages in cacheable.isml:
<iscache type="relative" hour="1" varyby="price_promotion"/>
4. Reduce Object Count
Only sync rankings for active, online products:
// In SyncRankings.js
function filterActiveProducts(rankings) {
var ProductMgr = require('dw/catalog/ProductMgr');
return rankings.filter(function(ranking) {
var product = ProductMgr.getProduct(ranking.productID);
return product && product.online;
});
}
5. Async Job Processing
For very large catalogs, consider splitting the sync job:
<!-- Split by product category or segment -->
<job job-id="BEYABLE-SyncRankings-CategoryA">
<!-- Sync only category A -->
</job>
<job job-id="BEYABLE-SyncRankings-CategoryB">
<!-- Sync only category B -->
</job>
Appendix
A. API Endpoint Reference
| Endpoint | Use Case |
|---|---|
/productranking/{accountId}/{rankingId} | Default tenant, no segmentation |
/productranking/{accountId}/{rankingId}/{tenant} | Specific tenant, no segmentation |
/segmented-productranking/{accountId}/{rankingId}/{segmentId} | Default tenant, with segmentation |
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant} | Full configuration |
B. Business Manager Navigation
| Task | Navigation Path |
|---|---|
| Configure BEYABLE | Merchant Tools > Site Preferences > Custom Preferences > BEYABLE |
| Manage Custom Objects | Merchant Tools > Custom Objects > BEYABLERanking |
| View Job Logs | Administration > Operations > Jobs > Job History |
| Manage Services | Administration > Operations > Services > beyable.http.ranking |
| Deploy Code | Administration > Site Development > Code Deployment |
| Import Metadata | Administration > Site Development > Import & Export |
| Clear Cache | Administration > Site Development > Cache Management |
C. Troubleshooting Checklist
- BEYABLE enabled in site preferences
- Account ID and Subscription Key configured
- Ranking rule IDs configured
- Cartridge added to cartridge path
-
caches.jsonpresent and registered inpackage.json - Metadata validated and imported successfully
- Job scheduled and running
- Custom objects created
- Rankings applied to product listings
- Logs free of errors
- Segment cookie detected (if using segmentation)
Support
For additional assistance:
- Cartridge Issues: Check SFCC logs and error messages
- BEYABLE API Issues: Contact your BEYABLE account manager
- SFCC Issues: Refer to SFCC documentation or contact Salesforce support
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.1.0 | 2026-06-04 | Fixed queryCustomObjects() calls (query arguments were never bound), replaced non-existent Request.getCurrent() with the global request, fixed getCachedRankings() (cache arrays via loader function, register cache in caches.json), corrected metadata length constraints causing import validation errors, fixed decorator to use the productSearch parameter. Added Metadata Validation and Cartridge Maintenance & Updates sections. |
| 1.0.0 | 2026-02-26 | Initial SFCC integration guide |