sfcc-cartridge-development

Build SFRA-based Salesforce Commerce Cloud cartridges with controllers, ISML templates, and hooks to customize storefront behavior

11 stars

Best use case

sfcc-cartridge-development is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build SFRA-based Salesforce Commerce Cloud cartridges with controllers, ISML templates, and hooks to customize storefront behavior

Teams using sfcc-cartridge-development should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/sfcc-cartridge-development/SKILL.md --create-dirs "https://raw.githubusercontent.com/finsilabs/awesome-ecommerce-skills/main/skills/platform-salesforce-cc/sfcc-cartridge-development/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/sfcc-cartridge-development/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How sfcc-cartridge-development Compares

Feature / Agentsfcc-cartridge-developmentStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build SFRA-based Salesforce Commerce Cloud cartridges with controllers, ISML templates, and hooks to customize storefront behavior

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# SFCC Cartridge Development

## Overview

Build custom cartridges for Salesforce Commerce Cloud (SFCC) using the Storefront Reference Architecture (SFRA), server-side JavaScript controllers, ISML templates, models, and the B2C Commerce Script API. This skill covers cartridge layering and the override mechanism, route handling with `server.js`, form handling, OCAPI/SCAPI integration, and Job Framework usage for scheduled data processing.

## When to Use This Skill

- When building a custom feature cartridge that extends SFRA functionality
- When overriding or extending existing SFRA controllers, templates, or models
- When implementing custom checkout steps or payment integrations on SFCC
- When creating scheduled jobs for data import/export (product feeds, order sync)
- When building OCAPI hooks or SCAPI integrations for headless storefronts

## Core Instructions

1. **Set up the cartridge structure and layering**

   SFCC uses a cartridge path for layering. Cartridges higher in the path override those lower. A custom cartridge extends `app_storefront_base`:

   ```
   int_acme_custom/
   ├── cartridge/
   │   ├── controllers/       # Server-side JS controllers
   │   ├── models/            # Data model wrappers
   │   ├── scripts/           # Business logic helpers
   │   ├── templates/
   │   │   └── default/       # ISML templates
   │   ├── forms/
   │   │   └── default/       # Form definitions (XML)
   │   ├── static/
   │   │   └── default/
   │   │       ├── css/
   │   │       └── js/
   │   └── int_acme_custom.properties  # Cartridge metadata
   └── package.json
   ```

   Set the cartridge path in Business Manager:
   ```
   int_acme_custom:app_storefront_base
   ```

   `int_acme_custom.properties`:
   ```properties
   ## cartridge.properties
   demandware.cartridges.int_acme_custom.multipleLanguageStorefront=true
   ```

2. **Create a server-side controller**

   Controllers in SFRA use `server.js` for route registration:

   ```javascript
   // controllers/CustomPage.js
   'use strict';

   var server = require('server');
   var cache = require('*/cartridge/scripts/middleware/cache');
   var consentTracking = require('*/cartridge/scripts/middleware/consentTracking');

   /**
    * CustomPage-Show : Renders a custom content page
    * @name CustomPage-Show
    * @function
    * @memberof CustomPage
    * @param {middleware} - server.middleware.https
    * @param {middleware} - consentTracking.consent
    * @param {middleware} - cache.applyDefaultCache
    * @param {querystringparameter} - cid : content asset ID
    * @param {renders} - isml
    * @param {serverfunction} - get
    */
   server.get('Show',
       server.middleware.https,
       consentTracking.consent,
       cache.applyDefaultCache,
       function (req, res, next) {
           var ContentMgr = require('dw/content/ContentMgr');
           var ContentModel = require('*/cartridge/models/content');

           var contentId = req.querystring.cid;
           var apiContent = ContentMgr.getContent(contentId);

           if (!apiContent) {
               res.setStatusCode(404);
               res.render('error/notFound');
               return next();
           }

           var contentModel = new ContentModel(apiContent);

           res.render('custom/contentPage', {
               content: contentModel,
               breadcrumbs: [
                   { htmlValue: 'Home', url: '/' },
                   { htmlValue: contentModel.name, url: '' }
               ]
           });

           next();
       }
   );

   /**
    * CustomPage-Submit : Handles form POST submissions
    */
   server.post('Submit',
       server.middleware.https,
       function (req, res, next) {
           var Transaction = require('dw/system/Transaction');
           var CustomObjectMgr = require('dw/object/CustomObjectMgr');

           var form = req.form;
           var name = form.name;
           var email = form.email;

           // Validate input
           if (!name || !email) {
               res.json({ success: false, error: 'Name and email are required.' });
               return next();
           }

           try {
               Transaction.wrap(function () {
                   var co = CustomObjectMgr.createCustomObject('AcmeSubmissions', email);
                   co.custom.name = name;
                   co.custom.submittedAt = new Date();
               });

               res.json({ success: true, message: 'Submission received.' });
           } catch (e) {
               var Logger = require('dw/system/Logger');
               Logger.error('Submission failed: {0}', e.message);
               res.json({ success: false, error: 'An error occurred. Please try again.' });
           }

           next();
       }
   );

   module.exports = server.exports();
   ```

3. **Extend an existing SFRA controller**

   Use `server.extend` to add or modify routes on an existing controller:

   ```javascript
   // controllers/Cart.js — extending app_storefront_base Cart
   'use strict';

   var server = require('server');
   var page = module.superModule;  // Reference to the base Cart controller
   server.extend(page);

   /**
    * Cart-Show : Append custom data to the Cart page
    */
   server.append('Show', function (req, res, next) {
       var viewData = res.getViewData();

       // Add custom upsell products to the cart page
       var ProductMgr = require('dw/catalog/ProductMgr');
       var ArrayList = require('dw/util/ArrayList');
       var upsells = new ArrayList();

       var basket = require('dw/order/BasketMgr').getCurrentBasket();
       if (basket) {
           var items = basket.getAllProductLineItems();
           for (var i = 0; i < items.length; i++) {
               var recommendations = items[i].product.getRecommendations();
               for (var j = 0; j < Math.min(recommendations.length, 2); j++) {
                   upsells.push(recommendations[j].getRecommendedItem());
               }
           }
       }

       viewData.upsellProducts = upsells.toArray().slice(0, 4);
       res.setViewData(viewData);
       next();
   });

   /**
    * Cart-AddCustomItem : New route added to the Cart controller
    */
   server.post('AddCustomItem', function (req, res, next) {
       var BasketMgr = require('dw/order/BasketMgr');
       var Transaction = require('dw/system/Transaction');
       var ProductMgr = require('dw/catalog/ProductMgr');

       var productId = req.form.pid;
       var quantity = parseInt(req.form.quantity, 10) || 1;
       var product = ProductMgr.getProduct(productId);

       if (!product || !product.isOnline()) {
           res.json({ error: true, message: 'Product not available.' });
           return next();
       }

       var basket = BasketMgr.getCurrentOrNewBasket();
       Transaction.wrap(function () {
           var pli = basket.createProductLineItem(productId, basket.getDefaultShipment());
           pli.setQuantityValue(quantity);
       });

       res.json({ success: true, itemCount: basket.productQuantityTotal });
       next();
   });

   module.exports = server.exports();
   ```

4. **Write ISML templates**

   ```html
   <--- templates/default/custom/contentPage.isml --->
   <isdecorate template="common/layout/page">

       <isscript>
           var assets = require('*/cartridge/scripts/assets');
           assets.addCss('/css/custom/content.css');
           assets.addJs('/js/custom/content.js');
       </isscript>

       <div class="container custom-content-page">
           <div class="row">
               <div class="col-12">
                   <nav aria-label="Breadcrumb">
                       <ol class="breadcrumb">
                           <isloop items="${pdict.breadcrumbs}" var="crumb" status="loopstatus">
                               <isif condition="${loopstatus.last}">
                                   <li class="breadcrumb-item active">${crumb.htmlValue}</li>
                               <iselse/>
                                   <li class="breadcrumb-item">
                                       <a href="${crumb.url}">${crumb.htmlValue}</a>
                                   </li>
                               </isif>
                           </isloop>
                       </ol>
                   </nav>

                   <h1>${pdict.content.name}</h1>
                   <div class="content-body">
                       <isprint value="${pdict.content.body}" encoding="off"/>
                   </div>
               </div>
           </div>
       </div>

   </isdecorate>
   ```

5. **Create a data model wrapper**

   ```javascript
   // models/content.js
   'use strict';

   var URLUtils = require('dw/web/URLUtils');

   /**
    * Content model wrapping a dw.content.Content API object
    * @param {dw.content.Content} contentObj - Content API object
    * @constructor
    */
   function ContentModel(contentObj) {
       this.id = contentObj.ID;
       this.name = contentObj.name || contentObj.ID;
       this.body = contentObj.custom.body ? contentObj.custom.body.markup : '';
       this.online = contentObj.online;
       this.url = URLUtils.url('CustomPage-Show', 'cid', contentObj.ID).toString();
       this.pageTitle = contentObj.pageTitle || this.name;
       this.pageDescription = contentObj.pageDescription || '';
       this.pageKeywords = contentObj.pageKeywords || '';
   }

   module.exports = ContentModel;
   ```

6. **Build a scheduled job for data processing**

   ```javascript
   // scripts/jobs/syncInventory.js
   'use strict';

   var Status = require('dw/system/Status');
   var Logger = require('dw/system/Logger').getLogger('inventory-sync', 'acme');
   var HTTPClient = require('dw/net/HTTPClient');
   var Transaction = require('dw/system/Transaction');
   var ProductInventoryMgr = require('dw/catalog/ProductInventoryMgr');

   /**
    * Job step: Fetch inventory from external ERP and update SFCC
    * @param {dw.util.HashMap} params - Job step parameters
    * @returns {dw.system.Status} - Job status
    */
   function execute(params) {
       var apiUrl = params.get('apiUrl');
       var apiKey = params.get('apiKey');
       var inventoryListId = params.get('inventoryListId') || 'default';

       var httpClient = new HTTPClient();
       httpClient.open('GET', apiUrl);
       httpClient.setRequestHeader('Authorization', 'Bearer ' + apiKey);
       httpClient.setRequestHeader('Accept', 'application/json');
       httpClient.setTimeout(30000);
       httpClient.send();

       if (httpClient.statusCode !== 200) {
           Logger.error('ERP API returned status {0}', httpClient.statusCode);
           return new Status(Status.ERROR, 'API_ERROR', 'ERP API returned ' + httpClient.statusCode);
       }

       var inventory = JSON.parse(httpClient.text);
       var inventoryList = ProductInventoryMgr.getInventoryList(inventoryListId);

       if (!inventoryList) {
           return new Status(Status.ERROR, 'LIST_NOT_FOUND', 'Inventory list not found');
       }

       var updated = 0;
       var errors = 0;

       inventory.items.forEach(function (item) {
           try {
               Transaction.wrap(function () {
                   var record = inventoryList.getRecord(item.sku);
                   if (!record) {
                       record = inventoryList.createRecord(item.sku);
                   }
                   record.setAllocation(item.quantity);
                   if (item.inStockDate) {
                       record.setInStockDate(new Date(item.inStockDate));
                   }
               });
               updated++;
           } catch (e) {
               Logger.error('Failed to update SKU {0}: {1}', item.sku, e.message);
               errors++;
           }
       });

       Logger.info('Inventory sync complete: {0} updated, {1} errors', updated, errors);
       return new Status(Status.OK, 'SYNC_COMPLETE', updated + ' records updated');
   }

   module.exports.execute = execute;
   ```

## Examples

### OCAPI hook for order creation

```javascript
// hooks/order/ocapiHooks.js
'use strict';

var Status = require('dw/system/Status');
var Logger = require('dw/system/Logger').getLogger('ocapi-hooks', 'acme');

/**
 * OCAPI after-POST hook for order creation
 * Called after a new order is placed via OCAPI
 */
exports.afterPOST = function (order) {
    try {
        // Send order data to external analytics
        var HTTPClient = require('dw/net/HTTPClient');
        var httpClient = new HTTPClient();
        httpClient.open('POST', 'https://analytics.acme.com/orders');
        httpClient.setRequestHeader('Content-Type', 'application/json');
        httpClient.send(JSON.stringify({
            orderId: order.orderNo,
            total: order.totalGrossPrice.value,
            currency: order.currencyCode,
            itemCount: order.productLineItems.length,
            customerEmail: order.customerEmail,
        }));

        if (httpClient.statusCode !== 200) {
            Logger.warn('Analytics push failed for order {0}: HTTP {1}',
                order.orderNo, httpClient.statusCode);
        }
    } catch (e) {
        Logger.error('OCAPI hook error: {0}', e.message);
    }

    return new Status(Status.OK);
};
```

Register in `hooks.json`:
```json
{
    "hooks": [
        {
            "name": "dw.ocapi.shop.order.afterPOST",
            "script": "./hooks/order/ocapiHooks"
        }
    ]
}
```

### Form definition and server-side validation

```xml
<!-- forms/default/contactus.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.demandware.com/xml/form/2008-04-19">
    <field formid="name" label="form.contactus.name"
           type="string" mandatory="true" max-length="100"/>
    <field formid="email" label="form.contactus.email"
           type="string" mandatory="true" max-length="254"
           regexp="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"/>
    <field formid="message" label="form.contactus.message"
           type="string" mandatory="true" max-length="2000"/>
    <action formid="submit" label="form.contactus.submit" valid-form="true"/>
</form>
```

```javascript
// controllers/ContactUs.js
'use strict';
var server = require('server');

server.get('Show', function (req, res, next) {
    var contactForm = server.forms.getForm('contactus');
    contactForm.clear();
    res.render('contactus/form', { contactForm: contactForm });
    next();
});

server.post('Submit', function (req, res, next) {
    var contactForm = server.forms.getForm('contactus');

    if (contactForm.valid) {
        var Transaction = require('dw/system/Transaction');
        var CustomObjectMgr = require('dw/object/CustomObjectMgr');

        Transaction.wrap(function () {
            var co = CustomObjectMgr.createCustomObject(
                'ContactSubmission',
                require('dw/util/UUIDUtils').createUUID()
            );
            co.custom.name = contactForm.name.value;
            co.custom.email = contactForm.email.value;
            co.custom.message = contactForm.message.value;
        });

        res.json({ success: true });
    } else {
        res.json({
            success: false,
            fields: {
                name: contactForm.name.error || null,
                email: contactForm.email.error || null,
                message: contactForm.message.error || null,
            }
        });
    }
    next();
});

module.exports = server.exports();
```

## Best Practices

- **Follow the cartridge layering convention** -- custom cartridges override base cartridges; use `module.superModule` to extend rather than replace controllers
- **Use `server.append` over `server.replace`** -- appending preserves the original controller logic and other cartridge extensions; replacing breaks the chain
- **Wrap all database writes in `Transaction.wrap()`** -- SFCC requires explicit transactions for all persistent changes; missing transactions cause silent failures
- **Use the Script API, not direct database access** -- SFCC has no direct SQL access; always use `*Mgr` classes (ProductMgr, BasketMgr, OrderMgr) for data operations
- **Log with categorized loggers** -- use `Logger.getLogger(category, prefix)` so log messages can be filtered in Log Center by category
- **Never hardcode site-specific values** -- use Site Preferences (custom site preferences in Business Manager) for configurable values like API keys and feature flags
- **Test with the SFCC sandbox** -- always develop and test on a sandbox instance before deploying to staging or production
- **Use the SFCC linting rules** -- enforce `'use strict'` and check for missing `next()` calls in controllers, which cause request hanging

## Common Pitfalls

| Problem | Solution |
|---------|----------|
| Controller route not found (404) | Verify the cartridge is in the cartridge path (Business Manager > Sites > Manage Sites > Cartridges) and the controller file name matches the route |
| `module.superModule` returns null | Ensure the base cartridge is listed after your custom cartridge in the cartridge path; the order matters |
| Template changes not appearing | Clear the SFCC template cache in Business Manager (Administration > Sites > Manage Sites > Cache); ISML templates are aggressively cached |
| Custom Object not persisting | Ensure the operation is inside `Transaction.wrap()`; check that the Custom Object type is defined in Business Manager (Administration > Site Development > Custom Objects) |
| Job step fails silently | Return a `Status` object from every job step; return `Status.ERROR` on failure so the job framework reports the failure correctly |
| ISML `<isprint>` double-escaping HTML | Use `encoding="off"` in `<isprint>` for trusted HTML content (e.g., CMS body markup); use default encoding for user-generated content |

## Related Skills

- @erp-integration
- @ecommerce-caching
- @ecommerce-seo
- @pci-dss-compliance
- @product-data-modeling

Related Skills

woocommerce-plugin-development

11
from finsilabs/awesome-ecommerce-skills

Create custom WooCommerce plugins using action/filter hooks, the Settings API, and REST API extensions to add features without modifying core

shopify-theme-development

11
from finsilabs/awesome-ecommerce-skills

Build and customize Shopify themes using Liquid templating, JSON sections, dynamic blocks, and theme app extensions for added functionality

sfcc-ocapi-scapi

11
from finsilabs/awesome-ecommerce-skills

Integrate with Salesforce Commerce Cloud's headless APIs (OCAPI and Shopper APIs) to build custom storefronts and mobile commerce experiences

sfcc-business-manager

11
from finsilabs/awesome-ecommerce-skills

Configure Salesforce Commerce Cloud via Business Manager — manage catalogs, promotions, site preferences, and run XML import/export jobs

magento-module-development

11
from finsilabs/awesome-ecommerce-skills

Build custom Magento 2 modules using dependency injection, plugins, observers, and service contracts to extend core functionality cleanly

saleor-development

11
from finsilabs/awesome-ecommerce-skills

Build and extend Saleor's GraphQL-based headless commerce platform with custom apps, webhook handlers, and dashboard UI customizations

medusa-development

11
from finsilabs/awesome-ecommerce-skills

Extend the open-source Medusa commerce platform with custom services, event subscribers, and API endpoints for unique business requirements

wishlist-save-for-later

11
from finsilabs/awesome-ecommerce-skills

Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price

storefront-theming

11
from finsilabs/awesome-ecommerce-skills

Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode

search-autocomplete

11
from finsilabs/awesome-ecommerce-skills

Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch

responsive-storefront

11
from finsilabs/awesome-ecommerce-skills

Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion

recently-viewed-products

11
from finsilabs/awesome-ecommerce-skills

Show shoppers the products they recently browsed using browser storage so they can easily pick up where they left off on your store