Custom Agentforce Action with LWC UI

Build a custom Agentforce action with branded LWC input and output components.

Introduction

30 Second Summary

Think about how frustrating it is when a chatbot can only respond with plain text. You ask for account details and get a wall of unformatted words instead of a clean, structured card you can actually read at a glance.

In this project, you will build a custom Agentforce agent action that replaces the default text-based UI with rich, branded components for both input and output inside the agent chat.

What You'll Build

A fully functional Agentforce Employee agent with a custom Apex-backed action and Lightning Web Components that render a branded search form and styled account card directly inside the agent conversation.

By the end of this project, you'll have:

  • An Apex class with @InvocableMethod that queries Account records on demand.
  • Custom Lightning Type Bundles with schema, editor, and renderer metadata wired to your agent action.
  • Custom LWC components that render a rich input form and styled output card inside the Agentforce chat.
  • Secret Mission: Add an account update action with a custom edit form rendered inside the chat.

Are there any prerequisites?

This project assumes working knowledge of Apex and Lightning Web Components, plus familiarity with the Salesforce DX project structure. You will also need a Salesforce Developer Edition org with Agentforce enabled.

Before We Start

Before you can build custom UI for your Agentforce agent, you need a Salesforce DX project connected to a Developer Edition org with the right features enabled.

In this step, you'll scaffold your project, authorize your org, and confirm that Einstein and Agentforce are ready for development.

In this step, get ready to:
  • Create a Salesforce DX project for your custom agent code.
  • Authorize your Developer Edition org as the deployment target.
  • Confirm Einstein and Agentforce are enabled in your org.
Create a new Salesforce DX project and authorize your org

The Salesforce CLI (sf) scaffolds a standard DX project structure with all the folders you need for Apex classes, LWC components, and metadata.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project by running this command:
sf project generate --name agentforce-custom-ui

What does this command do?

sf project generate creates a standard Salesforce DX project folder with the directories you need for source-format metadata.

The --name flag sets the project folder name and the name field in sfdx-project.json.

  • You should see a message confirming the project was created. Move into the new project folder by running:
cd agentforce-custom-ui
  • Authorize your Developer Edition org by running this command:
sf org login web --alias agentforce-dev --set-default

What happens next?

This command opens your browser to the Salesforce login page. Sign in with your Developer Edition credentials.

The --alias flag gives this org a short name (agentforce-dev) so you can reference it in later commands. The --set-default flag makes it your default org for deployments.

  • Sign in with your Developer Edition credentials when the browser opens.
  • Click Allow when prompted to grant access.
  • Return to your terminal. You should see a success message confirming the org is authorized.

Browser didn't open?

Check that your terminal isn't blocking pop-ups. Try running the command again.

If you're on a corporate network, your browser may need a manual URL paste. Look for a URL printed in the terminal output and open it directly.

Help me troubleshoot my org authorization

Confirm Einstein and Agentforce are enabled

Custom Lightning Types and agent actions require both Einstein and Agentforce to be active in your org. Both come pre-enabled in new Developer Edition orgs, but it's worth confirming.

  • In your Salesforce org, click the gear icon in the top-right corner and select Setup.
  • In the Quick Find search box on the left, type Einstein Setup.
  • Select Einstein Setup from the results.
  • Verify the toggle shows Einstein is enabled. If it is not, toggle it on.
  • Back in Quick Find, type Agentforce Agents.
  • Select Agentforce Agents from the results and confirm the feature is active.
  • If either toggle was off, enable it and refresh the page to confirm the change took effect.

Don't see Agentforce Agents in Quick Find?

Make sure you are using a Developer Edition org that was created after March 2025. Older Developer Edition orgs may not include Agentforce.

If you need a new org, sign up at developer.salesforce.com for a free Developer Edition with Agentforce and Data Cloud included.

Help me enable Agentforce in my org

Verify your project structure is correct

Custom Lightning Types require API version 64.0 or higher. Your CLI generates the project with its current API version, but you should confirm this before writing any code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the agentforce-custom-ui folder, then click Open.
  • In the Explorer sidebar on the left, expand the force-app folder, then main, then default.
  • Confirm you can see the force-app/main/default/ directory. This is where all your Apex classes, LWC components, and Lightning Type metadata will live.
  • Click on sfdx-project.json in the root of the Explorer to open it.
  • Find the sourceApiVersion field and confirm it is set to 64.0 or higher.

Why does the API version matter?

The LightningTypeBundle metadata type (which powers Custom Lightning Types) was introduced in API version 64.0. If your project targets an older API version, deployments of your type bundles will fail.

Your sfdx-project.json should look similar to this:

{
  "packageDirectories": [
    {
      "path": "force-app",
      "default": true
    }
  ],
  "name": "agentforce-custom-ui",
  "namespace": "",
  "sfdcLoginUrl": "https://login.salesforce.com",
  "sourceApiVersion": "67.0"
}

What should I see?

Your sourceApiVersion might show 67.0 or another version depending on your CLI. Any value of 64.0 or higher is correct.

Seeing a version lower than 64.0?

Your Salesforce CLI may be outdated. Update it by running sf update in your terminal, then regenerate the project.

Alternatively, you can manually edit sfdx-project.json and change sourceApiVersion to "64.0" or higher.

Help me fix my API version

Your project is scaffolded, your org is authorized, and both Einstein and Agentforce are confirmed active. Next up, you'll create the Apex class that powers your custom agent action.

Set Up Your Project and Authorize Your Org

To build a custom Agentforce agent with rich UI, you need a Salesforce DX project connected to an org where Agentforce is active.

In this step, you'll scaffold a new SFDX project, authorize your Developer Edition org, and confirm that Einstein and Agentforce are switched on and ready.

In this step, get ready to:
  • Create a Salesforce DX project and authorize your org.
  • Confirm Einstein and Agentforce are enabled.
  • Verify your project structure is correct.
Create a new Salesforce DX project and authorize your org

The Salesforce CLI scaffolds your project with the correct folder structure for source-format metadata.

  • Open your terminal.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project by running this command:
sf project generate --name agentforce-custom-ui --api-version 64.0

What does this command do?

The sf project generate command scaffolds a standard Salesforce DX project structure.

The --name flag sets the project folder name. The --api-version 64.0 flag pins the sourceApiVersion in sfdx-project.json to 64.0. Custom Lightning Types require API version 64.0 or later.

  • Confirm the project folder was created by running:
ls

You should see agentforce-custom-ui listed in the output.

  • Move into the project folder by running:
cd agentforce-custom-ui
  • Authorize your Developer Edition org by running:
sf org login web --alias agentforce-dev --set-default

What happens during authorization?

This command opens a browser window where you sign in to your Salesforce Developer Edition org. After you sign in, the CLI stores the authorization locally so future commands target this org by default.

The --alias agentforce-dev flag gives the org a friendly name. The --set-default flag means you won't need to specify the org on every command.

  • Sign in with your Developer Edition credentials in the browser window that opens.
  • Return to your terminal after the browser confirms success.

You should see a success message in the terminal showing your org's username and instance URL.

Browser didn't open?

If no browser window appeared, copy the URL printed in the terminal and paste it into your browser manually. Make sure you're signing in to your Developer Edition org, not a production org or sandbox.

If you see an authentication error, verify you're using the correct username and password for your Developer Edition org.

Help me troubleshoot my Salesforce CLI org authorization

Confirm Einstein and Agentforce are enabled

Your org needs both Einstein and Agentforce active for the agent to work. Both features come enabled in the new Developer Edition, but it's worth confirming before you build.

  • In your Salesforce org, click the gear icon in the top right and select Setup.
  • In the Quick Find box on the left, type Einstein Setup.
  • Select Einstein Setup from the results.
  • Verify that Einstein is toggled on. If it is not, toggle it on and refresh the page.
  • Back in the Quick Find box, type Agentforce Agents.
  • Select Agentforce Agents from the results.
  • Confirm Agentforce is enabled on this page. If it is not, toggle it on (you may need to refresh the page afterward).

Why do both need to be on?

Einstein provides the underlying AI foundation (large language model access, reasoning). Agentforce is the orchestration layer that routes user messages to your custom actions.

Without Einstein, the agent has no LLM to power its responses. Without Agentforce, there's no framework to wire your Apex actions and custom UI into a conversational experience.

Don't see Agentforce Agents in Quick Find?

Make sure you are using a Developer Edition org that includes Agentforce. Older Developer Edition orgs created before March 2025 may not have this feature.

If you need a new org, sign up at developer.salesforce.com for a free Developer Edition with Agentforce and Data Cloud included.

Help me enable Agentforce in my Developer Edition org

Verify your project structure is correct

Before writing any code, confirm the CLI generated the correct folder structure and API version for your project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the agentforce-custom-ui folder, and click Open.
  • In the Explorer sidebar on the left, expand force-app > main > default.
  • Confirm you see subfolders like classes, lwc, and aura. This is where all your metadata will live.
  • Open sfdx-project.json from the project root (click it in the Explorer sidebar).
  • Verify the sourceApiVersion value is "64.0". This is the minimum required for LightningTypeBundle metadata.

Why API version 64.0?

Custom Lightning Types (LightningTypeBundle metadata) became available in API version 64.0. Any version at or above 64.0 works, but 64.0 is the minimum.

By pinning to 64.0 explicitly, your project remains compatible across different org release versions while still supporting the metadata types you need.

Seeing a different API version?

If your sourceApiVersion shows a value other than "64.0", open sfdx-project.json and change the value to "64.0". Save the file.

Help me fix my sourceApiVersion in sfdx-project.json

Your project is scaffolded, your org is connected, and the features you need are live. Next up, you'll write the Apex class that powers your agent's account search action.

Create the Apex Action Class

Your project is scaffolded and your org is authorized. Now it is time to build the brain of your agent action.

Agentforce needs Apex methods marked with @InvocableMethod to know which logic it can call as an action. In this step, you will create an Apex class that queries Account records by name and returns the results in a structured format that your custom UI can render later.

In this step, get ready to:
  • Create an Apex class with inner classes defining input and output data shapes.
  • Implement the @InvocableMethod that queries Account records by name.
  • Deploy and verify the class in your org.
Create the Apex class with @InvocableMethod

The @InvocableMethod annotation exposes an Apex method as a configurable action inside Agentforce Agent Builder. The method also needs clearly defined input and output classes so the agent knows what data to collect and what data to return.

  • In VS Code, create a new file called AccountSearchService.cls inside force-app/main/default/classes/.
  • Paste the following code into AccountSearchService.cls:
public with sharing class AccountSearchService {

    @InvocableMethod(label='Search Accounts' description='Searches for accounts by name and returns matching results')
    public static List<AccountSearchResult> searchAccounts(List<AccountSearchInput> inputs) {
        List<AccountSearchResult> results = new List<AccountSearchResult>();

        for (AccountSearchInput input : inputs) {
            AccountSearchResult result = new AccountSearchResult();
            String searchTerm = '%' + String.escapeSingleQuotes(input.accountName) + '%';
            List<Account> accounts = [
                SELECT Id, Name, Industry, Phone, Website
                FROM Account
                WHERE Name LIKE :searchTerm
                LIMIT 5
            ];

            if (!accounts.isEmpty()) {
                Account acc = accounts[0];
                result.accountId = acc.Id;
                result.accountName = acc.Name;
                result.industry = acc.Industry != null ? acc.Industry : 'Not specified';
                result.phone = acc.Phone != null ? acc.Phone : 'Not available';
                result.website = acc.Website != null ? acc.Website : 'Not available';
                result.found = true;
            } else {
                result.accountName = 'No results';
                result.industry = '';
                result.phone = '';
                result.website = '';
                result.found = false;
            }
            results.add(result);
        }
        return results;
    }

    public class AccountSearchInput {
        @InvocableVariable(required=true label='Account Name' description='The name or partial name of the account to search for')
        public String accountName;
    }

    public class AccountSearchResult {
        @InvocableVariable(label='Account ID' description='The ID of the found account')
        public String accountId;

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

        @InvocableVariable(label='Industry' description='The industry of the found account')
        public String industry;

        @InvocableVariable(label='Phone' description='The phone number of the found account')
        public String phone;

        @InvocableVariable(label='Website' description='The website of the found account')
        public String website;

        @InvocableVariable(label='Found' description='Whether an account was found')
        public Boolean found;
    }
}

What does this code do?

This class has three parts that work together:

  • searchAccounts is the @InvocableMethod that Agentforce calls as an action. It takes an account name, wraps it in wildcards for a SOQL LIKE query, and returns the first matching Account's details.
  • String.escapeSingleQuotes sanitizes the user's input to prevent SOQL injection. This is critical because the search term comes directly from the agent chat.
  • AccountSearchInput defines the data the agent collects (an account name). AccountSearchResult defines the data the agent returns (ID, name, industry, phone, website, and a found flag).
  • The @InvocableVariable annotations on each field give Agentforce metadata it uses to map data between the action, your custom UI, and the LLM.
  • Now create the metadata companion file. Create a new file called AccountSearchService.cls-meta.xml in the same force-app/main/default/classes/ folder.
  • Paste the following into AccountSearchService.cls-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <status>Active</status>
</ApexClass>

Why API version 64.0?

Custom Lightning Types (the rich UI you will build in the next steps) require API version 64.0 or later. Setting this now keeps your entire project aligned on the same version.

  • Save both files.

Don't see the classes folder?

If force-app/main/default/classes/ does not exist yet, create it manually in VS Code's Explorer sidebar. Right-click force-app/main/default/ and select New Folder, then name it classes.

help me create the classes folder in my Salesforce DX project

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete AccountSearchService.cls:

public with sharing class AccountSearchService {

    @InvocableMethod(label='Search Accounts' description='Searches for accounts by name and returns matching results')
    public static List<AccountSearchResult> searchAccounts(List<AccountSearchInput> inputs) {
        List<AccountSearchResult> results = new List<AccountSearchResult>();

        for (AccountSearchInput input : inputs) {
            AccountSearchResult result = new AccountSearchResult();
            String searchTerm = '%' + String.escapeSingleQuotes(input.accountName) + '%';
            List<Account> accounts = [
                SELECT Id, Name, Industry, Phone, Website
                FROM Account
                WHERE Name LIKE :searchTerm
                LIMIT 5
            ];

            if (!accounts.isEmpty()) {
                Account acc = accounts[0];
                result.accountId = acc.Id;
                result.accountName = acc.Name;
                result.industry = acc.Industry != null ? acc.Industry : 'Not specified';
                result.phone = acc.Phone != null ? acc.Phone : 'Not available';
                result.website = acc.Website != null ? acc.Website : 'Not available';
                result.found = true;
            } else {
                result.accountName = 'No results';
                result.industry = '';
                result.phone = '';
                result.website = '';
                result.found = false;
            }
            results.add(result);
        }
        return results;
    }

    public class AccountSearchInput {
        @InvocableVariable(required=true label='Account Name' description='The name or partial name of the account to search for')
        public String accountName;
    }

    public class AccountSearchResult {
        @InvocableVariable(label='Account ID' description='The ID of the found account')
        public String accountId;

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

        @InvocableVariable(label='Industry' description='The industry of the found account')
        public String industry;

        @InvocableVariable(label='Phone' description='The phone number of the found account')
        public String phone;

        @InvocableVariable(label='Website' description='The website of the found account')
        public String website;

        @InvocableVariable(label='Found' description='Whether an account was found')
        public Boolean found;
    }
}

And here is AccountSearchService.cls-meta.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <status>Active</status>
</ApexClass>
Deploy the Apex class to your org

With both files saved, you can push the class to your authorized org so Agentforce can see it.

  • Deploy the classes directory by running this command in your VS Code terminal:
sf project deploy start --source-dir force-app/main/default/classes --target-org agentforce-dev
  • Wait for the deployment to finish. You should see a success status in the terminal output confirming your components deployed.

Seeing a deployment error?

Check that both AccountSearchService.cls and AccountSearchService.cls-meta.xml are saved in the same folder. The CLI expects the meta XML to accompany every Apex class file.

Also verify that apiVersion in the meta XML is set to 64.0 and that there are no typos in the Apex code (mismatched brackets, missing semicolons).

help me debug my Salesforce Apex deployment error

Confirm the action is available in Setup

A successful deploy means your class is live in the org. Confirm it by checking Setup.

  • Switch to your browser and open your Salesforce org.
  • Click the gear icon in the top-right corner and select Setup.
  • In the Quick Find box on the left, type Apex Classes.
  • Click Apex Classes under Custom Code.
  • Verify that AccountSearchService appears in the list.

What should I see?

You should see AccountSearchService listed with a status of Active. The @InvocableMethod label ("Search Accounts") will be visible later when you configure the agent action in Agent Builder.

Your Apex action is deployed and ready. Next up, you will build the Custom Lightning Type Bundles that tell Agentforce to render your own LWC components instead of the default text UI.

Build the Custom Lightning Type Bundles

Your Apex action is deployed and ready to serve data. Now you need to tell Agentforce to render your custom LWC components instead of the default plain-text UI when this action runs.

The bridge between your Apex class and your LWC components is a metadata structure called a Custom Lightning Type. In this step, you will create two Lightning Type Bundles: one for input and one for output.

In this step, get ready to:
  • Create the input Lightning Type Bundle with a schema and editor configuration.
  • Create the output Lightning Type Bundle with a schema and renderer configuration.
  • Deploy both bundles to your org.
Create the input Lightning Type Bundle

A Lightning Type Bundle is a folder containing JSON metadata files that map an Apex class to a custom LWC component. You need one bundle for the action's input (the search form) and one for the output (the results card).

  • Create the folder structure for the input bundle by running this command in your VS Code terminal:
mkdir -p force-app/main/default/lightningTypes/accountSearchInput/lightningDesktopGenAi

What does this folder structure mean?

The lightningTypes folder is where all Custom Lightning Type Bundles live. Inside it, accountSearchInput is the type name that must align with your Apex inner class and LWC target.

The lightningDesktopGenAi subfolder is a channel folder. It targets the Agentforce Employee agent in Lightning Experience.

  • Confirm the folders were created by running:
ls force-app/main/default/lightningTypes/accountSearchInput

You should see lightningDesktopGenAi listed in the output.

  • Create a new file called schema.json inside the force-app/main/default/lightningTypes/accountSearchInput/ folder.
  • Paste the following content into schema.json:
{
    "title": "Account Search Input",
    "description": "Input for searching accounts by name",
    "lightning:type": "@apexClassType/c__AccountSearchService$AccountSearchInput"
}

What does this schema do?

  • title and description are display labels for your type in Salesforce metadata.
  • lightning:type points to your Apex inner class using the syntax @apexClassType/c__ClassName$InnerClassName. The c__ prefix is the default namespace, and $ separates the outer class from the inner class.
  • Save schema.json.

Next, you will create the editor configuration that tells Agentforce which LWC to render for this input type.

  • Create a new file called editor.json inside the force-app/main/default/lightningTypes/accountSearchInput/lightningDesktopGenAi/ folder.
  • Paste the following content into editor.json:
{
    "editor": {
        "componentOverrides": {
            "$": {
                "definition": "c/accountSearchEditor"
            }
        }
    }
}

What does editor.json do?

  • The componentOverrides section tells Agentforce to replace the default text input with your custom component.
  • The $ key means this is a top-level override that replaces the entire default UI for the type.
  • definition points to the LWC you will build in the next step (c/accountSearchEditor).
  • Save editor.json.

File not saving in the right location?

Make sure schema.json is directly inside accountSearchInput/ and editor.json is inside accountSearchInput/lightningDesktopGenAi/. The channel folder nesting is critical.

help me fix my lightningTypes folder structure

Create the output Lightning Type Bundle

The output bundle follows the same pattern, but uses a renderer instead of an editor. This tells Agentforce how to display the search results.

  • Create the folder structure for the output bundle by running this command:
mkdir -p force-app/main/default/lightningTypes/accountSearchResult/lightningDesktopGenAi
  • Confirm the folder was created by running:
ls force-app/main/default/lightningTypes/accountSearchResult

You should see lightningDesktopGenAi listed.

  • Create a new file called schema.json inside the force-app/main/default/lightningTypes/accountSearchResult/ folder.
  • Paste the following content into schema.json:
{
    "title": "Account Search Result",
    "description": "Output for account search results",
    "lightning:type": "@apexClassType/c__AccountSearchService$AccountSearchResult"
}

What does this schema do?

This schema links to the AccountSearchResult inner class inside your Apex service. Agentforce uses this reference to know which data structure to pass to your renderer component.

  • Save schema.json.
  • Create a new file called renderer.json inside the force-app/main/default/lightningTypes/accountSearchResult/lightningDesktopGenAi/ folder.
  • Paste the following content into renderer.json:
{
    "renderer": {
        "componentOverrides": {
            "$": {
                "definition": "c/accountSearchRenderer"
            }
        }
    }
}

What does renderer.json do?

This file works identically to editor.json but for the output side. The definition field points to c/accountSearchRenderer, which is the LWC you will build to display account results as a styled card.

  • Save renderer.json.

Why do naming conventions matter so much here?

Custom Lightning Types silently fall back to the default text UI if any name in the chain is misaligned. The type folder name, the Apex class reference, and the LWC component name all need to match exactly.

If something looks wrong later, check: type folder (accountSearchInput) matches the LWC target config, and the Apex class reference uses $ (not a dot) to separate the inner class.

✔️ Awesome, I've got everything!

Great. Double check you have saved all four JSON files before moving on to deployment.

ⓧ I'd like to double check the full code

Here are all four files. Make sure each one lives in the exact path shown in the filename.

{
    "title": "Account Search Input",
    "description": "Input for searching accounts by name",
    "lightning:type": "@apexClassType/c__AccountSearchService$AccountSearchInput"
}
{
    "editor": {
        "componentOverrides": {
            "$": {
                "definition": "c/accountSearchEditor"
            }
        }
    }
}
{
    "title": "Account Search Result",
    "description": "Output for account search results",
    "lightning:type": "@apexClassType/c__AccountSearchService$AccountSearchResult"
}
{
    "renderer": {
        "componentOverrides": {
            "$": {
                "definition": "c/accountSearchRenderer"
            }
        }
    }
}
Deploy the Lightning Type Bundles
  • Deploy the Lightning Type Bundles to your org by running this command:
sf project deploy start --source-dir force-app/main/default/lightningTypes --target-org agentforce-dev
  • Wait for the deployment to finish. You should see a success message in the terminal output confirming both bundles deployed without errors.

Seeing a deployment error?

  • Check that your sfdx-project.json has sourceApiVersion set to at least 64.0. The LightningTypeBundle metadata type requires API version 64.0 or later.
  • Verify the folder names are exactly accountSearchInput and accountSearchResult with no typos or extra characters.
  • Make sure the channel folder is spelled lightningDesktopGenAi exactly (camelCase with a capital A and lowercase i).

help me debug my Lightning Type Bundle deployment error

Your metadata is in place. Agentforce now knows which LWC components to use for input and output. Next up, you will build the actual LWC editor and renderer components that render the rich UI inside the chat.

Build the LWC Editor and Renderer Components

Your Lightning Type Bundles are deployed and waiting for their visual counterparts. The editor.json and renderer.json files each point to an LWC component that doesn't exist yet.

In this step, you'll build the two Lightning Web Components that bring your agent action to life. The editor component renders a branded search form inside the agent chat. The renderer component displays a styled account card with the results.

In this step, get ready to:
  • Create the editor LWC that renders a search form for input.
  • Create the renderer LWC that displays an account card for output.
  • Deploy all LWC components to your org.
Create the editor LWC for input

The editor component is what the user sees when the agent needs input. Instead of a plain text box, your component renders a styled search form with a heading, an input field, and a button.

  • Create the component folder by running this command in your VS Code terminal:
mkdir -p force-app/main/default/lwc/accountSearchEditor
  • Create a new file called accountSearchEditor.html inside the accountSearchEditor folder and paste the following code:
<template>
    <div class="slds-card slds-p-around_medium">
        <h2 class="slds-text-heading_small slds-m-bottom_small">Search Accounts</h2>
        <lightning-input
            label="Account Name"
            placeholder="Enter account name..."
            value={accountName}
            onchange={handleNameChange}
        ></lightning-input>
        <lightning-button
            label="Search"
            variant="brand"
            class="slds-m-top_small"
            onclick={handleSearch}
        ></lightning-button>
    </div>
</template>

What does this template do?

This HTML template uses SLDS (Salesforce Lightning Design System) classes to style a card container. Inside it, a lightning-input captures the account name and a lightning-button triggers the search.

The curly-brace bindings like {accountName} and {handleNameChange} connect the template to your JavaScript controller, which you'll create next.

  • Save accountSearchEditor.html.
  • Create a new file called accountSearchEditor.js in the same folder and paste the following code:
import { LightningElement, api } from "lwc";

export default class AccountSearchEditor extends LightningElement {
    accountName = "";

    handleNameChange(event) {
        this.accountName = event.target.value;
    }

    handleSearch() {
        this.dispatchEvent(
            new CustomEvent("valuechange", {
                detail: {
                    value: {
                        accountName: this.accountName
                    }
                }
            })
        );
    }
}

What does this JavaScript do?

The handleNameChange method updates the tracked accountName property every time the user types in the input field.

The handleSearch method dispatches a valuechange CustomEvent. This is the event Agentforce listens for to know the user has submitted their input. The payload must match the fields defined in your Apex AccountSearchInput class.

  • Save accountSearchEditor.js.
  • Create a new file called accountSearchEditor.js-meta.xml in the same folder and paste the following code:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Account Search Editor</masterLabel>
    <targets>
        <target>lightning__AgentforceInput</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AgentforceInput">
            <targetType name="c__accountSearchInput"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

What does this metadata file do?

The lightning__AgentforceInput target tells Salesforce this component is designed to render inside the Agentforce chat as an input form.

The targetType value c__accountSearchInput links this component to your Lightning Type Bundle folder name. This name alignment is what wires the full chain together.

  • Save accountSearchEditor.js-meta.xml.
  • Confirm all three files exist by running:
ls force-app/main/default/lwc/accountSearchEditor/

You should see accountSearchEditor.html, accountSearchEditor.js, and accountSearchEditor.js-meta.xml in the output.

Missing a file?

Make sure you created all three files inside the accountSearchEditor folder, not in the parent lwc folder. LWC requires all component files to share the same base name as the folder.

Also check that the meta XML file is named accountSearchEditor.js-meta.xml (with the .js-meta.xml extension, not just .xml).

Help me debug my editor LWC file structure.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on to the renderer component.

ⓧ I'd like to double check the full code

Here are all three editor component files for comparison.

<template>
    <div class="slds-card slds-p-around_medium">
        <h2 class="slds-text-heading_small slds-m-bottom_small">Search Accounts</h2>
        <lightning-input
            label="Account Name"
            placeholder="Enter account name..."
            value={accountName}
            onchange={handleNameChange}
        ></lightning-input>
        <lightning-button
            label="Search"
            variant="brand"
            class="slds-m-top_small"
            onclick={handleSearch}
        ></lightning-button>
    </div>
</template>
import { LightningElement, api } from "lwc";

export default class AccountSearchEditor extends LightningElement {
    accountName = "";

    handleNameChange(event) {
        this.accountName = event.target.value;
    }

    handleSearch() {
        this.dispatchEvent(
            new CustomEvent("valuechange", {
                detail: {
                    value: {
                        accountName: this.accountName
                    }
                }
            })
        );
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Account Search Editor</masterLabel>
    <targets>
        <target>lightning__AgentforceInput</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AgentforceInput">
            <targetType name="c__accountSearchInput"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
Create the renderer LWC for output

The renderer component is what the user sees after the agent returns results. Instead of plain text, it displays a styled account card with icons for each field.

  • Create the component folder by running this command:
mkdir -p force-app/main/default/lwc/accountSearchRenderer
  • Create a new file called accountSearchRenderer.html inside the accountSearchRenderer folder and paste the following code:
<template>
    <div class="slds-card slds-p-around_medium">
        <template if:true={isFound}>
            <div class="slds-media">
                <div class="slds-media__figure">
                    <lightning-icon icon-name="standard:account" size="large"></lightning-icon>
                </div>
                <div class="slds-media__body">
                    <h2 class="slds-text-heading_medium">{displayName}</h2>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:company" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayIndustry}
                    </p>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:call" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayPhone}
                    </p>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:world" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayWebsite}
                    </p>
                </div>
            </div>
        </template>
        <template if:false={isFound}>
            <div class="slds-illustration slds-illustration_small">
                <p class="slds-text-body_regular">No accounts found matching your search.</p>
            </div>
        </template>
    </div>
</template>

What does this template do?

The template uses conditional rendering with if:true={isFound} and if:false={isFound}. When the Apex action finds a matching account, the card layout appears with icons for industry, phone, and website.

When no match is found, the component displays a simple "No accounts found" message instead. The slds-media layout creates the icon-beside-text pattern common in Salesforce UIs.

  • Save accountSearchRenderer.html.
  • Create a new file called accountSearchRenderer.js in the same folder and paste the following code:
import { LightningElement, api } from "lwc";

export default class AccountSearchRenderer extends LightningElement {
    @api value;

    get isFound() {
        return this.value && this.value.found;
    }

    get displayName() {
        return this.value ? this.value.accountName : "";
    }

    get displayIndustry() {
        return this.value ? this.value.industry : "";
    }

    get displayPhone() {
        return this.value ? this.value.phone : "";
    }

    get displayWebsite() {
        return this.value ? this.value.website : "";
    }
}

What does this JavaScript do?

The @api value property is how Agentforce passes the full result object from your Apex action into this component. The framework automatically populates it with the AccountSearchResult fields.

The getter methods (isFound, displayName, etc.) safely access nested properties with null checks. This prevents errors if the value hasn't been set yet when the component first renders.

  • Save accountSearchRenderer.js.
  • Create a new file called accountSearchRenderer.js-meta.xml in the same folder and paste the following code:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Account Search Renderer</masterLabel>
    <targets>
        <target>lightning__AgentforceOutput</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AgentforceOutput">
            <sourceType name="c__accountSearchResult"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

What does this metadata file do?

The lightning__AgentforceOutput target tells Salesforce this component renders agent output (results) rather than capturing input.

The sourceType value c__accountSearchResult links this renderer to your output Lightning Type Bundle. Notice the editor uses targetType while the renderer uses sourceType. The naming must match your type folder names exactly.

  • Save accountSearchRenderer.js-meta.xml.
  • Confirm all three files exist by running:
ls force-app/main/default/lwc/accountSearchRenderer/

You should see accountSearchRenderer.html, accountSearchRenderer.js, and accountSearchRenderer.js-meta.xml in the output.

Missing files or wrong folder?

Double-check that the folder is named accountSearchRenderer (camelCase, no hyphens). LWC folder names must exactly match the component class name.

If the files are there but you later get deployment errors, verify the filenames all start with accountSearchRenderer (matching the folder name).

Help me debug my renderer LWC file structure.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before deploying.

ⓧ I'd like to double check the full code

Here are all three renderer component files for comparison.

<template>
    <div class="slds-card slds-p-around_medium">
        <template if:true={isFound}>
            <div class="slds-media">
                <div class="slds-media__figure">
                    <lightning-icon icon-name="standard:account" size="large"></lightning-icon>
                </div>
                <div class="slds-media__body">
                    <h2 class="slds-text-heading_medium">{displayName}</h2>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:company" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayIndustry}
                    </p>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:call" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayPhone}
                    </p>
                    <p class="slds-text-body_regular slds-m-top_xx-small">
                        <lightning-icon icon-name="utility:world" size="xx-small" class="slds-m-right_xx-small"></lightning-icon>
                        {displayWebsite}
                    </p>
                </div>
            </div>
        </template>
        <template if:false={isFound}>
            <div class="slds-illustration slds-illustration_small">
                <p class="slds-text-body_regular">No accounts found matching your search.</p>
            </div>
        </template>
    </div>
</template>
import { LightningElement, api } from "lwc";

export default class AccountSearchRenderer extends LightningElement {
    @api value;

    get isFound() {
        return this.value && this.value.found;
    }

    get displayName() {
        return this.value ? this.value.accountName : "";
    }

    get displayIndustry() {
        return this.value ? this.value.industry : "";
    }

    get displayPhone() {
        return this.value ? this.value.phone : "";
    }

    get displayWebsite() {
        return this.value ? this.value.website : "";
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Account Search Renderer</masterLabel>
    <targets>
        <target>lightning__AgentforceOutput</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AgentforceOutput">
            <sourceType name="c__accountSearchResult"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
Deploy all LWC components
  • Deploy both components to your org by running this command:
sf project deploy start --source-dir force-app/main/default/lwc --target-org agentforce-dev

What does this command do?

This deploys everything in the lwc folder to your org. Both accountSearchEditor and accountSearchRenderer are included in a single deploy operation.

  • Verify the deployment succeeds. You should see a success message in your terminal output with both components listed.

Seeing a deployment error?

  • If you see an invalid file name error, make sure every file inside each LWC folder starts with the same name as the folder (e.g. all files in accountSearchEditor/ start with accountSearchEditor).
  • If you see an API version error, confirm the apiVersion in both meta XML files is set to 64.0.
  • If you see a target-related error, check that the target values are exactly lightning__AgentforceInput and lightning__AgentforceOutput (double underscore, no spaces).

Help me debug my LWC deployment error.

Both components are live in your org. Next up, you'll wire everything together in Agent Builder and test the full flow with your custom UI rendering inside the chat.

Wire Everything Together in Agent Builder

Your Apex class, Lightning Type Bundles, and LWC components are all deployed and ready. Each piece handles one part of the puzzle, but they won't work together until you connect them inside Agent Builder.

In this step, you will create a Topic, wire your custom action to it, and configure the agent to use your custom Lightning Types for input and output rendering. Then you will test the full flow and see your branded LWC components appear directly in the agent chat.

In this step, get ready to:
  • Create an agent topic in Agent Builder.
  • Add a custom action with Lightning Type rendering for input and output.
  • Test the agent and verify your custom LWC UI appears in the conversation.
Create a new agent and topic

An Agentforce agent needs at least one Topic to know what it can help with. Topics group related actions together and give the agent context about when to use them.

  • Switch back to your Salesforce org in the browser.
  • Open Setup by clicking the gear icon in the top-right corner.
  • In the Quick Find box on the left, type Agentforce Agents.
  • Select Agentforce Agents from the results.
  • Click New Agent to create a fresh agent, or open the existing default agent if one is already listed.
  • Give your agent a name like Account Lookup Agent and save it.

You should now be inside Agent Builder with your agent open.

  • In the agent configuration, find the Topics section.
  • Click New Topic.
  • Set the Topic Label to Account Lookup.
  • Set the Description to Helps users find and view account information.
  • Save the topic.

Why does the description matter?

The topic description tells the agent's underlying LLM when to activate this topic. A clear, specific description helps the agent correctly route user requests to your custom action instead of falling back to generic responses.

Don't see Agentforce Agents in Setup?

Make sure Einstein and Agentforce are both enabled. In Setup, search for Einstein Setup and toggle it on, then search for Agentforce Agents and enable it there too. You may need to refresh the page after toggling.

Help me enable Agentforce in my Developer Edition org.

Add your custom action to the topic

Now you will connect your Apex @InvocableMethod to the topic and tell the agent to use your custom Lightning Types for rendering.

  • Inside the Account Lookup topic, click New Action.
  • Search for and select the Search Accounts action. This is the label from your @InvocableMethod annotation in AccountSearchService.
  • In the action's input configuration, find the Input Rendering setting.
  • Set the Input Rendering to your custom Lightning Type accountSearchInput.
  • In the action's output configuration, find the Output Rendering setting.
  • Set the Output Rendering to your custom Lightning Type accountSearchResult.
  • Save the action configuration.

What did you just configure?

By setting the Input Rendering, you told the agent to display your accountSearchEditor LWC (a styled search form) instead of plain text when collecting input from the user.

By setting the Output Rendering, you told the agent to display your accountSearchRenderer LWC (a styled account card) instead of plain text when showing results.

Test the agent in Agent Builder

This is the moment of truth. You will send a message to your agent and confirm that your custom LWC components render in the conversation instead of the default text UI.

  • In Agent Builder, open the test panel on the right side of the screen.
  • Type the following message into the chat:
  • Find an account called Edge Communications.
  • Press Enter to send the message.

You should see your custom editor LWC appear as a styled search form with an input field and a Search button. After the action runs, a styled account card should appear showing the account name, industry, phone, and website with SLDS icons.

What is Edge Communications?

Edge Communications is a sample Account record that comes pre-loaded in every Salesforce Developer Edition org. It is a reliable test case because you know it exists without having to create any data first.

Seeing plain text instead of your custom UI?

This almost always means a naming mismatch somewhere in the chain. Custom Lightning Types silently fall back to the default text UI when names do not align.

Check this alignment table:

  • The Lightning Type folder name (accountSearchInput) must match the targetType in your editor LWC's js-meta.xml (c__accountSearchInput).
  • The Lightning Type folder name (accountSearchResult) must match the sourceType in your renderer LWC's js-meta.xml (c__accountSearchResult).
  • The definition in editor.json and renderer.json must match the LWC folder names exactly (c/accountSearchEditor and c/accountSearchRenderer).
  • The lightning:type in each schema.json must reference the exact Apex inner class name with the $ separator (c__AccountSearchService$AccountSearchInput).

My agent shows plain text instead of my custom LWC. Help me debug the naming alignment.

Secret mission

Add an Account Update Action with Edit Form

Your search action is read-only. Real agents need to take action. In this mission, you will create a second Agentforce action that lets the agent update Account fields through a custom edit form rendered directly inside the chat conversation.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on a free Salesforce Developer Edition org, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder agentforce-custom-ui on your Desktop.
  • Deployed Apex classes in your org: AccountSearchService and AccountUpdateService.
  • Deployed Lightning Type Bundles: accountSearchInput, accountSearchResult, accountUpdateInput, accountUpdateResult.
  • Deployed LWC components: accountSearchEditor, accountSearchRenderer, accountUpdateEditor, accountUpdateRenderer.
  • Agentforce agent configuration with the Account Lookup topic and its two actions.

Keep everything running

No action needed. Choose this if you are still actively building on this agent or plan to extend it further.

  • Your Salesforce Developer Edition org is free and does not expire as long as you log in periodically.
  • Your local project folder stays on your Desktop for future edits.
  • All deployed metadata remains active in the org, ready for further iteration.

Pause - I'll come back to this later

Deactivate the agent to keep everything in place without it responding to user queries.

  • In Setup, search for Agentforce Agents in Quick Find.
  • Open your agent and deactivate it so it no longer responds in the org.
  • Your Apex classes, LWC components, and Lightning Type Bundles remain deployed but inactive without the agent.
  • Your local agentforce-custom-ui folder stays on your Desktop for when you return.

Delete - I don't want to use this again

Remove all project resources from your org and your machine to start fresh.

  • First, remove the agent configuration. In Setup, search for Agentforce Agents in Quick Find, open your agent, and delete the Account Lookup topic and its two actions.
  • Next, delete the deployed metadata from your org. In VS Code, right-click the classes folder inside force-app/main/default/ and select SFDX: Delete from Project and Org.
  • Right-click the lightningTypes folder and select SFDX: Delete from Project and Org.
  • Right-click the lwc folder and select SFDX: Delete from Project and Org.
  • Finally, delete your local project folder. On macOS or Linux, run:
rm -rf ~/Desktop/agentforce-custom-ui
  • On Windows, run:
rmdir /s /q "$HOME\Desktop\agentforce-custom-ui"

What about my Salesforce Developer Edition org?

Your Developer Edition org is free and has no expiration as long as you log in periodically. There is no need to delete the org itself.

Nice Work!

Nice Work!

Nice work! You just built a fully functional Agentforce agent that replaces the default plain-text interaction with custom branded LWC components for both input and output.

You've learned how to:

  • Build a custom Agentforce agent action backed by Apex @InvocableMethod that queries Account data and returns structured results.
  • Create Custom Lightning Type Bundles with schema.json, editor.json, and renderer.json to override the default agent UI with custom LWC components.
  • Wire the full chain together in Agent Builder: Apex classes, Lightning Types targeting lightning__AgentforceInput and lightning__AgentforceOutput, and topic-action configuration for rich UI rendering inside the conversation.
  • Secret Mission: Extended the agent with a DML update action using a custom edit form and confirmation card.

Ready to quiz yourself?