Introduction
The Summer '25 release of Salesforce (or maybe prior, given Agentforce
releases are on a monthly cycle) introduced
Custom Lightning Types, which finally gives us a degree of fine-grained control over the user
interface. At the time of writing (August 2025) this is only for actions that
use Apex classes for requests and responses.
In essence, a Custom Lightning Type configures an Agentforce user interface
override for an Apex class. The Apex class can either be used as the input to
send information to an Agentforce action, or as the output containing the
response to the user request.
Example
I've been playing around with a Custom Lighting Type to display overview
information from my
Limit Tracker
that I created for my session at
London's Calling 2025. For this I wanted two custom types:
-
An input that captured the number of days of Limit Snapshots to include in
the overview
- An output that showed the Heap and CPU details from the snapshots
I also wanted these to be more interesting than a simple form field to capture
an integer and a list of values, but that only comes into play when developing
the Lightning Web Components to handle the user interface, so I'll come to
those later.
Output
public class LimitConsumption {
@InvocableVariable
public String name;
@InvocableVariable
public List<LimitSnapshot> snapshots;
public LimitConsumption(String name, List<LimitSnapshot> snapshots) {
this.name = name;
this.snapshots = snapshots;
}
}
It can sometimes be tricky to pinpoint the exact class returned from an
action, as they tend to be inner classes and nested inside response wrappers.
An easy way to find out exactly which class Agentforce will use is to go
through the New Action process and see what it picks up for the output:
If you aren't from a developer background, the class reference might look a
bit odd, but it breaks down as:
-
c__ this is the namespace the
class is present in. c__ means
the default (or lack of) namespace.
-
LimitConsumptionOverview is the
outer, or containing class
-
The $ separator indicates this
is an inner class of LimitConsumptionOverview
-
LimitConsumption is the name of
the actual inner class
Once I know which class my type with will be working with, I can create an
entry in the
lightningTypes source folder, in this case
limitConsumptionResponse, to make it clear this is used to render the response from an Agent.
The
schema.json
file configures the type to work with my custom class:
{
"title": "Limit Consumption",
"description": "Overview of limit consumption by a particular piece of Apex code",
"lightning:type": "@apexClassType/c__LimitConsumptionOverview$LimitConsumption"
}
The lightning:type entry is the key
piece of wiring that couples this Custom Lightning Type with my Apex class.
Next I had to decide how I wanted to render this information. Something that
couldn't easily be found on the regular detail pages was appealing, and as it
is handled by a Lightning Web Component I have access to the full power of
JavaScript and the Lightning Design System. I decided to return a tabbed
interface containing a chart :
and a list of values:
Note that I've taken a leaf out of the Salesforce examples and my custom Apex action returns the same list of fake data regardless of how it's invoked.
You can find the Lightning Web Component at :
limitConsumption - it uses the Chart.js library
to create the bar chart.
Once my Lightning Web Component is in place, I need to create a file in my
Custom Lightning Type folder named
lightningDesktopGenAi
named
renderer.json:
{
"collection": {
"renderer": {
"componentOverrides": {
"$": {
"definition": "c/limitConsumption"
}
}
}
}
}
The key aspect here is the
componentOverrides which defines my
Lightning Web Component (limitConsumption
in the default namespace) as the override to render the type.
Input
My custom Apex action relies on the following input class:
public with sharing class SliderIntegerWrapper {
@InvocableVariable
public Integer value;
}
Note that this is simply wrapping an Integer value - while lots of the
examples I've seen are around complex classes containing multiple
values/nested classes, they don't have to. In this case I just want a funkier
way to enter the value, so I need an Apex class to hold it.
To capture the information I decided to go with a slider component - note that
this did give Agentforce a little trouble with the rendering - once the value
got to double figures it wrapped to the next line!
Similar to the output, I create a lightningType folder named
sliderInteger
and define the schema.json to couple the type to my class:
{
"title": "Slider Integer",
"description": "Slider Integer",
"lightning:type": "@apexClassType/c__SliderIntegerWrapper"
}
as this time my class is top level, the entry just contains
c__ (for the default namespace) and
the name of the class.
The Lightning Web Component that provides the input slider can be found at
sliderInteger.
This time, once my Lightning Web Component is in place, I need to define a
lightningDesktopGenAi/editor.json
file to define it as the override:
{
"editor": {
"componentOverrides": {
"$": {
"definition": "c/sliderInteger"
}
}
}
}
The Agent Action
Now that I have my Custom Lightning Types defined and the overrides
configured, I can create the Agentforce action that makes use of them, based
on my
LimitConsumptionOverview
invocable Apex class. When defining the inputs I choose my
SliderInteger type for the
daysWrapper :
and for outputting the limit consumption information, I choose the
LimitConsumptionResponse type:
Executing this action brings both my types into play, as can be seen from the
following short video:
Lessons Learned
Something I'd strongly recommend when working with Custom Lightning Types is
to build yourself a simple test page so that you can try them out without
having to go via Agentforce every time you make a change. While it doesn't
sound onerous, making requests and supplying inputs gets tedious very
quickly! I created a test harness Lightning Web Component (limitConsumptionHarness) that sends fake data to the output component and handles the event from
the input component, and it's saved me a lot of time.
The first scratch org I tried this in, I'd already used the example from the
Salesforce docs, and for some reason only that one would be picked up. After
spending a couple of hours trying various things, and creating yet another
set of custom types, I spun up a new scratch org and it all worked fine. No
idea what went wrong, but by the same token I was learning and tweaking so
it could have been anything.
Make sure to use distinct names for your types, classes and Lightning Web
Components. My first attempt I used the same name for the type and the
component and quickly lost track of what I was configuring where.
More Information