Salesforce · · 31 min read

Agentforce for Developers

Building AI agents on Salesforce — setting up the Agentforce VS Code extension, configuring agents, creating custom Apex agent actions, and building an agent that uses Apex to make decisions.

Part 59: Agentforce for Developers

Welcome back to the Salesforce series. Throughout this series we have explored declarative tools like Flows and programmatic tools like Apex triggers, invocable actions, and integrations. All of those tools respond to user actions or scheduled events. Agentforce introduces something fundamentally different: autonomous AI agents that can reason about a user’s request, decide which actions to take, execute those actions, and return a natural-language response — all without a human mapping out every branch of logic in advance.

This is Part 59, and it is dedicated entirely to the developer side of Agentforce. We will start with the architecture and how it differs from everything that came before. Then we will set up the Agentforce for Developers VS Code extension so you can build and test agents locally. After that we will walk through creating an agent in Setup, defining topics, writing instructions, and adding actions. We will then dive deep into creating custom Apex agent actions — the @InvocableMethod pattern adapted for agentic reasoning. Finally, we will build a complete project: a Deal Advisor agent that uses Apex to query Opportunity data, calculate win probability, check discount limits, and recommend next steps.


What is Agentforce?

Agentforce is Salesforce’s platform for building and deploying AI agents directly inside the Salesforce ecosystem. An agent is an LLM-powered assistant that can hold a conversation with a user (or operate autonomously in the background), understand intent, and take actions against live Salesforce data.

How It Differs from Flows and Einstein Bots

Before Agentforce, Salesforce offered two primary automation and conversational tools:

  • Flows are deterministic. You draw every decision branch, every loop, and every assignment. The system follows the path you mapped. Flows are excellent for structured, repeatable processes, but they cannot handle ambiguous or open-ended user requests.
  • Einstein Bots (the classic version) are intent-based chatbots. You define intents, map utterances to those intents, and write dialog rules. They handle simple FAQ-style conversations but struggle with multi-step reasoning or dynamic action selection.

Agentforce agents sit at a higher level. Instead of following a rigid flow chart or matching keywords to intents, an Agentforce agent uses a large language model (LLM) to:

  1. Understand the user’s message in natural language.
  2. Reason about which topic the message belongs to.
  3. Plan which actions to execute to fulfill the request.
  4. Execute those actions (query data, run Apex, call Flows, invoke APIs).
  5. Synthesize the results into a natural-language response.

The developer’s job shifts from building every conditional branch to defining the topics an agent can handle, writing clear instructions for how the agent should behave within each topic, and providing well-designed actions that the agent can call.

The Agent Architecture

An Agentforce agent is composed of three core building blocks:

  1. Topics — A topic is a bounded area of responsibility. For example, a sales agent might have topics like “Deal Analysis,” “Account Lookup,” and “Pipeline Summary.” Each topic has its own instructions and its own set of actions. When a user sends a message, the agent’s reasoning engine decides which topic is most relevant and routes the conversation there.

  2. Actions — An action is a discrete operation the agent can perform. Actions can be standard (query a record, create a record, run a Flow) or custom (call an @InvocableMethod in Apex). Each action has defined inputs and outputs. The agent decides at runtime which actions to invoke and in what order.

  3. Instructions — Instructions are natural-language directives that guide the agent’s behavior within a topic. They tell the agent when to ask clarifying questions, which actions to prefer, what business rules to enforce, and how to format its responses. Well-written instructions are the single most important factor in agent quality.

LLM Grounding

Agentforce agents are grounded in your Salesforce data and metadata. The LLM does not hallucinate record values because the agent architecture forces it to retrieve real data through actions before including it in a response. Grounding works through several mechanisms:

  • Data Cloud retrieval — The agent can search Data Cloud segments and unstructured knowledge articles to find relevant context.
  • Action execution — When the agent needs a specific Opportunity amount or Account name, it calls an action that performs a SOQL query. The LLM reasons about the returned data rather than inventing values.
  • Metadata awareness — The agent understands your object model, field labels, and picklist values because they are surfaced through the platform’s metadata layer.

This grounding is what makes Agentforce practical for enterprise use. The LLM provides reasoning and language generation, but the facts come from your org.


Setting Up the Agentforce for Developers VS Code Extension

Salesforce provides a VS Code extension that lets you build, configure, and test Agentforce agents directly from your development environment. This is a significant productivity boost because you can iterate on agent behavior without constantly switching to the browser.

Prerequisites

Before you install the extension, make sure you have:

  • VS Code version 1.82 or later.
  • Salesforce CLI (sf) installed and up to date. Run sf update to ensure you have the latest version.
  • Salesforce Extensions for VS Code — the standard extension pack (salesforcedx-vscode) should already be installed if you do any Salesforce development.
  • An org with Agentforce enabled — Agentforce is available in Enterprise, Unlimited, and Developer Edition orgs that have the Einstein Generative AI features turned on. In a scratch org, you can enable them in your project scratch definition file.

Installation

  1. Open VS Code.
  2. Go to the Extensions view (Cmd+Shift+X on Mac, Ctrl+Shift+X on Windows/Linux).
  3. Search for Agentforce for Developers.
  4. Click Install.
  5. After installation, reload VS Code if prompted.

The extension adds a new Agentforce panel to the VS Code sidebar. You will see it as an icon that looks like a bot or a sparkle, depending on the version.

Connecting to an Org

The extension uses whatever org is currently authorized in your SFDX project. If you already ran sf org login web or set a default org, the extension picks it up automatically.

To verify your connection:

  1. Open the Command Palette (Cmd+Shift+P).
  2. Type Agentforce: Show Connection Status.
  3. You should see the org alias and username.

If the connection is not established, authorize an org first:

sf org login web --set-default --alias my-agentforce-org

Testing Agents Locally

Once connected, you can interact with any agent deployed to your org directly from VS Code:

  1. Open the Agentforce panel in the sidebar.
  2. Select the agent you want to test from the dropdown.
  3. A chat window opens at the bottom of the panel.
  4. Type a message and press Enter.

The extension sends your message to the agent running in your connected org and streams the response back. You can see which topic was activated, which actions were called, and what data was returned — all in a debug log pane next to the chat window.

This local testing loop is much faster than navigating through Setup or deploying to a channel every time you make a change. You can edit an Apex action, deploy it with sf project deploy start, and immediately test the agent’s behavior without leaving VS Code.

Key Extension Commands

CommandDescription
Agentforce: Show Connection StatusVerify the connected org
Agentforce: Open Agent ChatOpen the testing chat panel
Agentforce: List AgentsShow all agents in the org
Agentforce: View Agent DefinitionInspect an agent’s topics, actions, and instructions
Agentforce: Deploy Agent MetadataPush agent configuration from local source to the org

Setting Up an Agentforce Agent

Now let us walk through creating an agent from scratch in Salesforce Setup. We will configure it step by step.

Step 1: Create the Agent

  1. Go to Setup > Agentforce > Agents.
  2. Click New Agent.
  3. Give the agent a name (for example, Deal Advisor) and an API name (Deal_Advisor).
  4. Choose the agent type. For most custom agents, select Custom Agent.
  5. Optionally provide a description that explains the agent’s purpose. This description is metadata only — it does not affect the agent’s behavior.
  6. Click Save.

Step 2: Define Topics

A topic scopes the agent’s behavior to a specific domain. Within the agent configuration page:

  1. Click New Topic.
  2. Give the topic a name, such as Opportunity Analysis.
  3. Write a Topic Description. This tells the agent when this topic is relevant. For example: This topic handles questions about specific opportunities, including deal analysis, win probability, discount evaluation, and next-step recommendations.
  4. Write the Topic Instructions. This is the most important configuration. Instructions should be clear, specific, and written as directives. For example:
You are a deal analysis assistant. When a user asks about an opportunity:
1. First retrieve the opportunity details using the Get Opportunity Details action.
2. Analyze the data returned, including the stage, amount, close date, and discount.
3. Use the Calculate Win Probability action to get a probability score.
4. If the discount exceeds 20%, warn the user that the deal needs VP approval.
5. Recommend concrete next steps based on the opportunity stage.
6. Always present dollar amounts formatted with commas and two decimal places.
7. If the user does not specify which opportunity, ask them to provide the opportunity name or ID.

Good instructions are specific about the order of operations, the business rules to enforce, and the formatting of responses. Vague instructions like “help the user with opportunities” produce poor results.

Step 3: Add Actions to the Topic

Within the topic configuration, you add the actions the agent can call:

  1. Click Add Action.
  2. You can choose from:
    • Standard Actions — out-of-the-box actions like Query Records, Create Record, Get Record, and Run Flow.
    • Custom Actions — Apex classes with @InvocableMethod that you have deployed to the org.
  3. For each action, configure the input mappings (which parameters the agent should provide) and output mappings (which return values the agent can reference).
  4. Write Action Instructions — short directives that tell the agent when and how to use this specific action. For example: Use this action when you need to calculate the win probability for a specific opportunity. Pass the Opportunity ID as input.

Step 4: Configure Channels

An agent can be deployed to multiple channels:

  • Messaging for Web — embedded chat widget on your website or community.
  • Messaging for In-App — the agent appears inside the Salesforce UI.
  • Slack — the agent operates as a Slack bot.
  • API — external applications call the agent via REST API.

To configure a channel:

  1. Navigate to the agent’s Channels tab.
  2. Click Add Channel.
  3. Select the channel type.
  4. Configure channel-specific settings (for example, the website URL for Messaging for Web, or the Slack workspace for Slack).
  5. Activate the channel.

For development and testing, the VS Code extension and the built-in Agent Builder preview in Setup are the fastest options. You do not need to deploy to a full channel just to test.

Step 5: Activate the Agent

Once your topics, actions, instructions, and channels are configured, click Activate on the agent’s main configuration page. The agent is now live and will respond to messages on the configured channels.


Creating Apex Agent Actions

The real power for developers lies in creating custom Apex actions. An Agentforce agent action is, at its core, an @InvocableMethod — the same annotation we covered in Part 48 for Flow actions. However, designing actions for an AI agent requires a different mindset than designing actions for Flows.

The Key Difference: LLM Reasoning

When a Flow calls your invocable action, the Flow builder explicitly maps input values and consumes output values. Everything is deterministic. When an Agentforce agent calls your action, the LLM decides what input values to provide based on the conversation context, and it reasons about the output to formulate a response.

This means your action’s inputs and outputs must be:

  • Clearly named — the LLM reads the input and output labels to understand what to provide and what it receives.
  • Well-described — the description parameter on @InvocableVariable is critical. The LLM uses descriptions to decide how to populate inputs.
  • Structured for reasoning — instead of returning a raw number, return a structured response with labeled fields that the LLM can interpret.

Basic Agent Action Pattern

Here is the fundamental pattern for an Apex agent action:

public class GetOpportunityDetails {

    public class ActionInput {
        @InvocableVariable(
            label='Opportunity ID'
            description='The 18-character Salesforce ID of the Opportunity to retrieve'
            required=true
        )
        public String opportunityId;
    }

    public class ActionOutput {
        @InvocableVariable(label='Opportunity Name' description='The name of the opportunity')
        public String opportunityName;

        @InvocableVariable(label='Amount' description='The total deal amount in USD')
        public Decimal amount;

        @InvocableVariable(label='Stage' description='The current stage of the opportunity')
        public String stage;

        @InvocableVariable(label='Close Date' description='The expected close date')
        public Date closeDate;

        @InvocableVariable(label='Discount Percentage' description='The discount applied to this deal as a percentage')
        public Decimal discountPercentage;

        @InvocableVariable(label='Account Name' description='The name of the related account')
        public String accountName;

        @InvocableVariable(label='Error Message' description='If an error occurred, this contains the error details')
        public String errorMessage;
    }

    @InvocableMethod(
        label='Get Opportunity Details'
        description='Retrieves detailed information about a specific Opportunity including amount, stage, close date, discount, and related account name. Use this when the user asks about a specific deal or opportunity.'
    )
    public static List<ActionOutput> getDetails(List<ActionInput> inputs) {
        List<ActionOutput> outputs = new List<ActionOutput>();

        for (ActionInput input : inputs) {
            ActionOutput output = new ActionOutput();

            try {
                Opportunity opp = [
                    SELECT Name, Amount, StageName, CloseDate,
                           Discount_Percentage__c, Account.Name
                    FROM Opportunity
                    WHERE Id = :input.opportunityId
                    LIMIT 1
                ];

                output.opportunityName = opp.Name;
                output.amount = opp.Amount;
                output.stage = opp.StageName;
                output.closeDate = opp.CloseDate;
                output.discountPercentage = opp.Discount_Percentage__c;
                output.accountName = opp.Account.Name;

            } catch (QueryException e) {
                output.errorMessage = 'No opportunity found with the provided ID.';
            } catch (Exception e) {
                output.errorMessage = 'An unexpected error occurred: ' + e.getMessage();
            }

            outputs.add(output);
        }

        return outputs;
    }
}

Input and Output Design Principles

When building actions for agents, follow these guidelines:

Inputs:

  • Use the fewest inputs possible. The LLM has to figure out what value to provide for every input. If your action takes 10 inputs, the agent is more likely to make mistakes.
  • Mark truly required inputs as required=true. Optional inputs should have sensible defaults in your Apex logic.
  • Use primitive types when possible (String, Decimal, Boolean, Date). Complex types are harder for the LLM to construct.
  • Write descriptions that explain the expected format: “The 18-character Salesforce ID” is better than just “The ID.”

Outputs:

  • Return multiple labeled fields rather than a single concatenated string. The LLM reasons better about structured data.
  • Always include an errorMessage field. If something goes wrong, the agent can read the error and tell the user what happened instead of silently failing.
  • Include units and context in descriptions: “The total deal amount in USD” is better than “The amount.”
  • Keep output field names intuitive. The LLM sees these labels and incorporates them into its response.

Error Handling for Agent Actions

Error handling in agent actions is different from error handling in triggers or batch jobs. When an agent action fails, the agent needs to understand what went wrong so it can communicate the issue to the user or try a different approach.

public class SafeAgentAction {

    public class ActionInput {
        @InvocableVariable(label='Record ID' description='The ID of the record to process' required=true)
        public String recordId;
    }

    public class ActionOutput {
        @InvocableVariable(label='Success' description='Whether the action completed successfully')
        public Boolean success;

        @InvocableVariable(label='Result Message' description='A human-readable summary of the result')
        public String resultMessage;

        @InvocableVariable(label='Error Message' description='Details about what went wrong, if anything')
        public String errorMessage;
    }

    @InvocableMethod(label='Safe Agent Action' description='A template for error-safe agent actions')
    public static List<ActionOutput> execute(List<ActionInput> inputs) {
        List<ActionOutput> outputs = new List<ActionOutput>();

        for (ActionInput input : inputs) {
            ActionOutput output = new ActionOutput();

            if (String.isBlank(input.recordId)) {
                output.success = false;
                output.errorMessage = 'Record ID was not provided. Please ask the user for the record ID.';
                outputs.add(output);
                continue;
            }

            try {
                // Business logic here
                output.success = true;
                output.resultMessage = 'The operation completed successfully.';

            } catch (QueryException e) {
                output.success = false;
                output.errorMessage = 'The record was not found. The ID may be incorrect.';
            } catch (DmlException e) {
                output.success = false;
                output.errorMessage = 'Could not update the record: ' + e.getDmlMessage(0);
            } catch (Exception e) {
                output.success = false;
                output.errorMessage = 'An unexpected error occurred: ' + e.getMessage();
            }

            outputs.add(output);
        }

        return outputs;
    }
}

Notice the pattern: every output includes success, resultMessage, and errorMessage. The agent can check success to decide its next step. If success is false, it reads errorMessage and either asks the user for corrected input or explains the problem.

Returning Structured Responses for LLM Reasoning

For complex actions, you may want to return a rich analysis rather than raw fields. Consider returning a summary string that the LLM can incorporate directly into its response, alongside the raw data fields for precise reasoning:

public class ActionOutput {
    @InvocableVariable(label='Win Probability' description='Calculated probability of winning this deal, from 0 to 100')
    public Decimal winProbability;

    @InvocableVariable(label='Risk Level' description='LOW, MEDIUM, or HIGH based on the analysis')
    public String riskLevel;

    @InvocableVariable(label='Analysis Summary' description='A brief narrative summary of the deal analysis')
    public String analysisSummary;

    @InvocableVariable(label='Recommended Actions' description='A comma-separated list of recommended next steps')
    public String recommendedActions;

    @InvocableVariable(label='Requires Approval' description='True if the deal exceeds discount or amount thresholds requiring management approval')
    public Boolean requiresApproval;

    @InvocableVariable(label='Error Message' description='Error details if the analysis could not be completed')
    public String errorMessage;
}

The analysisSummary gives the agent a pre-built narrative it can adapt. The recommendedActions as a comma-separated list is easy for the LLM to parse and present as bullet points. The requiresApproval boolean gives the agent a clear signal to include an approval warning in its response.


PROJECT: Deal Advisor Agent

Let us build a complete working project. We will create a Deal Advisor agent that helps sales reps analyze their opportunities. The agent will use custom Apex actions to query data, calculate win probability, check discount limits, and recommend next steps.

Project Overview

The Deal Advisor agent has one topic — Opportunity Analysis — with three custom Apex actions:

  1. Get Opportunity Details — retrieves full details about an opportunity.
  2. Calculate Win Probability — scores the opportunity based on stage, amount, close date proximity, and engagement history.
  3. Check Discount Limits — evaluates whether the applied discount needs approval and calculates the maximum allowed discount.

The agent will orchestrate these actions based on the user’s request. If a user says “Analyze the Acme deal,” the agent will call all three actions and synthesize a comprehensive recommendation.

Prerequisites

For this project, we need a custom field on the Opportunity object:

  • Discount_Percentage__c (Number, 5 decimal places, 2) — the discount applied to the deal.

If your org does not have this field, create it in Setup under Object Manager > Opportunity > Fields & Relationships > New.

We will also use the standard fields: Name, Amount, StageName, CloseDate, Probability, and the related Account.Name.

Action 1: GetOpportunityDetails

We already built this class earlier in the post. Here is the complete version for reference:

public class GetOpportunityDetails {

    public class ActionInput {
        @InvocableVariable(
            label='Opportunity ID'
            description='The 18-character Salesforce ID of the Opportunity to retrieve'
            required=true
        )
        public String opportunityId;
    }

    public class ActionOutput {
        @InvocableVariable(label='Opportunity Name' description='The name of the opportunity')
        public String opportunityName;

        @InvocableVariable(label='Amount' description='The total deal amount in USD')
        public Decimal amount;

        @InvocableVariable(label='Stage' description='The current sales stage of the opportunity')
        public String stage;

        @InvocableVariable(label='Close Date' description='The expected close date in YYYY-MM-DD format')
        public Date closeDate;

        @InvocableVariable(label='Discount Percentage' description='The discount percentage applied to this deal')
        public Decimal discountPercentage;

        @InvocableVariable(label='Account Name' description='The name of the account associated with this opportunity')
        public String accountName;

        @InvocableVariable(label='Owner Name' description='The full name of the opportunity owner')
        public String ownerName;

        @InvocableVariable(label='Days Until Close' description='Number of days between today and the close date')
        public Integer daysUntilClose;

        @InvocableVariable(label='Error Message' description='Error details if the retrieval failed')
        public String errorMessage;
    }

    @InvocableMethod(
        label='Get Opportunity Details'
        description='Retrieves comprehensive details about a specific Opportunity. Use this when the user mentions a deal, opportunity, or asks for deal information.'
    )
    public static List<ActionOutput> getDetails(List<ActionInput> inputs) {
        List<ActionOutput> outputs = new List<ActionOutput>();

        for (ActionInput input : inputs) {
            ActionOutput output = new ActionOutput();

            try {
                Opportunity opp = [
                    SELECT Name, Amount, StageName, CloseDate,
                           Discount_Percentage__c, Account.Name,
                           Owner.Name
                    FROM Opportunity
                    WHERE Id = :input.opportunityId
                    LIMIT 1
                ];

                output.opportunityName = opp.Name;
                output.amount = opp.Amount;
                output.stage = opp.StageName;
                output.closeDate = opp.CloseDate;
                output.discountPercentage = opp.Discount_Percentage__c != null
                    ? opp.Discount_Percentage__c : 0;
                output.accountName = opp.Account.Name;
                output.ownerName = opp.Owner.Name;
                output.daysUntilClose = Date.today().daysBetween(opp.CloseDate);

            } catch (QueryException e) {
                output.errorMessage = 'No opportunity found with ID: ' + input.opportunityId;
            } catch (Exception e) {
                output.errorMessage = 'Error retrieving opportunity: ' + e.getMessage();
            }

            outputs.add(output);
        }

        return outputs;
    }
}

Action 2: CalculateWinProbability

This action implements a scoring algorithm that goes beyond the standard Opportunity probability field. It factors in stage progression, deal size, close date proximity, and activity history:

public class CalculateWinProbability {

    public class ActionInput {
        @InvocableVariable(
            label='Opportunity ID'
            description='The Salesforce ID of the Opportunity to analyze'
            required=true
        )
        public String opportunityId;
    }

    public class ActionOutput {
        @InvocableVariable(label='Win Probability Score' description='Calculated probability of winning, from 0 to 100')
        public Decimal winProbabilityScore;

        @InvocableVariable(label='Risk Level' description='LOW, MEDIUM, or HIGH risk assessment')
        public String riskLevel;

        @InvocableVariable(label='Score Breakdown' description='Explanation of how the score was calculated')
        public String scoreBreakdown;

        @InvocableVariable(label='Key Risk Factors' description='Comma-separated list of identified risk factors')
        public String keyRiskFactors;

        @InvocableVariable(label='Error Message' description='Error details if the calculation failed')
        public String errorMessage;
    }

    @InvocableMethod(
        label='Calculate Win Probability'
        description='Calculates a detailed win probability score for an opportunity based on stage, amount, close date, and engagement history. Use when the user asks about the likelihood of closing a deal.'
    )
    public static List<ActionOutput> calculate(List<ActionInput> inputs) {
        List<ActionOutput> outputs = new List<ActionOutput>();

        for (ActionInput input : inputs) {
            ActionOutput output = new ActionOutput();

            try {
                Opportunity opp = [
                    SELECT Id, StageName, Amount, CloseDate, Probability,
                           Discount_Percentage__c,
                           (SELECT Id FROM Tasks WHERE Status = 'Completed'),
                           (SELECT Id FROM Events WHERE EndDateTime < :Datetime.now())
                    FROM Opportunity
                    WHERE Id = :input.opportunityId
                    LIMIT 1
                ];

                Decimal score = 0;
                List<String> breakdown = new List<String>();
                List<String> riskFactors = new List<String>();

                // Factor 1: Stage progression (up to 40 points)
                Decimal stageScore = getStageScore(opp.StageName);
                score += stageScore;
                breakdown.add('Stage (' + opp.StageName + '): +' + stageScore + ' points');

                // Factor 2: Close date proximity (up to 20 points)
                Integer daysUntilClose = Date.today().daysBetween(opp.CloseDate);
                Decimal dateScore = 0;

                if (daysUntilClose < 0) {
                    dateScore = 0;
                    riskFactors.add('Close date has passed');
                } else if (daysUntilClose <= 30) {
                    dateScore = 20;
                } else if (daysUntilClose <= 60) {
                    dateScore = 15;
                } else if (daysUntilClose <= 90) {
                    dateScore = 10;
                } else {
                    dateScore = 5;
                    riskFactors.add('Close date is more than 90 days away');
                }

                score += dateScore;
                breakdown.add('Close Date Proximity (' + daysUntilClose + ' days): +' + dateScore + ' points');

                // Factor 3: Engagement level (up to 25 points)
                Integer completedTasks = opp.Tasks.size();
                Integer pastEvents = opp.Events.size();
                Integer totalActivities = completedTasks + pastEvents;
                Decimal engagementScore = 0;

                if (totalActivities >= 10) {
                    engagementScore = 25;
                } else if (totalActivities >= 5) {
                    engagementScore = 18;
                } else if (totalActivities >= 2) {
                    engagementScore = 10;
                } else {
                    engagementScore = 3;
                    riskFactors.add('Very low engagement activity (' + totalActivities + ' activities)');
                }

                score += engagementScore;
                breakdown.add('Engagement (' + totalActivities + ' activities): +' + engagementScore + ' points');

                // Factor 4: Deal size risk (up to 15 points)
                Decimal sizeScore = 15;
                if (opp.Amount != null && opp.Amount > 500000) {
                    sizeScore = 8;
                    riskFactors.add('Large deal size ($' + opp.Amount.format() + ') increases complexity');
                } else if (opp.Amount != null && opp.Amount > 100000) {
                    sizeScore = 12;
                }

                score += sizeScore;
                breakdown.add('Deal Size: +' + sizeScore + ' points');

                // Determine risk level
                if (score >= 70) {
                    output.riskLevel = 'LOW';
                } else if (score >= 45) {
                    output.riskLevel = 'MEDIUM';
                } else {
                    output.riskLevel = 'HIGH';
                }

                output.winProbabilityScore = score;
                output.scoreBreakdown = String.join(breakdown, '; ');
                output.keyRiskFactors = riskFactors.isEmpty()
                    ? 'No significant risk factors identified'
                    : String.join(riskFactors, ', ');

            } catch (QueryException e) {
                output.errorMessage = 'Opportunity not found with the provided ID.';
            } catch (Exception e) {
                output.errorMessage = 'Error calculating win probability: ' + e.getMessage();
            }

            outputs.add(output);
        }

        return outputs;
    }

    private static Decimal getStageScore(String stage) {
        Map<String, Decimal> stageScores = new Map<String, Decimal>{
            'Prospecting' => 5,
            'Qualification' => 10,
            'Needs Analysis' => 15,
            'Value Proposition' => 20,
            'Id. Decision Makers' => 25,
            'Perception Analysis' => 28,
            'Proposal/Price Quote' => 32,
            'Negotiation/Review' => 36,
            'Closed Won' => 40,
            'Closed Lost' => 0
        };

        return stageScores.containsKey(stage) ? stageScores.get(stage) : 10;
    }
}

Action 3: CheckDiscountLimits

This action evaluates the discount against company policy and determines whether approval is required:

public class CheckDiscountLimits {

    public class ActionInput {
        @InvocableVariable(
            label='Opportunity ID'
            description='The Salesforce ID of the Opportunity to check discount limits for'
            required=true
        )
        public String opportunityId;
    }

    public class ActionOutput {
        @InvocableVariable(label='Current Discount' description='The current discount percentage on the deal')
        public Decimal currentDiscount;

        @InvocableVariable(label='Maximum Allowed Discount' description='The maximum discount allowed without additional approval')
        public Decimal maxAllowedDiscount;

        @InvocableVariable(label='Requires Approval' description='True if the current discount exceeds the allowed limit')
        public Boolean requiresApproval;

        @InvocableVariable(label='Approval Level' description='The approval level required: NONE, MANAGER, VP, or C-LEVEL')
        public String approvalLevel;

        @InvocableVariable(label='Discount Analysis' description='Summary of the discount evaluation')
        public String discountAnalysis;

        @InvocableVariable(label='Revenue Impact' description='Estimated revenue impact of the discount in USD')
        public Decimal revenueImpact;

        @InvocableVariable(label='Error Message' description='Error details if the check failed')
        public String errorMessage;
    }

    @InvocableMethod(
        label='Check Discount Limits'
        description='Evaluates the discount on an opportunity against company discount policies and determines what approval level is required. Use when the user asks about discounts, pricing, or approval requirements.'
    )
    public static List<ActionOutput> checkLimits(List<ActionInput> inputs) {
        List<ActionOutput> outputs = new List<ActionOutput>();

        for (ActionInput input : inputs) {
            ActionOutput output = new ActionOutput();

            try {
                Opportunity opp = [
                    SELECT Amount, Discount_Percentage__c, StageName
                    FROM Opportunity
                    WHERE Id = :input.opportunityId
                    LIMIT 1
                ];

                Decimal discount = opp.Discount_Percentage__c != null
                    ? opp.Discount_Percentage__c : 0;
                Decimal amount = opp.Amount != null ? opp.Amount : 0;

                output.currentDiscount = discount;
                output.revenueImpact = (amount * discount) / 100;

                // Discount policy tiers
                // Deals under $50K: up to 15% without approval
                // Deals $50K-$200K: up to 10% without approval
                // Deals over $200K: up to 5% without approval
                Decimal maxDiscount;
                if (amount < 50000) {
                    maxDiscount = 15;
                } else if (amount < 200000) {
                    maxDiscount = 10;
                } else {
                    maxDiscount = 5;
                }

                output.maxAllowedDiscount = maxDiscount;

                if (discount <= maxDiscount) {
                    output.requiresApproval = false;
                    output.approvalLevel = 'NONE';
                    output.discountAnalysis = 'The discount of ' + discount + '% is within the allowed limit of '
                        + maxDiscount + '% for a deal of this size. No approval is required.';
                } else if (discount <= maxDiscount + 10) {
                    output.requiresApproval = true;
                    output.approvalLevel = 'MANAGER';
                    output.discountAnalysis = 'The discount of ' + discount + '% exceeds the standard limit of '
                        + maxDiscount + '% by ' + (discount - maxDiscount) + ' percentage points. '
                        + 'Manager approval is required. Revenue impact: $'
                        + output.revenueImpact.format();
                } else if (discount <= maxDiscount + 20) {
                    output.requiresApproval = true;
                    output.approvalLevel = 'VP';
                    output.discountAnalysis = 'The discount of ' + discount + '% significantly exceeds the standard limit. '
                        + 'VP-level approval is required. Revenue impact: $'
                        + output.revenueImpact.format();
                } else {
                    output.requiresApproval = true;
                    output.approvalLevel = 'C-LEVEL';
                    output.discountAnalysis = 'The discount of ' + discount + '% is extremely high and requires '
                        + 'C-level approval. Consider renegotiating the deal terms. Revenue impact: $'
                        + output.revenueImpact.format();
                }

            } catch (QueryException e) {
                output.errorMessage = 'Opportunity not found with the provided ID.';
            } catch (Exception e) {
                output.errorMessage = 'Error checking discount limits: ' + e.getMessage();
            }

            outputs.add(output);
        }

        return outputs;
    }
}

Writing Test Classes for Agent Actions

Agent actions are Apex classes, so they require test coverage. Here is a test class that covers all three actions:

@IsTest
private class DealAdvisorActionsTest {

    @TestSetup
    static void setupData() {
        Account acc = new Account(Name = 'Test Corp');
        insert acc;

        Opportunity opp = new Opportunity(
            Name = 'Test Deal',
            AccountId = acc.Id,
            StageName = 'Proposal/Price Quote',
            CloseDate = Date.today().addDays(30),
            Amount = 150000,
            Discount_Percentage__c = 12
        );
        insert opp;

        Task t = new Task(
            WhatId = opp.Id,
            Subject = 'Follow up call',
            Status = 'Completed'
        );
        insert t;

        Event e = new Event(
            WhatId = opp.Id,
            Subject = 'Demo meeting',
            StartDateTime = Datetime.now().addDays(-5),
            EndDateTime = Datetime.now().addDays(-5).addHours(1)
        );
        insert e;
    }

    @IsTest
    static void testGetOpportunityDetails() {
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];

        GetOpportunityDetails.ActionInput input = new GetOpportunityDetails.ActionInput();
        input.opportunityId = opp.Id;

        List<GetOpportunityDetails.ActionOutput> results =
            GetOpportunityDetails.getDetails(new List<GetOpportunityDetails.ActionInput>{ input });

        System.assertEquals(1, results.size(), 'Should return one result');
        System.assertEquals('Test Deal', results[0].opportunityName, 'Name should match');
        System.assertEquals(150000, results[0].amount, 'Amount should match');
        System.assertEquals('Proposal/Price Quote', results[0].stage, 'Stage should match');
        System.assertEquals('Test Corp', results[0].accountName, 'Account name should match');
        System.assertNotEquals(null, results[0].daysUntilClose, 'Days until close should be calculated');
        System.assertEquals(null, results[0].errorMessage, 'No error expected');
    }

    @IsTest
    static void testGetOpportunityDetailsInvalidId() {
        GetOpportunityDetails.ActionInput input = new GetOpportunityDetails.ActionInput();
        input.opportunityId = '006000000000000AAA';

        List<GetOpportunityDetails.ActionOutput> results =
            GetOpportunityDetails.getDetails(new List<GetOpportunityDetails.ActionInput>{ input });

        System.assertNotEquals(null, results[0].errorMessage, 'Should return an error message');
    }

    @IsTest
    static void testCalculateWinProbability() {
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];

        CalculateWinProbability.ActionInput input = new CalculateWinProbability.ActionInput();
        input.opportunityId = opp.Id;

        List<CalculateWinProbability.ActionOutput> results =
            CalculateWinProbability.calculate(new List<CalculateWinProbability.ActionInput>{ input });

        System.assertEquals(1, results.size(), 'Should return one result');
        System.assertNotEquals(null, results[0].winProbabilityScore, 'Score should be calculated');
        System.assert(results[0].winProbabilityScore > 0, 'Score should be positive');
        System.assertNotEquals(null, results[0].riskLevel, 'Risk level should be set');
        System.assertNotEquals(null, results[0].scoreBreakdown, 'Breakdown should be provided');
        System.assertEquals(null, results[0].errorMessage, 'No error expected');
    }

    @IsTest
    static void testCheckDiscountLimitsWithinLimit() {
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        opp.Discount_Percentage__c = 5;
        update opp;

        CheckDiscountLimits.ActionInput input = new CheckDiscountLimits.ActionInput();
        input.opportunityId = opp.Id;

        List<CheckDiscountLimits.ActionOutput> results =
            CheckDiscountLimits.checkLimits(new List<CheckDiscountLimits.ActionInput>{ input });

        System.assertEquals(false, results[0].requiresApproval, 'Should not require approval');
        System.assertEquals('NONE', results[0].approvalLevel, 'Approval level should be NONE');
    }

    @IsTest
    static void testCheckDiscountLimitsExceedsLimit() {
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        opp.Discount_Percentage__c = 18;
        update opp;

        CheckDiscountLimits.ActionInput input = new CheckDiscountLimits.ActionInput();
        input.opportunityId = opp.Id;

        List<CheckDiscountLimits.ActionOutput> results =
            CheckDiscountLimits.checkLimits(new List<CheckDiscountLimits.ActionInput>{ input });

        System.assertEquals(true, results[0].requiresApproval, 'Should require approval');
        System.assertEquals('MANAGER', results[0].approvalLevel, 'Should need manager approval');
    }

    @IsTest
    static void testCheckDiscountLimitsVPApproval() {
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        opp.Discount_Percentage__c = 28;
        update opp;

        CheckDiscountLimits.ActionInput input = new CheckDiscountLimits.ActionInput();
        input.opportunityId = opp.Id;

        List<CheckDiscountLimits.ActionOutput> results =
            CheckDiscountLimits.checkLimits(new List<CheckDiscountLimits.ActionInput>{ input });

        System.assertEquals(true, results[0].requiresApproval, 'Should require approval');
        System.assertEquals('VP', results[0].approvalLevel, 'Should need VP approval');
    }
}

Configuring the Agent in Setup

Now that the Apex actions are deployed, let us wire them up to the agent:

  1. Create the agent — Navigate to Setup > Agentforce > Agents. Click New Agent. Name it Deal Advisor. Select Custom Agent as the type.

  2. Create the topic — Within the agent, create a topic called Opportunity Analysis. Set the description to: Handles questions about specific opportunities including deal analysis, win probability, discount evaluation, pricing approval requirements, and next-step recommendations.

  3. Write the topic instructions:

You are the Deal Advisor, an AI assistant that helps sales representatives analyze their opportunities and make informed decisions.

When a user asks about a deal or opportunity:
1. Start by using the Get Opportunity Details action to retrieve the full deal information.
2. Present a brief summary of the deal including the name, account, amount, stage, and close date.
3. If the user asks about win probability or likelihood of closing, use the Calculate Win Probability action.
4. If the user asks about discounts or pricing, use the Check Discount Limits action.
5. If the user asks for a full analysis, run all three actions and provide a comprehensive assessment.

Business rules:
- Always warn the user when a deal requires discount approval. Be specific about the approval level needed.
- When the win probability risk level is HIGH, recommend specific actions to improve the deal.
- Format dollar amounts with commas and two decimal places.
- Format percentages with one decimal place.
- If an action returns an error, explain the issue to the user and ask them to verify their input.
- Never fabricate data. Only present information returned by the actions.

Recommended next steps by stage:
- Prospecting or Qualification: Schedule a discovery call, identify stakeholders.
- Needs Analysis or Value Proposition: Send a tailored case study, schedule a demo.
- Id. Decision Makers or Perception Analysis: Arrange an executive sponsor meeting, send ROI analysis.
- Proposal/Price Quote: Follow up within 48 hours, prepare negotiation talking points.
- Negotiation/Review: Confirm decision timeline, address final objections, prepare contract.
  1. Add the three actions — Add Get Opportunity Details, Calculate Win Probability, and Check Discount Limits as actions within the topic. For each action, configure the input so the agent knows to pass the Opportunity ID.

  2. Activate — Click Activate on the agent page.

Testing the Agent

Open the Agent Builder preview or use the VS Code extension to test. Here are some sample conversations to try:

Simple retrieval:

User: “Tell me about opportunity 006xx000001234” Agent: Calls Get Opportunity Details, presents a summary.

Win probability:

User: “What are our chances of closing the Acme renewal?” Agent: Asks for the Opportunity ID or name, then calls Calculate Win Probability.

Full analysis:

User: “Give me a full analysis of this deal: 006xx000001234” Agent: Calls all three actions, synthesizes a comprehensive report with deal details, win probability, discount status, and recommended next steps.

Discount check:

User: “Does the current discount on the Acme deal need approval?” Agent: Calls Check Discount Limits, reports the approval requirement and level.

Best Practices for Agent Actions

Here is a summary of the key principles we applied in this project:

  1. One responsibility per action. Each action does one thing well. The agent decides how to combine them.
  2. Descriptive labels and descriptions. The LLM relies on these to understand when and how to use each action.
  3. Structured outputs with labeled fields. Give the agent distinct fields to reason about rather than a single blob of text.
  4. Always include error handling. Return error messages the agent can relay to the user.
  5. Keep inputs minimal. The fewer inputs the agent must fill, the fewer mistakes it makes.
  6. Include pre-built narratives. Fields like discountAnalysis and scoreBreakdown give the agent ready-made summaries it can incorporate.
  7. Use consistent patterns. All three actions follow the same input/output structure with errorMessage. Consistency makes your codebase easier to maintain and the agent’s behavior more predictable.
  8. Test thoroughly. Agent actions are Apex — they need test classes and code coverage like any other Apex class.

Summary

Agentforce represents a fundamental shift in how we build on Salesforce. Instead of mapping every decision branch in a Flow or hard-coding every response in a bot, we define topics, write instructions in natural language, and provide well-designed Apex actions that the agent orchestrates at runtime. The developer’s role moves from building rigid control flows to crafting clear, structured building blocks that an LLM can reason about.

In this post we covered the Agentforce architecture (topics, actions, instructions, and LLM grounding), set up the Agentforce for Developers VS Code extension, created an agent with topics and instructions in Setup, built three custom Apex agent actions following the invocable method pattern with structured inputs and outputs, and tied it all together in a Deal Advisor project that queries opportunities, calculates win probability, checks discount limits, and recommends next steps.

The combination of declarative agent configuration and programmatic Apex actions gives you a powerful toolkit. The agent handles the conversation and reasoning layer, while your Apex handles the data access and business logic layer.


In Part 60, we will step back from agents and look at The Major Salesforce APIs — REST, SOAP, Bulk, Streaming, Metadata, Tooling, and Composite. Understanding these APIs is essential for any Salesforce developer working on integrations, tooling, or external applications. See you there.