Appearance
Exercise 4 (Optional): Extend with Apex Actions
This exercise goes beyond the core Merchant Account Manager build. The agent you built in Exercises 1-3 is intentionally code-free: standard actions and Flows cover every requirement. This optional exercise shows when and how you would reach for a custom Apex action when a requirement outgrows the platform's standard capabilities. You'll build a Merchant Risk Score action that blends review health, open cases, and engagement recency into a single 0-100 score, creating the Apex class yourself in the Web Console and then connecting it as an agent action.
Step 1: Set up the Web Console
Navigate to Setup.
In the Setup Quick Find, search for Web Console.
Enable Web Console.
Click the Setup Menu, then click Web Console (Beta).
Step 2: Create the Apex class in the Web Console
In the Web Console, press CMD + SHIFT + P (Mac) or CTRL + SHIFT + P (PC) to open the command palette.
Select Create Apex Class.
Select the default Apex class template.
Name the class
MerchantRiskScoreAction.Replace the generated code with the following:
apex/** * MerchantRiskScoreAction * * Calculates a simple risk score (0-100) for a merchant account by * combining three signals: review health, open cases, and engagement * recency. Returns a structured result the agent can present directly. */ public with sharing class MerchantRiskScoreAction { public class Request { @InvocableVariable(label='Account Id or Name' description='Account Id or merchant name to score.' required=true) public String accountIdOrName; } public class RiskFactor { @InvocableVariable(label='Category' description='Reviews, Cases, or Engagement.') public String category; @InvocableVariable(label='Score' description='Points contributed (higher = riskier).') public Decimal score; @InvocableVariable(label='Max Score' description='Maximum possible points for this category.') public Decimal maxScore; @InvocableVariable(label='Detail' description='Explanation of the score.') public String detail; } public class Response { @InvocableVariable(label='Success' description='True when the action executed successfully.') public Boolean success; @InvocableVariable(label='Message' description='Status or error message.') public String message; @InvocableVariable(label='Account Id' description='Resolved Account Id.') public Id accountId; @InvocableVariable(label='Account Name' description='Account Name.') public String accountName; @InvocableVariable(label='Risk Score' description='Overall risk score 0-100 (higher = more at risk).') public Decimal riskScore; @InvocableVariable(label='Risk Level' description='Low, Medium, High, or Critical.') public String riskLevel; @InvocableVariable(label='Risk Factors' description='Breakdown by category.') public List<RiskFactor> factors; @InvocableVariable(label='Risk Factors JSON' description='JSON-serialized factors.') public String factorsJson; } @InvocableMethod( label='Merchant Risk Score' description='Calculates a portfolio risk score for a merchant based on reviews, open cases, and engagement recency.' ) public static List<Response> invoke(List<Request> requests) { Response out = new Response(); out.factors = new List<RiskFactor>(); out.success = false; Request req = (requests != null && !requests.isEmpty()) ? requests[0] : null; try { if (req == null || String.isBlank(req.accountIdOrName)) { out.message = 'accountIdOrName is required.'; out.factorsJson = JSON.serialize(out.factors); return new List<Response>{ out }; } // Resolve account by Id or name Account acct = resolveAccount(req.accountIdOrName); if (acct == null) { out.message = 'No account found matching "' + req.accountIdOrName + '".'; out.factorsJson = JSON.serialize(out.factors); return new List<Response>{ out }; } out.accountId = acct.Id; out.accountName = acct.Name; // Gather storefronts List<Storefront__c> storefronts = [ SELECT Id, Average_Review_Score__c FROM Storefront__c WHERE Account__c = :acct.Id ]; Set<Id> sfIds = new Set<Id>(); for (Storefront__c sf : storefronts) { sfIds.add(sf.Id); } // Score each component out.factors.add(scoreReviews(storefronts)); out.factors.add(scoreCases(acct.Id)); out.factors.add(scoreEngagement(acct.Id, sfIds)); // Sum it up Decimal total = 0; for (RiskFactor f : out.factors) { total += f.score; } out.riskScore = total; out.riskLevel = total >= 80 ? 'Critical' : total >= 60 ? 'High' : total >= 30 ? 'Medium' : 'Low'; out.success = true; out.message = acct.Name + ': ' + out.riskScore.intValue() + '/100 (' + out.riskLevel + ')'; } catch (Exception e) { out.success = false; out.message = e.getMessage(); } out.factorsJson = JSON.serialize(out.factors); return new List<Response>{ out }; } // Helpers private static Account resolveAccount(String input) { // Try as Id first try { Id testId = Id.valueOf(input); List<Account> byId = [SELECT Id, Name FROM Account WHERE Id = :testId LIMIT 1]; if (!byId.isEmpty()) return byId[0]; } catch (Exception e) { /* not an Id */ } // Try exact name, then fuzzy List<Account> byName = [SELECT Id, Name FROM Account WHERE Name = :input LIMIT 1]; if (!byName.isEmpty()) return byName[0]; byName = [SELECT Id, Name FROM Account WHERE Name LIKE :('%' + input + '%') LIMIT 1]; return byName.isEmpty() ? null : byName[0]; } /** Reviews: 0-40 pts. Lower avg score = higher risk. */ private static RiskFactor scoreReviews(List<Storefront__c> storefronts) { RiskFactor f = new RiskFactor(); f.category = 'Reviews'; f.maxScore = 40; if (storefronts.isEmpty()) { f.score = 40; f.detail = 'No storefronts found.'; return f; } // Simple average across storefronts Decimal sum = 0; Integer count = 0; for (Storefront__c sf : storefronts) { if (sf.Average_Review_Score__c != null) { sum += sf.Average_Review_Score__c; count++; } } if (count == 0) { f.score = 40; f.detail = 'No reviews across ' + storefronts.size() + ' storefront(s).'; return f; } Decimal avg = (sum / count).setScale(1); // Map 5.0 to 0 pts, 1.0 to 40 pts (linear) f.score = Math.max(0, Math.min(40, ((5.0 - avg) * 10).setScale(0))); f.detail = 'Average review score: ' + avg + ' across ' + count + ' storefront(s).'; return f; } /** Cases: 0-35 pts. More open/escalated cases = higher risk. */ private static RiskFactor scoreCases(Id accountId) { RiskFactor f = new RiskFactor(); f.category = 'Cases'; f.maxScore = 35; List<Case> openCases = [ SELECT Id, IsEscalated FROM Case WHERE AccountId = :accountId AND IsClosed = false ]; if (openCases.isEmpty()) { f.score = 0; f.detail = 'No open cases.'; return f; } Integer escalated = 0; for (Case c : openCases) { if (c.IsEscalated) escalated++; } // 5 pts per open case + 5 bonus per escalated, capped at 35 f.score = Math.min(35, (openCases.size() * 5) + (escalated * 5)); f.detail = openCases.size() + ' open case(s), ' + escalated + ' escalated.'; return f; } /** Engagement: 0-25 pts. Longer since last activity = higher risk. */ private static RiskFactor scoreEngagement(Id accountId, Set<Id> storefrontIds) { RiskFactor f = new RiskFactor(); f.category = 'Engagement'; f.maxScore = 25; Date today = Date.today(); Integer daysSinceActivity = null; // Most recent marketing event List<Marketing_Event__c> events = [ SELECT Event_Date__c FROM Marketing_Event__c WHERE Account__c = :accountId AND Event_Date__c != null ORDER BY Event_Date__c DESC LIMIT 1 ]; if (!events.isEmpty()) { daysSinceActivity = events[0].Event_Date__c.daysBetween(today); } // Most recent promotion if (!storefrontIds.isEmpty()) { List<Promotion__c> promos = [ SELECT Start_Date__c FROM Promotion__c WHERE Storefront__c IN :storefrontIds AND Start_Date__c != null ORDER BY Start_Date__c DESC LIMIT 1 ]; if (!promos.isEmpty()) { Integer daysSincePromo = promos[0].Start_Date__c.daysBetween(today); if (daysSinceActivity == null || daysSincePromo < daysSinceActivity) { daysSinceActivity = daysSincePromo; } } } if (daysSinceActivity == null) { f.score = 25; f.detail = 'No events or promotions on record.'; } else if (daysSinceActivity <= 30) { f.score = 0; f.detail = 'Active - last activity ' + daysSinceActivity + ' day(s) ago.'; } else if (daysSinceActivity <= 90) { f.score = 10; f.detail = 'Moderate - last activity ' + daysSinceActivity + ' day(s) ago.'; } else { f.score = 25; f.detail = 'Inactive - last activity ' + daysSinceActivity + ' day(s) ago.'; } return f; } }Save the class to compile it.
Right-click the class and select Deploy to Source Org.
TIP
The class is read-only against your data: it queries
Account,Storefront__c,Case,Marketing_Event__c, andPromotion__cto build the score. Make sure those objects and fields exist in your org and that the agent's running user can read them.
Step 3: Create the Apex action
In Agentforce Builder, select the Account Health Overview subagent (a risk score is a natural fit for the health topic).
Click the plus icon besides Actions and select Create an action.
Set the following values:
Field Value Name Merchant Risk ScoreDescription Calculates a portfolio risk score for a merchant based on reviews, open cases, and engagement recency.Reference Action Type ApexReference Action Merchant Risk ScoreClick Create and Open, then click Save.
Step 4: Configure inputs and outputs
Update the following Inputs and Outputs:
Field Value Inputs - Account Id or Name Select checkbox Require Input to execute actionOutputs - Message Select checkbox Show in conversationOutputs - Risk Score Select checkbox Show in conversationOutputs - Risk Level Select checkbox Show in conversationOutputs - Risk Factors Select checkbox Show in conversationClick Save.
Update the Merchant Risk Score Action Agent Script
The Risk Factors output is a list of structured objects, so you need to tell the script its type.
In the Merchant Risk Score action, switch the current view from Canvas to Script.
Find the
Merchant_Risk_Scoreaction.Find the factors output and make the following changes:
- Update the output type to
list[object] - Update the
complex_data_type_nameto@apexClassType/c__MerchantRiskScoreAction$RiskFactor
- Update the output type to
Click Save.
Step 5: Preview
Click Preview and test:
txtWhich of my merchants is most at risk right now?txtGive me a risk score for Harvest Table Kitchens.Open Interaction Details to confirm the Merchant Risk Score action ran, and review the returned score, level, and the per-category breakdown.
Summary
- Created the MerchantRiskScoreAction Apex class in the Web Console.
- Connected it as a custom Apex action on the Account Health Overview subagent.
- Configured the structured Risk Factors output with its Apex class type in Agent Script.
- Learned when to escalate from standard actions to custom Apex: complex scoring, external call-outs, or structured objects.
Next (optional), you'll surface a Prompt Builder template as an agent action.