CREDITS.md
# Credits & Acknowledgments
This skill was built upon the collective wisdom of the Salesforce Industries and
Salesforce Apex communities. We gratefully acknowledge the following authors and
resources whose ideas, patterns, and best practices have shaped this skill.
---
## Authors & Contributors
### Primary Contributor
**Shreyas Dhond (CTA)**
Key contributions:
- Contributed the `sf-industry-commoncore-callable-apex` skill
- Strengthened callable guidance, migration patterns, and bundled examples
- Expanded coverage for `System.Callable` and legacy `VlocityOpenInterface` / `VlocityOpenInterface2` scenarios
### Salesforce Industries Documentation Team
**[Callable Implementations](https://help.salesforce.com/s/articleView?id=ind.v_dev_t_callable_implementations_651821.htm&type=5)**
Key contributions:
- Recommended patterns for Industries callable implementations
- Migration guidance from `VlocityOpenInterface`/`VlocityOpenInterface2`
- Action contract and response design guidance
---
## Frameworks & Libraries
### OmniStudio / Vlocity Open Interface (Legacy)
- **Provider**: Salesforce Industries
- **Interface signatures**: `VlocityOpenInterface` and `VlocityOpenInterface2` with `invokeMethod(String methodName, Map<String, Object> inputMap, Map<String, Object> outputMap, Map<String, Object> options)`
- **Reference**: https://help.salesforce.com/s/articleView?id=ind.v_dev_t_callable_implementations_651821.htm&type=5
### System.Callable (Salesforce Platform)
- **Provider**: Salesforce
- **Reference**: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/
---
## Official Salesforce Resources
- **Salesforce Help: Callable Implementations**: https://help.salesforce.com/s/articleView?id=ind.v_dev_t_callable_implementations_651821.htm&type=5
- **Apex Developer Guide**: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/
- **Trailhead**: https://trailhead.salesforce.com
---
## Community Resources
### Salesforce Stack Exchange
**[salesforce.stackexchange.com](https://salesforce.stackexchange.com/)**
- Community Q&A and solutions
- Industry-specific callable implementation discussions
---
examples/Test_QuoteByProductCallable/Industries_QuoteByProductCallable.cls
/**
* @description Industries callable that finds Quotes on an Account that have at least one
* QuoteLineItem with a specified Product2.ProductCode.
* Integrates with OmniStudio/Industries extension points.
* @author sf-industry-commoncore-callable-apex
*
* Actions:
* - findQuotesByProductCode: Returns Quotes for an Account with QuoteLineItems matching productCode
*
* Input (findQuotesByProductCode):
* - accountId (String, required): Id of the Account
* - productCode (String, required): Product2.ProductCode to match on QuoteLineItems
*
* Output envelope:
* - success (Boolean)
* - data (Map): quotes (List<Map> with Id, Name, QuoteNumber)
* - errors (List<Map>)
*/
public with sharing class Industries_QuoteByProductCallable implements System.Callable {
private static final String ACTION_FIND_QUOTES = 'findQuotesByProductCode';
/**
* @description Dispatches to the appropriate action handler.
* @param action Action name (e.g. 'findQuotesByProductCode')
* @param args Input map with accountId and productCode
* @return Response envelope: success, data, errors
*/
public Object call(String action, Map<String, Object> args) {
switch on action {
when ACTION_FIND_QUOTES {
return findQuotesByProductCode(args);
}
when else {
throw new IndustriesCallableException('Unsupported action: ' + action);
}
}
}
/**
* @description Finds all Quotes on an Account that have at least one QuoteLineItem
* with the specified Product2.ProductCode.
* @param args Map with keys: accountId (String), productCode (String)
* @return Response envelope with quotes list or errors
*/
private Map<String, Object> findQuotesByProductCode(Map<String, Object> args) {
List<Map<String, Object>> errors = new List<Map<String, Object>>();
Object accountIdObj = args != null ? args.get('accountId') : null;
Object productCodeObj = args != null ? args.get('productCode') : null;
String accountId = accountIdObj != null ? String.valueOf(accountIdObj).trim() : null;
String productCode = productCodeObj != null ? String.valueOf(productCodeObj).trim() : null;
if (String.isBlank(accountId)) {
errors.add(new Map<String, Object>{
'code' => 'MISSING_ACCOUNT_ID',
'message' => 'accountId is required'
});
}
if (String.isBlank(productCode)) {
errors.add(new Map<String, Object>{
'code' => 'MISSING_PRODUCT_CODE',
'message' => 'productCode is required'
});
}
if (!errors.isEmpty()) {
return new Map<String, Object>{
'success' => false,
'data' => new Map<String, Object>{ 'quotes' => new List<Map<String, Object>>() },
'errors' => errors
};
}
try {
List<Quote> quotes = [
SELECT Id, Name, QuoteNumber, OpportunityId
FROM Quote
WITH USER_MODE
WHERE Opportunity.AccountId = :accountId
AND Id IN (
SELECT QuoteId
FROM QuoteLineItem
WHERE Product2.ProductCode = :productCode
)
];
List<Map<String, Object>> quoteMaps = new List<Map<String, Object>>();
for (Quote q : quotes) {
quoteMaps.add(new Map<String, Object>{
'Id' => q.Id,
'Name' => q.Name,
'QuoteNumber' => q.QuoteNumber
});
}
return new Map<String, Object>{
'success' => true,
'data' => new Map<String, Object>{ 'quotes' => quoteMaps },
'errors' => new List<Map<String, Object>>()
};
} catch (Exception e) {
return new Map<String, Object>{
'success' => false,
'data' => new Map<String, Object>{ 'quotes' => new List<Map<String, Object>>() },
'errors' => new List<Map<String, Object>>{
new Map<String, Object>{
'code' => 'QUERY_ERROR',
'message' => e.getMessage()
}
}
};
}
}
}
examples/Test_QuoteByProductCallable/Industries_QuoteByProductCallableTest.cls
/**
* @description Test class for Industries_QuoteByProductCallable.
* Covers positive, negative, contract, and unsupported action scenarios.
* @author sf-industry-commoncore-callable-apex
*/
@IsTest
private class Industries_QuoteByProductCallableTest {
// ═══════════════════════════════════════════════════════════════════════════
// TEST DATA SETUP
// ═══════════════════════════════════════════════════════════════════════════
@TestSetup
static void setupTestData() {
Account acc = new Account(Name = 'Test Account');
insert acc;
Pricebook2 pb = [SELECT Id FROM Pricebook2 WHERE IsStandard = true LIMIT 1];
Opportunity opp = new Opportunity(
Name = 'Test Opp',
AccountId = acc.Id,
Pricebook2Id = pb.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addMonths(1)
);
insert opp;
Product2 prod = new Product2(
Name = 'Test Product',
ProductCode = 'PROD-001',
IsActive = true
);
insert prod;
PricebookEntry pbe = new PricebookEntry(
Pricebook2Id = pb.Id,
Product2Id = prod.Id,
UnitPrice = 100,
IsActive = true
);
insert pbe;
Quote q = new Quote(
Name = 'Test Quote',
OpportunityId = opp.Id,
Pricebook2Id = pb.Id
);
insert q;
QuoteLineItem qli = new QuoteLineItem(
QuoteId = q.Id,
PricebookEntryId = pbe.Id,
Quantity = 1,
UnitPrice = 100
);
insert qli;
}
// ═══════════════════════════════════════════════════════════════════════════
// POSITIVE TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests findQuotesByProductCode returns Quotes with matching productCode
*/
@IsTest
static void testFindQuotesByProductCode_Success() {
Account acc = [SELECT Id FROM Account LIMIT 1];
System.Callable svc = new Industries_QuoteByProductCallable();
Map<String, Object> args = new Map<String, Object>{
'accountId' => acc.Id,
'productCode' => 'PROD-001'
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('findQuotesByProductCode', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
List<Object> quotes = (List<Object>) data.get('quotes');
Assert.isNotNull(quotes, 'quotes list should not be null');
Assert.areEqual(1, quotes.size(), 'Should return 1 Quote');
}
/**
* @description Tests findQuotesByProductCode returns empty list when no match
*/
@IsTest
static void testFindQuotesByProductCode_NoMatch_ReturnsEmpty() {
Account acc = [SELECT Id FROM Account LIMIT 1];
System.Callable svc = new Industries_QuoteByProductCallable();
Map<String, Object> args = new Map<String, Object>{
'accountId' => acc.Id,
'productCode' => 'NONEXISTENT'
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('findQuotesByProductCode', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed with empty result');
Map<String, Object> data = (Map<String, Object>) result.get('data');
List<Object> quotes = (List<Object>) data.get('quotes');
Assert.areEqual(0, quotes.size(), 'Should return 0 Quotes');
}
// ═══════════════════════════════════════════════════════════════════════════
// CONTRACT / INPUT VALIDATION TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests missing accountId returns error envelope
*/
@IsTest
static void testFindQuotesByProductCode_MissingAccountId_ReturnsError() {
Map<String, Object> args = new Map<String, Object>{
'productCode' => 'PROD-001'
};
System.Callable svc = new Industries_QuoteByProductCallable();
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('findQuotesByProductCode', args);
Test.stopTest();
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
List<Object> errors = (List<Object>) result.get('errors');
Assert.isTrue(errors.size() >= 1, 'Should have at least one error');
}
/**
* @description Tests missing productCode returns error envelope
*/
@IsTest
static void testFindQuotesByProductCode_MissingProductCode_ReturnsError() {
Account acc = [SELECT Id FROM Account LIMIT 1];
Map<String, Object> args = new Map<String, Object>{
'accountId' => acc.Id
};
System.Callable svc = new Industries_QuoteByProductCallable();
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('findQuotesByProductCode', args);
Test.stopTest();
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
List<Object> errors = (List<Object>) result.get('errors');
Assert.isTrue(errors.size() >= 1, 'Should have at least one error');
}
/**
* @description Tests null args returns error envelope
*/
@IsTest
static void testFindQuotesByProductCode_NullArgs_ReturnsError() {
System.Callable svc = new Industries_QuoteByProductCallable();
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('findQuotesByProductCode', null);
Test.stopTest();
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
}
// ═══════════════════════════════════════════════════════════════════════════
// NEGATIVE TESTS (Unsupported Action)
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests unsupported action throws IndustriesCallableException
*/
@IsTest
static void testUnsupportedAction_ThrowsException() {
System.Callable svc = new Industries_QuoteByProductCallable();
Test.startTest();
try {
svc.call('unknownAction', new Map<String, Object>());
Assert.fail('Expected IndustriesCallableException');
} catch (IndustriesCallableException e) {
Assert.isTrue(
e.getMessage().contains('Unsupported action'),
'Error message should mention unsupported action: ' + e.getMessage()
);
}
Test.stopTest();
}
}
examples/Test_QuoteByProductCallable/IndustriesCallableException.cls
/**
* @description Custom exception for Industries callable implementations.
* Thrown when an unsupported or invalid action is requested.
* @author sf-industry-commoncore-callable-apex
*/
public class IndustriesCallableException extends Exception {
}
examples/Test_QuoteByProductCallable/TRANSCRIPT.md
# Reasoning Transcript: Industries Quote-by-Product Callable
This document records the reasoning process and skills used to generate `Industries_QuoteByProductCallable.cls` and `Industries_QuoteByProductCallableTest.cls`.
---
## Task Summary
Create a callable Apex class that finds all Quotes on an Account that have at least one QuoteLineItem with a specified Product2.ProductCode, plus a supporting test class.
---
## Skills Applied
### 1. **sf-industry-commoncore-callable-apex** (Primary)
**Path:** `~/.claude/skills/sf-industry-commoncore-callable-apex/SKILL.md`
**Trigger:** Callable implementations, OmniStudio, Vlocity, Industries Apex extensions
**Application:**
- **Phase 1 – Requirements Gathering:**
Entry point: Integration Procedure / OmniScript. Action: `findQuotesByProductCode`.
Inputs: `accountId` (String), `productCode` (String).
Data: Quote, QuoteLineItem, Product2. No side effects (read-only).
- **Phase 2 – Contract Definition:**
Response envelope: `{ success, data: { quotes }, errors }`.
Action dispatch via `switch on action` with typed exception for unsupported actions.
- **Phase 3 – Implementation Pattern:**
- `call()` kept thin; logic delegated to `findQuotesByProductCode(args)`
- Input validation for `accountId` and `productCode` (blank checks)
- `with sharing` on the callable class
- SOQL with `WITH USER_MODE` for CRUD/FLS
- No SOQL/DML in loops
- Consistent envelope shape
- **Phase 4 – Testing:**
Positive, negative, contract, and unsupported-action tests per skill requirements.
- **Exception Class:**
`IndustriesCallableException` used per skill pattern for unsupported actions.
---
### 2. **sf-apex** (Reference)
**Path:** `~/.claude/skills/sf-apex/SKILL.md`
**Trigger:** Apex classes, code quality
**Application:**
- ApexDoc on the class and action method
- Naming: `Industries_QuoteByProductCallable` (callable pattern)
- Null-safe input handling
- Clear separation of concerns
---
### 3. **sf-testing** (Reference)
**Path:** `~/.claude/skills/sf-testing/SKILL.md`
**Trigger:** Apex tests
**Application:**
- `@TestSetup` for shared data (Account, Opportunity, Product2, PricebookEntry, Quote, QuoteLineItem)
- Positive: success case and empty-result case
- Contract: missing `accountId`, missing `productCode`, null args
- Negative: unsupported action throws `IndustriesCallableException`
- GIVEN/WHEN/THEN comments (adapted from basic-test template)
---
### 4. **sf-soql / sf-data** (Reference)
**Paths:** `skills/sf-data/assets/soql/`, `skills/sf-diagram-mermaid/assets/datamodel/`
**Trigger:** SOQL and data model
**Application:**
- Quote–Opportunity–Account: `Quote.Opportunity.AccountId`
- QuoteLineItem–Product2: `QuoteLineItem.Product2.ProductCode`
- Subquery: `Id IN (SELECT QuoteId FROM QuoteLineItem WHERE Product2.ProductCode = :productCode)`
- Confirmation that Product2.ProductCode is queryable from QuoteLineItem
---
## Design Decisions
| Decision | Rationale |
|----------|-----------|
| **Response envelope** | Aligns with Industries pattern: `success`, `data`, `errors` for consistent handling by Integration Procedures |
| **Error envelope for validation** | Return structured errors instead of throwing; callers can handle without try/catch |
| **Exception for unsupported action** | Follows skill pattern; `IndustriesCallableException` makes misuse explicit |
| **`IndustriesCallableException` in separate class** | Reusable across callable classes, matches skill examples |
| **`WITH USER_MODE` on SOQL** | Enforces CRUD/FLS; requires API 59+ |
| **Quote ↔ Account path** | Standard model: `Quote.Opportunity.AccountId` (no direct Quote → Account) |
| **Product code on Product2** | Standard `Product2.ProductCode` via `QuoteLineItem.Product2` relationship |
---
## Artifacts Produced
1. **IndustriesCallableException.cls** – Custom exception for unsupported actions
2. **Industries_QuoteByProductCallable.cls** – Callable implementation
3. **Industries_QuoteByProductCallableTest.cls** – Test class (6 methods)
4. **TRANSCRIPT.md** – This reasoning transcript
---
## Deployment Notes
Copy the `.cls` files into your Salesforce project under `force-app/main/default/classes/` and add the corresponding `-meta.xml` files with `apiVersion` 59.0 or higher (for `WITH USER_MODE`). If your org uses an older API, remove `WITH USER_MODE` from the SOQL query.
examples/Test_VlocityOpenInterface2Conversion/IndustriesCallableException.cls
/**
* @description Custom exception for Industries callable implementations.
* Thrown when an unsupported or invalid action is requested.
* @author sf-industry-commoncore-callable-apex
*/
public class IndustriesCallableException extends Exception {
}
examples/Test_VlocityOpenInterface2Conversion/MyCustomCallable.cls
/**
* @description Callable implementation for calculateTotal (converted from VlocityOpenInterface2).
* Accepts price and quantity from inputMap, returns total in response envelope.
* Original: MyCustomRemoteClass implements omnistudio.VlocityOpenInterface2.
* @author sf-industry-commoncore-callable-apex
*/
public with sharing class MyCustomCallable implements System.Callable {
public Object call(String action, Map<String, Object> args) {
Map<String, Object> inputMap = (args != null && args.containsKey('inputMap'))
? (Map<String, Object>) args.get('inputMap') : (args != null ? args : new Map<String, Object>());
Map<String, Object> options = (args != null && args.containsKey('options'))
? (Map<String, Object>) args.get('options') : new Map<String, Object>();
if (inputMap == null) { inputMap = new Map<String, Object>(); }
if (options == null) { options = new Map<String, Object>(); }
switch on action {
when 'calculateTotal' {
return calculateTotal(inputMap, options);
}
when else {
throw new IndustriesCallableException('Unsupported action: ' + action);
}
}
}
private Map<String, Object> calculateTotal(Map<String, Object> inputMap, Map<String, Object> options) {
Decimal price = toDecimal(inputMap.get('price'));
Integer qty = toInteger(inputMap.get('quantity'));
if (price == null || qty == null) {
return new Map<String, Object>{
'success' => false,
'data' => new Map<String, Object>(),
'errors' => new List<Map<String, Object>>{
new Map<String, Object>{
'code' => 'INVALID_INPUT',
'message' => 'price and quantity are required and must be numeric'
}
}
};
}
Decimal total = price * qty;
return new Map<String, Object>{
'success' => true,
'data' => new Map<String, Object>{ 'total' => total },
'errors' => new List<Map<String, Object>>()
};
}
private Decimal toDecimal(Object o) {
if (o == null) return null;
if (o instanceof Decimal) return (Decimal) o;
if (o instanceof Integer) return Decimal.valueOf((Integer) o);
if (o instanceof Double) return Decimal.valueOf((Double) o);
if (o instanceof String) {
try { return Decimal.valueOf((String) o); } catch (TypeException e) { return null; }
}
return null;
}
private Integer toInteger(Object o) {
if (o == null) return null;
if (o instanceof Integer) return (Integer) o;
if (o instanceof Long) return ((Long) o).intValue();
if (o instanceof Decimal) return ((Decimal) o).intValue();
if (o instanceof String) {
try { return Integer.valueOf((String) o); } catch (TypeException e) { return null; }
}
return null;
}
}
examples/Test_VlocityOpenInterface2Conversion/MyCustomCallableTest.cls
/**
* @description Test class for MyCustomCallable (converted from MyCustomRemoteClass VlocityOpenInterface2).
* Covers positive, negative, contract, and unsupported action scenarios.
* @author sf-industry-commoncore-callable-apex
*/
@IsTest
private class MyCustomCallableTest {
// ═══════════════════════════════════════════════════════════════════════════
// POSITIVE TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests calculateTotal returns total with inputMap/options format
*/
@IsTest
static void testCalculateTotal_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{
'price' => 10.50,
'quantity' => 3
},
'options' => new Map<String, Object>()
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(31.50, (Decimal) data.get('total'), 'total should be 10.50 * 3');
}
/**
* @description Tests calculateTotal with flat args (backward compatibility)
*/
@IsTest
static void testCalculateTotal_FlatArgs_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'price' => 100,
'quantity' => 5
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(500, (Decimal) data.get('total'), 'total should be 100 * 5');
}
/**
* @description Tests calculateTotal with string inputs (type coercion from OmniScript)
*/
@IsTest
static void testCalculateTotal_StringInputs_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{
'price' => '25.00',
'quantity' => '4'
},
'options' => new Map<String, Object>()
};
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(100, (Decimal) data.get('total'), 'total should be 25 * 4');
}
// ═══════════════════════════════════════════════════════════════════════════
// NEGATIVE / CONTRACT TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests unsupported action throws IndustriesCallableException
*/
@IsTest
static void testUnsupportedAction_Throws() {
try {
System.Callable svc = new MyCustomCallable();
svc.call('unknownAction', new Map<String, Object>{
'inputMap' => new Map<String, Object>(),
'options' => new Map<String, Object>()
});
Assert.fail('Expected IndustriesCallableException');
} catch (IndustriesCallableException e) {
Assert.isTrue(e.getMessage().contains('Unsupported action'), 'Message should mention unsupported action');
}
}
/**
* @description Tests missing price returns error envelope
*/
@IsTest
static void testCalculateTotal_MissingPrice_ReturnsError() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{ 'quantity' => 2 },
'options' => new Map<String, Object>()
};
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
List<Object> errors = (List<Object>) result.get('errors');
Assert.isNotNull(errors, 'errors should not be null');
Assert.areEqual(1, errors.size(), 'Should have one error');
}
/**
* @description Tests missing quantity returns error envelope
*/
@IsTest
static void testCalculateTotal_MissingQuantity_ReturnsError() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{ 'price' => 50 },
'options' => new Map<String, Object>()
};
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
List<Object> errors = (List<Object>) result.get('errors');
Assert.isNotNull(errors, 'errors should not be null');
}
/**
* @description Tests null args handled gracefully
*/
@IsTest
static void testCalculateTotal_NullArgs_ReturnsError() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', null);
Assert.isFalse((Boolean) result.get('success'), 'Should fail with null inputMap');
}
}
examples/Test_VlocityOpenInterface2Conversion/MyCustomRemoteClass.cls
global with sharing class MyCustomRemoteClass implements omnistudio.VlocityOpenInterface2 {
global Boolean invokeMethod(String methodName, Map<String,Object> inputMap, Map<String,Object> outputMap, Map<String,Object> options) {
if (methodName.equals('calculateTotal')) {
calculateTotal(inputMap, outputMap);
return true;
}
return false;
}
private void calculateTotal(Map<String,Object> inputMap, Map<String,Object> outputMap) {
Decimal price = (Decimal)inputMap.get('price');
Decimal qty = (Decimal)inputMap.get('quantity');
outputMap.put('total', price * qty);
}
}
examples/Test_VlocityOpenInterface2Conversion/TRANSCRIPT.md
# Reasoning Transcript: VlocityOpenInterface2 → Callable Conversion (MyCustomRemoteClass)
This document records the reasoning process and skills used to convert `MyCustomRemoteClass.cls` (VlocityOpenInterface2) to `MyCustomCallable.cls` and create `MyCustomCallableTest.cls`.
---
## Task Summary
Convert a VlocityOpenInterface2 implementation (`MyCustomRemoteClass`) to `System.Callable`, add a test class, and document reasoning and skills used.
---
## Source Analysis
**Original class:** `MyCustomRemoteClass.cls`
- **Implements:** `omnistudio.VlocityOpenInterface2`
- **Action:** `calculateTotal`
- **Input contract:** `inputMap` with keys `price` (Decimal), `quantity` (Decimal)
- **Output contract:** `outputMap.put('total', price * qty)`
- **Behavior:** Single action; returns `true` on success, `false` for unsupported methods
---
## Skills Applied
### 1. **sf-industry-commoncore-callable-apex** (Primary)
**Path:** `skills/sf-industry-commoncore-callable-apex/SKILL.md`
**Trigger:** Callable implementations, VlocityOpenInterface, migration to System.Callable
**Application:**
- **Migration guidance (SKILL § Migration: VlocityOpenInterface to System.Callable):**
- Preserved action name: `calculateTotal` → `action` in `call()`
- Pass `inputMap` and `options` as keys in `args`: `{ 'inputMap' => inputMap, 'options' => options }`
- Return response envelope instead of mutating `outputMap`
- Keep `call()` thin; delegate to `calculateTotal(inputMap, options)`
- Backward compatibility: if `args` lacks `inputMap`, treat `args` as `inputMap`
- **Callable skeleton (SKILL § Phase 3 – Callable skeleton):**
- Extracted `inputMap` and `options` from `args` with null guards
- `switch on action` with `when else` throwing `IndustriesCallableException`
- **Response envelope:**
```
{ success, data: { total }, errors }
```
- **Input validation:** Added `toDecimal()` and `toInteger()` type coercion (OmniScript often passes strings); return error envelope when `price` or `quantity` is null or invalid.
- **Contract & Dispatch:** Explicit action list; typed exception for unsupported actions.
---
### 2. **sf-apex** (Reference)
**Path:** `~/.claude/skills/sf-apex/SKILL.md`
**Trigger:** Apex classes, code quality
**Application:**
- ApexDoc on class and key methods
- `with sharing` on the callable class
- Null-safe input handling; no direct casts without validation
- Naming: `MyCustomCallable` (callable pattern; preserves "MyCustom" from source)
---
### 3. **sf-testing** (Reference)
**Path:** `~/.claude/skills/sf-testing/SKILL.md`
**Trigger:** Apex tests
**Application:**
- **Positive:** Success with `inputMap`/`options`, flat args, and string inputs (type coercion)
- **Contract:** Missing `price`, missing `quantity`, null args → error envelope
- **Negative:** Unsupported action throws `IndustriesCallableException`
- GIVEN/WHEN/THEN implied via clear assertions and method names
---
## Design Decisions
| Decision | Rationale |
|----------|-----------|
| **Preserve lowercase keys** (`price`, `quantity`, `total`) | Original `MyCustomRemoteClass` used lowercase; maintains contract for existing Integration Procedures / OmniScripts |
| **Response envelope** | Aligns with Industries pattern; callers get consistent `success`, `data`, `errors` shape |
| **Error envelope for validation** | Return structured errors instead of throwing; callers can handle without try/catch |
| **Exception for unsupported action** | Skill pattern; `IndustriesCallableException` makes misuse explicit |
| **Type coercion helpers** | OmniScript/Integration Procedures often pass strings; `toDecimal`/`toInteger` allow `'25.00'`, `'4'` etc. |
| **Flat args fallback** | When `args` lacks `inputMap`, treat `args` as `inputMap` for backward compatibility |
---
## Artifacts Produced
1. **IndustriesCallableException.cls** – Custom exception for unsupported actions
2. **MyCustomCallable.cls** – Callable implementation (converted from MyCustomRemoteClass)
3. **MyCustomCallableTest.cls** – Test class (7 methods)
4. **TRANSCRIPT.md** – This reasoning transcript
---
## Contract Mapping (Before → After)
| VlocityOpenInterface2 | System.Callable |
|----------------------|-----------------|
| `methodName` = `'calculateTotal'` | `action` = `'calculateTotal'` |
| `inputMap.get('price')`, `inputMap.get('quantity')` | Same keys in `args.inputMap` or flat `args` |
| `outputMap.put('total', ...)` | `return { success => true, data => { total => ... } }` |
| `return false` for unsupported | `throw IndustriesCallableException` |
| `options` (unused in original) | Passed through for future use |
---
## Deployment Notes
Copy the `.cls` files into your Salesforce project under `force-app/main/default/classes/` and add the corresponding `-meta.xml` files. No SOQL/DML; no special API version requirements.
examples/Test_VlocityOpenInterfaceConversion/IndustriesCallableException.cls
/**
* @description Custom exception for Industries callable implementations.
* Thrown when an unsupported or invalid action is requested.
* @author sf-industry-commoncore-callable-apex
*/
public class IndustriesCallableException extends Exception {
}
examples/Test_VlocityOpenInterfaceConversion/MyCustomCallable.cls
/**
* @description Callable implementation for calculateTotal (converted from VlocityOpenInterface).
* Accepts Price and Quantity, returns TotalAmount in response envelope.
* @author sf-industry-commoncore-callable-apex
*/
public with sharing class MyCustomCallable implements System.Callable {
public Object call(String action, Map<String, Object> args) {
Map<String, Object> inputMap = (args != null && args.containsKey('inputMap'))
? (Map<String, Object>) args.get('inputMap') : (args != null ? args : new Map<String, Object>());
Map<String, Object> options = (args != null && args.containsKey('options'))
? (Map<String, Object>) args.get('options') : new Map<String, Object>();
if (inputMap == null) { inputMap = new Map<String, Object>(); }
if (options == null) { options = new Map<String, Object>(); }
switch on action {
when 'calculateTotal' {
return calculateTotal(inputMap, options);
}
when else {
throw new IndustriesCallableException('Unsupported action: ' + action);
}
}
}
private Map<String, Object> calculateTotal(Map<String, Object> inputMap, Map<String, Object> options) {
Decimal price = toDecimal(inputMap.get('Price'));
Integer qty = toInteger(inputMap.get('Quantity'));
if (price == null || qty == null) {
return new Map<String, Object>{
'success' => false,
'data' => new Map<String, Object>(),
'errors' => new List<Map<String, Object>>{
new Map<String, Object>{
'code' => 'INVALID_INPUT',
'message' => 'Price and Quantity are required and must be numeric'
}
}
};
}
Decimal total = price * qty;
return new Map<String, Object>{
'success' => true,
'data' => new Map<String, Object>{ 'TotalAmount' => total },
'errors' => new List<Map<String, Object>>()
};
}
private Decimal toDecimal(Object o) {
if (o == null) return null;
if (o instanceof Decimal) return (Decimal) o;
if (o instanceof Integer) return Decimal.valueOf((Integer) o);
if (o instanceof Double) return Decimal.valueOf((Double) o);
if (o instanceof String) {
try { return Decimal.valueOf((String) o); } catch (TypeException e) { return null; }
}
return null;
}
private Integer toInteger(Object o) {
if (o == null) return null;
if (o instanceof Integer) return (Integer) o;
if (o instanceof Long) return ((Long) o).intValue();
if (o instanceof Decimal) return ((Decimal) o).intValue();
if (o instanceof String) {
try { return Integer.valueOf((String) o); } catch (TypeException e) { return null; }
}
return null;
}
}
examples/Test_VlocityOpenInterfaceConversion/MyCustomCallableTest.cls
/**
* @description Test class for MyCustomCallable.
* Covers positive, negative, contract, and unsupported action scenarios.
* @author sf-industry-commoncore-callable-apex
*/
@IsTest
private class MyCustomCallableTest {
// ═══════════════════════════════════════════════════════════════════════════
// POSITIVE TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests calculateTotal returns TotalAmount with inputMap/options format
*/
@IsTest
static void testCalculateTotal_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{
'Price' => 10.50,
'Quantity' => 3
},
'options' => new Map<String, Object>()
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(31.50, (Decimal) data.get('TotalAmount'), 'TotalAmount should be 10.50 * 3');
}
/**
* @description Tests calculateTotal with flat args (backward compatibility)
*/
@IsTest
static void testCalculateTotal_FlatArgs_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'Price' => 100,
'Quantity' => 5
};
Test.startTest();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Test.stopTest();
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(500, (Decimal) data.get('TotalAmount'), 'TotalAmount should be 100 * 5');
}
/**
* @description Tests calculateTotal with string inputs (type coercion from OmniScript)
*/
@IsTest
static void testCalculateTotal_StringInputs_Success() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{
'Price' => '25.00',
'Quantity' => '4'
},
'options' => new Map<String, Object>()
};
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Assert.isTrue((Boolean) result.get('success'), 'Should succeed');
Map<String, Object> data = (Map<String, Object>) result.get('data');
Assert.areEqual(100, (Decimal) data.get('TotalAmount'), 'TotalAmount should be 25 * 4');
}
// ═══════════════════════════════════════════════════════════════════════════
// NEGATIVE / CONTRACT TESTS
// ═══════════════════════════════════════════════════════════════════════════
/**
* @description Tests unsupported action throws IndustriesCallableException
*/
@IsTest
static void testUnsupportedAction_Throws() {
try {
System.Callable svc = new MyCustomCallable();
svc.call('unknownAction', new Map<String, Object>{
'inputMap' => new Map<String, Object>(),
'options' => new Map<String, Object>()
});
Assert.fail('Expected IndustriesCallableException');
} catch (IndustriesCallableException e) {
Assert.isTrue(e.getMessage().contains('Unsupported action'), 'Message should mention unsupported action');
}
}
/**
* @description Tests missing Price returns error envelope
*/
@IsTest
static void testCalculateTotal_MissingPrice_ReturnsError() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> args = new Map<String, Object>{
'inputMap' => new Map<String, Object>{ 'Quantity' => 2 },
'options' => new Map<String, Object>()
};
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', args);
Assert.isFalse((Boolean) result.get('success'), 'Should fail');
List<Object> errors = (List<Object>) result.get('errors');
Assert.isNotNull(errors, 'errors should not be null');
Assert.areEqual(1, errors.size(), 'Should have one error');
}
/**
* @description Tests null args handled gracefully
*/
@IsTest
static void testCalculateTotal_NullArgs_ReturnsError() {
System.Callable svc = new MyCustomCallable();
Map<String, Object> result = (Map<String, Object>) svc.call('calculateTotal', null);
Assert.isFalse((Boolean) result.get('success'), 'Should fail with null inputMap');
}
}
examples/Test_VlocityOpenInterfaceConversion/MyCustomVlocityOpenInterface2.cls
global class MyCustomClass implements vlocity_cmt.VlocityOpenInterface {
global Boolean invokeMethod(String methodName, Map<String, Object> inputs,
Map<String, Object> output, Map<String, Object> options) {
if (methodName == 'calculateTotal') {
calculateTotal(inputs, output);
}
return true;
}
private void calculateTotal(Map<String, Object> inputs, Map<String, Object> output) {
// Retrieve inputs from OmniScript/IP
Decimal price = (Decimal)inputs.get('Price');
Integer qty = (Integer)inputs.get('Quantity');
// Perform calculation
Decimal total = price * qty;
// Return result to JSON
output.put('TotalAmount', total);
}
}
examples/Test_VlocityOpenInterfaceConversion/TRANSCRIPT.md
# TRANSCRIPT: VlocityOpenInterface → System.Callable Conversion
## Task
Convert `MyCustomVlocityOpenInterface2.cls` (VlocityOpenInterface) to System.Callable and create a test class. Keep a transcript of reasoning and skills used.
---
## Skills Used
| Skill | Purpose |
|-------|---------|
| **sf-industry-commoncore-callable-apex** | Migration pattern, response envelope, action dispatch, inputMap/options contract, test patterns |
| **sf-apex** (implicit) | ApexDoc, type coercion, null safety, exception handling |
---
## Reasoning
### 1. Source Analysis
**Original class**: `MyCustomClass` implements `vlocity_cmt.VlocityOpenInterface`
- **Action**: `calculateTotal`
- **Inputs**: `Price` (Decimal), `Quantity` (Integer) from `inputs` map
- **Output**: Mutates `output` map with `TotalAmount`
- **Observation**: Original returns `true` for all method names (no unsupported-action handling)
### 2. Design Decisions
| Decision | Rationale |
|----------|------------|
| **Open Interface–compatible args** | Use `inputMap` and `options` keys per skill Phase 3 so OmniScript/IP callers can pass the same structure |
| **Flat args fallback** | Skill allows flat args when `inputMap` is absent; supports both calling styles |
| **Response envelope** | Return `{ success, data, errors }` instead of mutating `outputMap` per migration guidance |
| **Input validation** | Skill requires null-safe type coercion; OmniScript can send numbers as String |
| **Unsupported action** | Skill: use `switch on action` and throw `IndustriesCallableException` |
| **`with sharing`** | Skill guardrail: enforce sharing on callable classes |
### 3. Implementation Pattern
- **Entry point**: Extract `inputMap` and `options` from `args` (or treat flat `args` as `inputMap`)
- **Action dispatch**: `switch on action` with `when 'calculateTotal'` and `when else` throwing
- **Business logic**: `calculateTotal(inputMap, options)` returns envelope
- **Type coercion**: `toDecimal()` and `toInteger()` handle Decimal, Integer, Long, String (OmniScript casts)
- **Error envelope**: Missing/invalid inputs → `success: false` with `INVALID_INPUT` error code
### 4. Test Coverage (Phase 4)
| Test | Category | Purpose |
|------|----------|---------|
| `testCalculateTotal_Success` | Positive | InputMap/options format, validates TotalAmount |
| `testCalculateTotal_FlatArgs_Success` | Positive | Flat args backward compatibility |
| `testCalculateTotal_StringInputs_Success` | Contract | String coercion from OmniScript |
| `testUnsupportedAction_Throws` | Negative | Unsupported action throws `IndustriesCallableException` |
| `testCalculateTotal_MissingPrice_ReturnsError` | Contract | Missing required key returns error envelope |
| `testCalculateTotal_NullArgs_ReturnsError` | Contract | Null args handled without NPE |
### 5. Files Produced
| File | Role |
|------|------|
| `MyCustomCallable.cls` | Converted System.Callable |
| `MyCustomCallableTest.cls` | Test class |
| `IndustriesCallableException.cls` | Custom exception (skill pattern) |
| `TRANSCRIPT.md` | This transcript |
---
## Skill References
- **Phase 2 (Design)**: Contract mapping (`methodName`→`action`, `inputMap`/`options`→`args`)
- **Phase 3 (Implementation)**: Callable skeleton (inputMap/options), implementation rules (validate early, null-safe)
- **Phase 4 (Testing)**: Positive, negative, contract tests
- **Migration**: Preserve action names, return envelope instead of mutating output
README.md
# sf-industry-commoncore-callable-apex
Generates and reviews Salesforce Industries Common Core (OmniStudio/Vlocity) Apex callable
implementations. Build secure, deterministic `System.Callable` classes with a 120-point scoring
rubric and migration guidance from legacy `VlocityOpenInterface` implementations.
## Features
- **Callable Generation**: Create `System.Callable` classes with safe action dispatch
- **Callable Review**: Analyze existing callable implementations for risks and fixes
- **120-Point Scoring**: Validation across 7 callable-specific categories
- **VlocityOpenInterface / VlocityOpenInterface2 Support**: Phase 2 contract mapping and Phase 3 implementation patterns for `invokeMethod(String methodName, Map<String, Object> inputMap, Map<String, Object> outputMap, Map<String, Object> options)`
- **Migration Guidance**: Patterns for moving from `VlocityOpenInterface`/`VlocityOpenInterface2` to `System.Callable`
- **Testing Examples**: Test class patterns for actions, errors, and bulk inputs
## Installation
```bash
# Install as part of sf-skills
npx skills add Jaganpro/sf-skills
# Or install just this skill
npx skills add Jaganpro/sf-skills --skill sf-industry-commoncore-callable-apex
```
## Quick Start
### 1. Invoke the skill
```
Skill: sf-industry-commoncore-callable-apex
Request: "Create a callable implementation for Order actions with createOrder and cancelOrder"
```
### 2. Answer requirements questions
The skill will ask about:
- Industries entry point (OmniScript, Integration Procedure, DataRaptor)
- Action names (strings passed into `call`)
- Input/output contract (required keys, response shape)
- Data access needs and security expectations
### 3. Review generated code
The skill generates:
- Callable class with explicit `switch on action`
- Consistent response envelope
- Test class examples for action coverage and error paths
## Bundled Examples
- [examples/Test_QuoteByProductCallable/](examples/Test_QuoteByProductCallable/) — read-only callable example with SOQL and test coverage
- [examples/Test_VlocityOpenInterfaceConversion/](examples/Test_VlocityOpenInterfaceConversion/) — migration pattern from legacy `VlocityOpenInterface`
- [examples/Test_VlocityOpenInterface2Conversion/](examples/Test_VlocityOpenInterface2Conversion/) — migration pattern from `VlocityOpenInterface2`
## Scoring System (120 Points)
| Category | Points | Focus |
|----------|--------|-------|
| Contract & Dispatch | 20 | Explicit actions, `switch on`, versioned strings |
| Input Validation | 20 | Required keys, type coercion, null guards |
| Security | 20 | CRUD/FLS checks, `with sharing`, stripInaccessible |
| Error Handling | 15 | Typed exceptions, consistent errors |
| Bulkification & Limits | 20 | No SOQL/DML in loops, list inputs |
| Testing | 15 | Positive/negative/contract/bulk tests |
| Documentation | 10 | ApexDoc for class and action methods |
**Thresholds**: ✅ 90+ (Ready) | ⚠️ 70-89 (Review) | ❌ <70 (Block)
## Cross-Skill Integration
| Related Skill | When to Use |
|---------------|-------------|
| sf-apex | General Apex work beyond callable implementations |
| sf-metadata | Verify object/field availability before coding |
| sf-testing | Run tests and analyze coverage |
| sf-deploy | Deploy callable classes to an org |
## Documentation
- [Skill Instructions](SKILL.md)
- [Callable Implementations (Salesforce Help)](https://help.salesforce.com/s/articleView?id=ind.v_dev_t_callable_implementations_651821.htm&type=5)
### VlocityOpenInterface / VlocityOpenInterface2
The skill includes design (Phase 2) and implementation (Phase 3) guidance for the Open Interface signature `invokeMethod(String methodName, Map<String, Object> inputMap, Map<String, Object> outputMap, Map<String, Object> options)`. Use this when extending legacy OmniStudio/Vlocity integration points or building dual Callable + Open Interface implementations.
## Requirements
- sf CLI v2
- Target Salesforce org
## License
MIT License. See LICENSE file.
Copyright (c) 2024-2025 Jag Valaiyapathy