This is the seventh blog in the series “Design Your First Salesforce AI Agent with Agentforce : From Sales Persona to Automation“.
In this appendix, we’ve documented the key custom actions used in this build — including what they do, how they’re implemented, and how they return information to the agent.
⚙️ Custom Actions Implemented in This Build
Query Events
Description:
Processes natural language queries from the user to retrieve relevant Event records associated with Accounts, Opportunities, or Contacts. This Apex service parses the query to detect date filters (e.g., “upcoming,” “past,” “this week”) and referenced entities (e.g., “Acme Corp”, “Project Alpha”), then dynamically builds and executes a SOQL query to fetch the matching records. It returns a rich HTML list of events formatted for display in UI or agent responses.
Type:
Apex (Invocable Method with natural language input) — accepts:
userQuery: a free-text query like “Show me upcoming meetings with Acme Corp” or “List past events for Project Alpha”
Returns:
- A styled HTML summary of up to 100 matching events, including:
- Event subject (linked)
- Date/time
- Location
- Related Account or Opportunity
- Related Contact or Lead
- Truncated description
Key Behaviors:
- Parses time context:
upcoming,past,today,this week,this month - Extracts entity references: Account name, Opportunity name, Contact name
- Constructs dynamic SOQL queries using LIKE filters and relationship fields
- Formats the response in a UI-friendly, scannable HTML card list
public class DynamicEventQueryService {
public class Request {
@InvocableVariable(required=true)
public String userQuery;
}
@InvocableMethod(label='Query Events with Natural Language' description='Process natural language queries to find relevant events')
public static List<String> queryEvents(List<Request> requests) {
List<String> allResults = new List<String>();
for (Request req : requests) {
try {
// Parse the natural language query
QueryParameters params = parseUserQuery(req.userQuery);
// Build dynamic SOQL query
String soqlQuery = buildEventQuery(params);
// Execute query
List<Event> events = Database.query(soqlQuery);
System.debug('Found ' + events.size() + ' events for query: ' + req.userQuery);
System.debug('Generated SOQL: ' + soqlQuery);
// Generate HTML response
String htmlResponse = generateHTMLResponse(events, req.userQuery);
allResults.add(htmlResponse);
} catch (Exception e) {
System.debug('Error processing event query: ' + e.getMessage() + '\n' + e.getStackTraceString());
allResults.add('<p style="color: red;">Error: Unable to process query - ' + e.getMessage() + '</p>');
}
}
return allResults;
}
// Generate HTML response with clickable links
private static String generateHTMLResponse(List<Event> events, String originalQuery) {
if (events.isEmpty()) {
return '<p>No events found matching your query.</p>';
}
String html = '<ul style="list-style-type: none; padding-left: 0;">';
for (Event evt : events) {
html += '<li style="margin-bottom: 15px; padding: 10px; border-left: 3px solid #0176d3; background-color: #f8f9fa;">';
// Event subject with link
html += '<strong><a href="/' + evt.Id + '" target="_blank" style="color: #0176d3; text-decoration: none;">';
html += (String.isNotBlank(evt.Subject) ? evt.Subject : 'Untitled Event') + '</a></strong><br/>';
// Date and time
if (evt.StartDateTime != null) {
String formattedDateTime = evt.StartDateTime.format('EEEE, MMMM dd, yyyy \'at\' h:mm a');
html += '<span style="color: #706e6b;">đź“… ' + formattedDateTime + '</span><br/>';
}
// Location
if (String.isNotBlank(evt.Location)) {
html += '<span style="color: #706e6b;">📍 ' + evt.Location + '</span><br/>';
}
// Related Account/Opportunity (What)
if (evt.WhatId != null && String.isNotBlank(evt.What?.Name)) {
String whatType = String.valueOf(evt.WhatId).startsWith('006') ? 'Opportunity' : 'Account';
html += '<span style="color: #706e6b;">🏢 ' + whatType + ': ';
html += '<a href="/' + evt.WhatId + '" target="_blank" style="color: #0176d3;">' + evt.What.Name + '</a></span><br/>';
}
// Related Contact/Lead (Who)
if (evt.WhoId != null && String.isNotBlank(evt.Who?.Name)) {
String whoType = String.valueOf(evt.WhoId).startsWith('003') ? 'Contact' : 'Lead';
html += '<span style="color: #706e6b;">👤 ' + whoType + ': ';
html += '<a href="/' + evt.WhoId + '" target="_blank" style="color: #0176d3;">' + evt.Who.Name + '</a></span><br/>';
}
// Description (truncated)
if (String.isNotBlank(evt.Description)) {
String shortDesc = evt.Description.length() > 100 ?
evt.Description.substring(0, 100) + '...' : evt.Description;
html += '<span style="color: #706e6b; font-style: italic;">' + shortDesc + '</span>';
}
html += '</li>';
}
html += '</ul>';
return html;
}
// Parse natural language query to extract parameters
private static QueryParameters parseUserQuery(String userQuery) {
QueryParameters params = new QueryParameters();
String queryLower = userQuery.toLowerCase();
// Determine time filter
if (queryLower.contains('upcoming') || queryLower.contains('future')) {
params.timeFilter = 'upcoming';
} else if (queryLower.contains('past') || queryLower.contains('previous') || queryLower.contains('completed')) {
params.timeFilter = 'past';
} else if (queryLower.contains('today')) {
params.timeFilter = 'today';
} else if (queryLower.contains('this week')) {
params.timeFilter = 'thisWeek';
} else if (queryLower.contains('this month')) {
params.timeFilter = 'thisMonth';
} else {
params.timeFilter = 'upcoming'; // Default to upcoming
}
// Extract account name if mentioned
params.accountName = extractAccountName(userQuery);
// Extract opportunity name if mentioned
params.opportunityName = extractOpportunityName(userQuery);
// Extract contact name if mentioned
params.contactName = extractContactName(userQuery);
System.debug('Parsed query parameters: ' + JSON.serialize(params));
return params;
}
// Extract account name from query
private static String extractAccountName(String userQuery) {
String queryLower = userQuery.toLowerCase();
// Look for patterns like "for [account name]", "with [account name]", "at [account name]"
List<String> accountPatterns = new List<String>{
'for account ',
'with account ',
'at account ',
'for ',
'with ',
'at '
};
for (String pattern : accountPatterns) {
Integer startIndex = queryLower.indexOf(pattern);
if (startIndex != -1) {
startIndex += pattern.length();
// Find the end of the account name (next preposition or end of string)
List<String> stopWords = new List<String>{' and ', ' or ', ' that ', ' which ', ' where ', ' when '};
Integer endIndex = userQuery.length();
for (String stopWord : stopWords) {
Integer stopIndex = queryLower.indexOf(stopWord, startIndex);
if (stopIndex != -1 && stopIndex < endIndex) {
endIndex = stopIndex;
}
}
if (startIndex < endIndex) {
String accountName = userQuery.substring(startIndex, endIndex).trim();
if (String.isNotBlank(accountName) && accountName.length() > 2) {
return accountName;
}
}
}
}
return null;
}
// Extract opportunity name from query
private static String extractOpportunityName(String userQuery) {
String queryLower = userQuery.toLowerCase();
// Look for patterns that suggest opportunity names
List<String> opportunityPatterns = new List<String>{
'opportunity ',
'opportunities for ',
'deal ',
'project '
};
for (String pattern : opportunityPatterns) {
Integer startIndex = queryLower.indexOf(pattern);
if (startIndex != -1) {
startIndex += pattern.length();
// Find the end of the opportunity reference
List<String> stopWords = new List<String>{' and ', ' or ', ' that ', ' which ', ' where ', ' when ', ' events'};
Integer endIndex = userQuery.length();
for (String stopWord : stopWords) {
Integer stopIndex = queryLower.indexOf(stopWord, startIndex);
if (stopIndex != -1 && stopIndex < endIndex) {
endIndex = stopIndex;
}
}
if (startIndex < endIndex) {
String opportunityName = userQuery.substring(startIndex, endIndex).trim();
if (String.isNotBlank(opportunityName) && opportunityName.length() > 2) {
return opportunityName;
}
}
}
}
return null;
}
// Extract contact name from query
private static String extractContactName(String userQuery) {
String queryLower = userQuery.toLowerCase();
// Look for patterns that suggest contact names
List<String> contactPatterns = new List<String>{
'with ',
'contact ',
'person ',
'attendee '
};
for (String pattern : contactPatterns) {
Integer startIndex = queryLower.indexOf(pattern);
if (startIndex != -1) {
startIndex += pattern.length();
// Find the end of the contact name
List<String> stopWords = new List<String>{' and ', ' or ', ' that ', ' which ', ' where ', ' when ', ' events'};
Integer endIndex = userQuery.length();
for (String stopWord : stopWords) {
Integer stopIndex = queryLower.indexOf(stopWord, startIndex);
if (stopIndex != -1 && stopIndex < endIndex) {
endIndex = stopIndex;
}
}
if (startIndex < endIndex) {
String contactName = userQuery.substring(startIndex, endIndex).trim();
// Look for typical name patterns (First Last or First Middle Last)
if (String.isNotBlank(contactName) && contactName.contains(' ') && contactName.length() > 3) {
return contactName;
}
}
}
}
// Also check for direct name patterns (Firstname Lastname)
List<String> words = userQuery.split(' ');
for (Integer i = 0; i < words.size() - 1; i++) {
String word1 = words[i].trim();
String word2 = words[i + 1].trim();
// Check if both words start with capital letter (likely names)
if (word1.length() > 1 && word2.length() > 1 &&
isCapitalized(word1) && isCapitalized(word2) &&
!isCommonWord(word1.toLowerCase()) && !isCommonWord(word2.toLowerCase())) {
return word1 + ' ' + word2;
}
}
return null;
}
// Helper method to check if a word is capitalized
private static Boolean isCapitalized(String word) {
if (String.isBlank(word) || word.length() == 0) {
return false;
}
String firstChar = word.substring(0, 1);
return firstChar == firstChar.toUpperCase() && firstChar != firstChar.toLowerCase();
}
// Helper method to identify common non-name words
private static Boolean isCommonWord(String word) {
Set<String> commonWords = new Set<String>{
'events', 'event', 'meeting', 'meetings', 'call', 'calls', 'with', 'for', 'and', 'or', 'the', 'a', 'an',
'upcoming', 'past', 'future', 'today', 'tomorrow', 'yesterday', 'this', 'next', 'last', 'week', 'month',
'account', 'opportunity', 'contact', 'show', 'find', 'get', 'list', 'display', 'where', 'when', 'what'
};
return commonWords.contains(word);
}
// Build dynamic SOQL query based on parsed parameters
private static String buildEventQuery(QueryParameters params) {
List<String> selectFields = new List<String>{
'Id', 'Subject', 'StartDateTime', 'EndDateTime', 'Location',
'Description', 'WhatId', 'WhoId', 'Type', 'IsAllDayEvent',
'What.Name', 'Who.Name'
};
String query = 'SELECT ' + String.join(selectFields, ', ') + ' FROM Event';
List<String> whereConditions = new List<String>();
// Add time-based filters
switch on params.timeFilter {
when 'upcoming' {
whereConditions.add('StartDateTime >= TODAY');
}
when 'past' {
whereConditions.add('StartDateTime < TODAY');
}
when 'today' {
whereConditions.add('StartDateTime = TODAY');
}
when 'thisWeek' {
whereConditions.add('StartDateTime = THIS_WEEK');
}
when 'thisMonth' {
whereConditions.add('StartDateTime = THIS_MONTH');
}
}
// Add account-based filtering
if (String.isNotBlank(params.accountName)) {
String accountSubquery = '(SELECT Id FROM Account WHERE Name LIKE \'%' +
String.escapeSingleQuotes(params.accountName) + '%\')';
whereConditions.add('WhatId IN ' + accountSubquery);
}
// Add opportunity-based filtering
if (String.isNotBlank(params.opportunityName)) {
String opportunitySubquery = '(SELECT Id FROM Opportunity WHERE Name LIKE \'%' +
String.escapeSingleQuotes(params.opportunityName) + '%\')';
whereConditions.add('WhatId IN ' + opportunitySubquery);
}
// Add contact-based filtering
if (String.isNotBlank(params.contactName)) {
String contactSubquery = '(SELECT Id FROM Contact WHERE Name LIKE \'%' +
String.escapeSingleQuotes(params.contactName) + '%\')';
whereConditions.add('WhoId IN ' + contactSubquery);
}
// Build WHERE clause
if (!whereConditions.isEmpty()) {
query += ' WHERE ' + String.join(whereConditions, ' AND ');
}
// Add ORDER BY
if (params.timeFilter == 'past') {
query += ' ORDER BY StartDateTime DESC';
} else {
query += ' ORDER BY StartDateTime ASC';
}
// Add LIMIT
query += ' LIMIT 100';
System.debug('Generated SOQL: ' + query);
return query;
}
// Helper class to store parsed query parameters
private class QueryParameters {
public String timeFilter = 'upcoming';
public String accountName;
public String opportunityName;
public String contactName;
}
}Meeting Prep
Description:
Generates a rich, structured meeting preparation brief using data from Salesforce, including Account information, Opportunity pipeline, and recent interactions. This capability equips reps with timely, contextual insights to help them drive strategic conversations, anticipate objections, and align with the customer’s priorities — all before they walk into the meeting.
Type:
Prompt-based, orchestrated via a Salesforce Flow that:
- Gathers and structures grounding data from Account, Opportunity, and Event records
- Passes the merged data into a carefully structured prompt for high-quality output
- Stores the result for UI display or downstream email use
Prompt Behavior Guidelines:
- Input:
{$Flow:Meeting_Prep_Grounding.Prompt}(includes structured data: account info, opps, event history) - Output Requirements:
- 300–500 word HTML-formatted prep brief
- Clearly defined sections:
- Account Overview
- Recent Opportunity Status
- Key Recent Interactions
- Key Stakeholders
- Discussion Points
- Action Items & Preparation
- Formatting Instructions:
- Use HTML only (no Markdown or code blocks)
- Include headings, bold text, and emojis for visual clarity
- Keep the structure scannable and UI-friendly
- Do not echo prompt content or variable names
Prompt –
You are an expert enterprise sales strategist helping a sales representative prepare for an upcoming customer meeting. Using the provided account data, opportunity history, and past interactions, generate a rich HTML-formatted meeting preparation brief. Your goal is to make the sales rep confident, informed, and able to drive strategic conversation aligned with the client’s priorities.
You will receive the following input:
Grounding variable: {!$Flow:Meeting_Prep_Grounding.Prompt}
This includes:
Account Information: Name, Industry, Annual Revenue
Opportunity Details: Multiple open opportunities with Name, Amount, Stage, and Close Date
Event History: Recent meetings or calls with dates and summarized notes
Instructions:
Analyze the data and generate a 300–500 word structured meeting preparation brief with clear sections. Focus on surfacing high-impact insights and actionable next steps.
Output Sections (required):
Account Overview
1–2 sentence summary of the account
Industry, size, and strategic value of the account
Recent Opportunity Status
List top 3–5 open opportunities
Include stage, close date, and short recommendation (bundle, prioritize, drop, etc.)
Key Recent Interactions
Summarize the last 1–2 meaningful events
Highlight themes, action items, or stated goals
Key Stakeholders
Identify named decision-makers or influencers
Note their roles, interests, and priorities
Discussion Points
Recommend 3–5 points to raise in the meeting
Include strategic follow-ups and suggested next steps
Action Items & Preparation
List 3–5 things the rep must do before the meeting
Include materials to prepare, stakeholders to reach out to, or internal reviews
Output Format:
Return everything as styled HTML (do not use Markdown or code blocks)
Use rich formatting: headings, bold text, and emojis in bullets for visual clarity
Keep it scannable and organized for UI display
Do not include headings like “Input Data” or “Prompt” in your response
Flow-

Send meeting Prep Notes
Description:
Formats and sends the meeting preparation summary to the appropriate contact or team. This is handled through a Salesforce Flow that retrieves the most recent prep notes, invokes a prompt to clean up embedded HTML, and then sends the polished, readable summary via email. The result is a clean, human-friendly version of the meeting prep content that can be shared externally or internally with minimal editing.
Type:
Flow-driven – orchestrates:
- Retrieval of the prep summary
- Prompt call for HTML-to-rich-text cleanup
- Email delivery via Flow using pre-configured email templates or actions
Prompt Behavior Guidelines:
- Removes all HTML tags, scripts, styles, and metadata
Prompt –
You are a formatting assistant responsible for converting HTML content into rich text format for use in Salesforce email bodies.
You will receive full HTML content that may include tags such as <!DOCTYPE html>, <html>, <head>, <style>, and <body>. Your task is to convert this HTML into clean, readable rich text that maintains formatting without HTML tags.
Ensure the following:
Remove ALL HTML tags completely (including <!DOCTYPE html>, <html>, <head>, <style>, <body>, <h1>, <h2>, <p>, <ul>, <li>, <strong>, <span>, etc.)
Convert HTML headings to properly formatted text with clear hierarchy using line breaks and spacing
Convert bullet points and lists to plain text with bullet symbols (•) or dashes (-)
Convert bold/strong text to ALL CAPS or bold markdown formatting
Preserve line breaks and paragraph spacing for readability
Retain any emojis as-is
Remove any inline styles, scripts, meta tags, or title tags
Final output must be clean, readable rich text that displays well in any email client
Use the information below to generate the cleaned meeting prep content: {!$Input:Meeting_Prep_Notes}Flow –

Post Meeting Notes Processing
Description:
Takes a rep’s unstructured input — such as raw notes, short recaps, or bullet points — and transforms it into a well-written meeting summary along with a list of actionable follow-up tasks. This prompt is triggered by user intent (e.g., “summarize my notes”) and is designed to streamline post-meeting documentation, ensuring reps capture critical details and next steps with minimal effort.
Type:
Prompt (invoked via user intent) — accepts:
User_Input: Free-form meeting notes provided by the rep
Returns:
- A professional meeting summary (3–6 sentences)
- A list of follow-up tasks (bullet points), only if relevant
Prompt Behavior Guidelines:
- The summary captures key discussion points, decisions, and action items.
- Tasks are phrased clearly and only included if explicitly mentioned or strongly implied.
Prompt –
You are a sales assistant helping a rep summarize a customer meeting. Based on the rep’s input {!$Input:User_Input} — which may include raw notes, bullet points, or short summaries — return two things:
A detailed, professional summary of the meeting written in paragraph form (about 3–6 sentences).
A list of follow-up tasks written as simple bullet points, one per task.
oGuidelines:
The meeting summary should clearly capture key discussion points, decisions made, and any next steps.
Follow-up tasks should be phrased as clear, actionable items the rep needs to complete (e.g., Send onboarding plan to customer).
Only include tasks that are explicitly mentioned or strongly implied by the input.
If no tasks are needed, do not include the list.Find Past Events
Description:
Retrieves and formats a list of recent past Events associated with a given Account or Opportunity. This Apex class takes a single input — the record ID — and returns a clean, styled HTML snippet that can be used in summaries, meeting prep, or embedded UI. It dynamically detects the entity type (Account vs. Opportunity) based on the record prefix and returns up to 5 past events, ordered by most recent first.
Type:
Apex (Invocable Method) — accepts:
recordId(required): ID of an Account or Opportunity
Returns:
- A single HTML string containing a styled list of past Events, including:
- Event subject (with link)
- Date and time
- Location (if present)
- Related record name
- Truncated description (first 100 characters)
Apex Class –
public class EventListByEntityService {
public class Request {
@InvocableVariable(required=true)
public Id recordId;
}
@InvocableMethod(label='Find Past Events' description='Returns HTML list of past events for a given Account or Opportunity')
public static List<String> findEvents(List<Request> requests) {
List<String> results = new List<String>();
if (requests == null || requests.isEmpty() || requests[0].recordId == null) {
results.add('<p style="color: red;">Error: No recordId provided.</p>');
return results;
}
Id recordId = requests[0].recordId;
String entityType = 'General';
String prefix = String.valueOf(recordId).substring(0, 3);
if (prefix == '001') {
entityType = 'Account';
} else if (prefix == '006') {
entityType = 'Opportunity';
}
List<Event> events = [
SELECT Id, Subject, StartDateTime, WhatId, What.Name, Location, Description
FROM Event
WHERE WhatId = :recordId
AND StartDateTime <= :System.now()
ORDER BY StartDateTime DESC
LIMIT 5
];
if (events.isEmpty()) {
results.add('<p>No past events found for this ' + entityType + '.</p>');
return results;
}
String html = '<ul style="list-style-type: none; padding-left: 0;">';
for (Event evt : events) {
html += '<li style="margin-bottom: 15px; padding: 10px; border-left: 3px solid #0176d3; background-color: #f8f9fa;">';
// Subject + link
html += '<strong><a href="/' + evt.Id + '" target="_blank" style="color: #0176d3; text-decoration: none;">';
html += (String.isNotBlank(evt.Subject) ? evt.Subject : 'Untitled Event') + '</a></strong><br/>';
// Date/time
if (evt.StartDateTime != null) {
String formatted = evt.StartDateTime.format('EEEE, MMMM dd, yyyy \'at\' h:mm a');
html += '<span style="color: #706e6b;">đź“… ' + formatted + '</span><br/>';
}
// Location
if (String.isNotBlank(evt.Location)) {
html += '<span style="color: #706e6b;">📍 ' + evt.Location + '</span><br/>';
}
// Related record
if (evt.WhatId != null && evt.What != null) {
html += '<span style="color: #706e6b;">🏢 ' + entityType + ': ';
html += '<a href="/' + evt.WhatId + '" target="_blank" style="color: #0176d3;">' + evt.What.Name + '</a></span><br/>';
}
// Description (optional)
if (String.isNotBlank(evt.Description)) {
String shortDesc = evt.Description.length() > 100 ? evt.Description.substring(0, 100) + '...' : evt.Description;
html += '<span style="color: #706e6b; font-style: italic;">' + shortDesc + '</span>';
}
html += '</li>';
}
html += '</ul>';
results.add(html);
return results;
}
}Update Event Description
Description:
Takes in an Event ID and a block of text (notes), then updates the Description field on the corresponding Salesforce Event. After the update, the Apex action returns a polished HTML summary of the event, making it suitable for embedding in emails, confirmation messages, or CRM summaries. This ensures meeting notes are logged properly and displayed clearly to end users.
Type:
Apex (Invocable Method) — accepts:
eventId(required): ID of the Event record to updatenotes(required): New description text to store in theDescriptionfield
Returns:
- Success status
- Message string (success/failure)
- HTML preview of the updated Event (
eventHtml), including subject, date, location, related record, and truncated description
Apex Class –
public class UpdateEventDescriptionService {
public class Request {
@InvocableVariable(required=true)
public Id eventId;
@InvocableVariable(required=true)
public String notes;
}
public class Response {
@InvocableVariable
public Boolean success;
@InvocableVariable
public String message;
@InvocableVariable
public String eventHtml; // HTML summary of updated event
}
@InvocableMethod(label='Update Event Description' description='Updates Event.Description and returns HTML summary of updated event')
public static List<Response> updateDescription(List<Request> requests) {
List<Response> responses = new List<Response>();
for (Request req : requests) {
Response res = new Response();
try {
Event evt = [
SELECT Id, Subject, StartDateTime, WhatId, What.Name, Location, Description
FROM Event
WHERE Id = :req.eventId
LIMIT 1
];
evt.Description = req.notes;
update evt;
res.success = true;
res.message = 'Event description updated successfully.';
res.eventHtml = buildHtml(evt);
} catch (Exception e) {
res.success = false;
res.message = 'Failed to update event: ' + e.getMessage();
res.eventHtml = '';
}
responses.add(res);
}
return responses;
}
private static String buildHtml(Event evt) {
String html = '<ul style="list-style-type: none; padding-left: 0;">';
html += '<li style="margin-bottom: 15px; padding: 10px; border-left: 3px solid #0176d3; background-color: #f8f9fa;">';
html += '<strong><a href="/' + evt.Id + '" target="_blank" style="color: #0176d3; text-decoration: none;">';
html += (String.isNotBlank(evt.Subject) ? evt.Subject : 'Untitled Event') + '</a></strong><br/>';
if (evt.StartDateTime != null) {
String formatted = evt.StartDateTime.format('EEEE, MMMM dd, yyyy \'at\' h:mm a');
html += '<span style="color: #706e6b;">đź“… ' + formatted + '</span><br/>';
}
if (String.isNotBlank(evt.Location)) {
html += '<span style="color: #706e6b;">📍 ' + evt.Location + '</span><br/>';
}
if (evt.WhatId != null && evt.What != null) {
String entityType = String.valueOf(evt.WhatId).startsWith('006') ? 'Opportunity' : 'Account';
html += '<span style="color: #706e6b;">🏢 ' + entityType + ': ';
html += '<a href="/' + evt.WhatId + '" target="_blank" style="color: #0176d3;">' + evt.What.Name + '</a></span><br/>';
}
if (String.isNotBlank(evt.Description)) {
String shortDesc = evt.Description.length() > 100 ? evt.Description.substring(0, 100) + '...' : evt.Description;
html += '<span style="color: #706e6b; font-style: italic;">' + shortDesc + '</span>';
}
html += '</li></ul>';
return html;
}
}Create Event
Description:
Creates a new Salesforce Event based on structured inputs including subject, description, date, time, and optional duration. This Apex class handles both 12-hour and 24-hour time formats, calculates the correct start and end times, and inserts the event into the system. It also returns a formatted HTML summary of the event that can be displayed in UI or emails, providing a clean preview of the meeting details.
Type:
Apex (Invocable Method) — accepts structured input via Flow or another Apex context. Inputs include:
whatId(Account or Opportunity)subjectdescriptioneventDate(Date)eventTime(String, 12h or 24h)durationInMinutes(optional)
Returns a response with:
- Success status
- Created
eventId - Message string
- Rendered HTML snippet of the event
Apex Class –
public class CreateEvent {
public class Request {
@InvocableVariable(required=true)
public Id whatId;
@InvocableVariable(required=true)
public String subject;
@InvocableVariable(required=true)
public String description;
@InvocableVariable(required=true)
public Date eventDate;
@InvocableVariable(required=true)
public String eventTime;
@InvocableVariable
public Integer durationInMinutes;
}
public class Response {
@InvocableVariable public Boolean success;
@InvocableVariable public String message;
@InvocableVariable public Id eventId;
@InvocableVariable public String eventHtml;
}
@InvocableMethod(label='Create Event' description='Creates a new Event from separate date, time, and duration input')
public static List<Response> createEvents(List<Request> requests) {
List<Response> responses = new List<Response>();
for (Request req : requests) {
Response res = new Response();
try {
Time parsedTime = parseTime(req.eventTime);
DateTime startDT = DateTime.newInstance(req.eventDate, parsedTime);
Integer duration = req.durationInMinutes != null ? req.durationInMinutes : 30;
DateTime endDT = startDT.addMinutes(duration);
Event evt = new Event();
evt.WhatId = req.whatId;
evt.Subject = req.subject;
evt.Description = req.description;
evt.StartDateTime = startDT;
evt.EndDateTime = endDT;
evt.OwnerId = UserInfo.getUserId();
insert evt;
res.success = true;
res.message = 'Event created successfully.';
res.eventId = evt.Id;
res.eventHtml = buildHtml(evt);
} catch (Exception e) {
res.success = false;
res.message = 'Failed to create event: ' + e.getMessage();
res.eventHtml = '';
}
responses.add(res);
}
return responses;
}
private static Time parseTime(String input) {
input = input.trim().toLowerCase();
// 24h format: "15:00"
if (Pattern.matches('\\d{1,2}:\\d{2}', input)) {
List<String> parts = input.split(':');
Integer hour = Integer.valueOf(parts[0]);
Integer minute = Integer.valueOf(parts[1]);
return Time.newInstance(hour, minute, 0, 0);
}
// 12h format: "3:00 PM"
Matcher m = Pattern.compile('(\\d{1,2}):(\\d{2})\\s*(am|pm)').matcher(input);
if (m.matches()) {
Integer hour = Integer.valueOf(m.group(1));
Integer minute = Integer.valueOf(m.group(2));
String ampm = m.group(3);
if (ampm == 'pm' && hour < 12) hour += 12;
if (ampm == 'am' && hour == 12) hour = 0;
return Time.newInstance(hour, minute, 0, 0);
}
throw new IllegalArgumentException('Invalid time format: ' + input);
}
private static String buildHtml(Event evt) {
String html = '<ul style="list-style-type: none; padding-left: 0;">';
html += '<li style="margin-bottom: 15px; padding: 10px; border-left: 3px solid #0176d3; background-color: #f8f9fa;">';
html += '<strong><a href="/' + evt.Id + '" target="_blank" style="color: #0176d3; text-decoration: none;">';
html += (String.isNotBlank(evt.Subject) ? evt.Subject : 'Untitled Event') + '</a></strong><br/>';
if (evt.StartDateTime != null) {
String formatted = evt.StartDateTime.format('EEEE, MMMM dd, yyyy \'at\' h:mm a');
html += '<span style="color: #706e6b;">đź“… ' + formatted + '</span><br/>';
}
if (evt.WhatId != null) {
String entityType = String.valueOf(evt.WhatId).startsWith('006') ? 'Opportunity' : 'Account';
html += '<span style="color: #706e6b;">🏢 ' + entityType + ': ';
html += '<a href="/' + evt.WhatId + '" target="_blank" style="color: #0176d3;">' + evt.WhatId + '</a></span><br/>';
}
if (String.isNotBlank(evt.Description)) {
String shortDesc = evt.Description.length() > 100 ? evt.Description.substring(0, 100) + '...' : evt.Description;
html += '<span style="color: #706e6b; font-style: italic;">' + shortDesc + '</span>';
}
html += '</li></ul>';
return html;
}
}Create Opportunity
Description:
Creates a new Opportunity record using key input fields such as name, amount, stage, close date, and associated account. This Flow-driven action streamlines early pipeline entry by allowing reps or agents to create qualified deals without navigating the UI. It’s typically invoked from a user command or follow-up workflow.
Type:
Flow-only — triggered from a button, intent, or other automation logic using standard Flow actions like:
Create RecordsforOpportunity- Optional:
Get Recordsto relate the Opportunity to an existing Account
