Skip to content

Salesforce Action Launcher Feature Documentation ​

Overview ​

Implementation guide for adding Prepare and Present action buttons in Salesforce that launch Pitcher applications using custom settings for domain and connection configuration, with full record context.

Setup Requirements ​

1. Create Custom Settings (Hierarchical) ​

Navigate to Setup → Custom Settings → New

Name: Pitcher_Settings__c
Setting Type: Hierarchy
Visibility: Public

Fields:
1. Pitcher_Domain__c (Text, 255) - Example: "demo" (for demo.my.pitcher.com)
2. Connection_ID__c (Text, 255) - Provided by Pitcher

2. Configure Custom Settings ​

apex
// Via Anonymous Apex or Setup UI
Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
settings.Pitcher_Domain__c = 'your-domain'; // provided by Pitcher
settings.Connection_ID__c = 'your-connection-id'; // provided by Pitcher
upsert settings;

Complete Implementation with Record Context ​

Apex Controller with Full Context ​

PitcherActionController.cls ​

apex
public with sharing class PitcherActionController {
    
    @AuraEnabled(cacheable=true)
    public static Map<String, Object> getPitcherSettings() {
        try {
            Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
            
            // Check if settings exist and are configured
            Boolean isConfigured = settings != null && 
                                 String.isNotBlank(settings.Pitcher_Domain__c) && 
                                 String.isNotBlank(settings.Connection_ID__c);
            
            return new Map<String, Object>{
                'domain' => settings?.Pitcher_Domain__c,
                'connectionId' => settings?.Connection_ID__c,
                'isConfigured' => isConfigured,
                'orgId' => UserInfo.getOrganizationId()
            };
        } catch (Exception e) {
            throw new AuraHandledException('Error retrieving Pitcher settings: ' + e.getMessage());
        }
    }
    
    @AuraEnabled(cacheable=true)
    public static RecordWithInfo getRecordWithInfo(Id recordId) {
        // Get the object type of the record
        String objectType = recordId.getSObjectType().getDescribe().getName();

        String accountIdField = null;
        String accountId = null;

        if (objectType == 'Event') {
            // Handle Event object exceptional case
            accountIdField = 'WhatId';
            SObject record = [SELECT WhatId FROM Event WHERE Id = :recordId LIMIT 1];
            accountId = (String) record.get(accountIdField);
        } else if (objectType == 'Account') {
            // Handle Account object exceptional case
            accountIdField = 'Id';
            accountId = recordId;
        } else {
            // Describe the object to get its fields
            Schema.SObjectType sObjectType = Schema.getGlobalDescribe().get(objectType);
            Map<String, Schema.SObjectField> fieldMap = sObjectType.getDescribe().fields.getMap();

            for (String fieldName : fieldMap.keySet()) {
                Schema.SObjectField field = fieldMap.get(fieldName);
                if (field.getDescribe().getType() == Schema.DisplayType.REFERENCE && 
                    field.getDescribe().getReferenceTo().toString().contains('Account')) {
                    accountIdField = fieldName;
                    break;
                }
            }

            // Query the record to get the AccountId field value
            if (accountIdField != null) {
                String query = 'SELECT ' + accountIdField + ' FROM ' + objectType + ' WHERE Id = :recordId';
                SObject record = Database.query(query);
                accountId = (String) record.get(accountIdField);
            }
        }

        RecordWithInfo result = new RecordWithInfo();
        result.recordId = recordId;
        result.accountIdField = accountIdField;
        result.accountId = accountId;
        result.objectType = objectType;
        return result;
    }

    public class RecordWithInfo {
        @AuraEnabled public Id recordId;
        @AuraEnabled public String accountIdField;
        @AuraEnabled public String accountId;
        @AuraEnabled public String objectType;
    }
}

Lightning Web Component with Full Parameters ​

actionLauncher.html ​

html
<template>
    <lightning-card title="Pitcher Actions" icon-name="action:flow">
        <div class="slds-p-around_medium">
            <template if:true={isReady}>
                <div class="slds-button-group-row">
                    <lightning-button 
                        label="Prepare" 
                        variant="brand"
                        onclick={handlePrepare}
                        icon-name="utility:setup"
                        disabled={!isReady}>
                    </lightning-button>
                    <lightning-button 
                        label="Present" 
                        variant="success"
                        onclick={handlePresent}
                        icon-name="utility:call"
                        class="slds-m-left_x-small"
                        disabled={!isReady}>
                    </lightning-button>
                </div>
            </template>
            <template if:false={isConfigured}>
                <div class="slds-text-color_error">
                    Pitcher settings not configured. Please contact your administrator.
                </div>
            </template>
            <template if:true={isLoading}>
                <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
            </template>
        </div>
    </lightning-card>
</template>

actionLauncher.js ​

javascript
import { LightningElement, api, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getPitcherSettings from '@salesforce/apex/PitcherActionController.getPitcherSettings';
import getRecordWithInfo from '@salesforce/apex/PitcherActionController.getRecordWithInfo';

export default class ActionLauncher extends LightningElement {
    @api recordId;
    
    // Settings data
    pitcherDomain;
    connectionId;
    orgId;
    isConfigured = false;
    
    // Record data
    recordInfo;
    
    // UI state
    isLoading = true;
    settingsLoaded = false;
    recordLoaded = false;

    @wire(getPitcherSettings)
    wiredSettings({ error, data }) {
        if (data) {
            this.pitcherDomain = data.domain;
            this.connectionId = data.connectionId;
            this.orgId = data.orgId;
            this.isConfigured = data.isConfigured;
            this.settingsLoaded = true;
            this.checkIfReady();
        } else if (error) {
            console.error('Error loading Pitcher settings:', error);
            this.showToast('Error', 'Failed to load Pitcher settings', 'error');
            this.isLoading = false;
        }
    }

    @wire(getRecordWithInfo, { recordId: '$recordId' })
    wiredRecordInfo({ error, data }) {
        if (data) {
            this.recordInfo = data;
            this.recordLoaded = true;
            this.checkIfReady();
        } else if (error) {
            console.error('Error loading record info:', error);
            this.showToast('Error', 'Failed to load record information', 'error');
            this.isLoading = false;
        }
    }

    checkIfReady() {
        if (this.settingsLoaded && this.recordLoaded) {
            this.isLoading = false;
        }
    }

    get isReady() {
        return this.isConfigured && !this.isLoading && this.recordInfo;
    }

    handlePrepare() {
        this.launchPitcher('pitcherai', false);
    }

    handlePresent() {
        this.launchPitcher('start_call', true);
    }

    launchPitcher(action, isStart) {
        if (!this.isReady) {
            this.showToast('Configuration Error', 'System not ready or configured', 'error');
            return;
        }

        // Build comprehensive parameters matching Pitcher's expected format
        const params = new URLSearchParams({
            PostcallId: this.recordId || '',
            AccountId: this.recordInfo.accountId || '',
            Type: this.recordInfo.objectType || '',
            orgId: this.orgId,
            timestamp: Date.now().toString(),
            start: isStart.toString()
        });

        // Build redirect URL following Pitcher format
        const redirectUrl = `https://${this.pitcherDomain}.my.pitcher.com/?connection=${this.connectionId}&next=/${action}?${params.toString()}`;
        
        console.log('Launching Pitcher with URL:', redirectUrl);
        
        window.open(
            redirectUrl,
            isStart ? 'pitcherPresent' : 'pitcherPrepare',
            'width=1600,height=900,toolbar=no,location=no,menubar=no'
        );

        const actionLabel = isStart ? 'Presentation' : 'Preparation';
        this.showToast('Launching', `Opening Pitcher ${actionLabel}...`, 'success');
    }

    showToast(title, message, variant) {
        this.dispatchEvent(
            new ShowToastEvent({
                title: title,
                message: message,
                variant: variant
            })
        );
    }
}

actionLauncher.js-meta.xml ​

xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Pitcher Action Launcher</masterLabel>
    <description>Launch Pitcher AI and presentation tools with full context</description>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <target>lightning__UtilityBar</target>
    </targets>
</LightningComponentBundle>

Alternative: Visualforce Implementation with Full Context ​

PitcherActionPage.page ​

html
<apex:page controller="PitcherActionPageController" showHeader="false" sidebar="false">
    <script>
        var pitcherDomain = '{!JSENCODE(pitcherDomain)}';
        var connectionId = '{!JSENCODE(connectionId)}';
        var orgId = '{!JSENCODE(orgId)}';
        var recordId = '{!$CurrentPage.parameters.recordId}';
        var recordInfo = null;
        
        // Load record info on page load using vanilla JavaScript
        window.onload = function() {
            if (recordId) {
                Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.PitcherActionPageController.getRecordInfo}',
                    recordId,
                    function(result, event) {
                        if (event.status) {
                            recordInfo = result;
                            enableButtons();
                        } else {
                            console.error('Failed to load record info:', event.message);
                        }
                    }
                );
            }
        };
        
        function enableButtons() {
            document.getElementById('prepareBtn').disabled = false;
            document.getElementById('presentBtn').disabled = false;
        }
        
        function launchPrepare() {
            launchPitcher('pitcherai', false);
        }
        
        function launchPresent() {
            launchPitcher('start_call', true);
        }
        
        function buildQueryString(params) {
            var queryString = '';
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    if (queryString.length > 0) queryString += '&';
                    queryString += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
                }
            }
            return queryString;
        }
        
        function launchPitcher(action, isStart) {
            if (!pitcherDomain || !connectionId) {
                alert('Pitcher settings not configured');
                return;
            }
            
            var params = buildQueryString({
                PostcallId: recordId || '',
                AccountId: recordInfo ? recordInfo.accountId : '',
                Type: recordInfo ? recordInfo.objectType : '',
                orgId: orgId,
                timestamp: Date.now(),
                start: isStart
            });
            
            var redirectUrl = 'https://' + pitcherDomain + '.my.pitcher.com/?connection=' + 
                            connectionId + '&next=/' + action + '?' + params;
            
            window.open(redirectUrl, isStart ? 'pitcherPresent' : 'pitcherPrepare', 
                       'width=1600,height=900');
        }
    </script>
    
    <div style="padding: 20px; text-align: center;">
        <button id="prepareBtn" onclick="launchPrepare()" disabled="true"
                style="padding: 10px 20px; margin: 5px; background: #0070d2; color: white; 
                       border: none; border-radius: 4px; cursor: pointer;">
            Prepare
        </button>
        <button id="presentBtn" onclick="launchPresent()" disabled="true"
                style="padding: 10px 20px; margin: 5px; background: #04844b; color: white; 
                       border: none; border-radius: 4px; cursor: pointer;">
            Present
        </button>
    </div>
</apex:page>

PitcherActionPageController.cls ​

apex
public with sharing class PitcherActionPageController {
    
    public String pitcherDomain { get; private set; }
    public String connectionId { get; private set; }
    public String orgId { get; private set; }
    
    public PitcherActionPageController() {
        loadPitcherSettings();
        orgId = UserInfo.getOrganizationId();
    }
    
    private void loadPitcherSettings() {
        try {
            Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
            if (settings != null) {
                pitcherDomain = settings.Pitcher_Domain__c;
                connectionId = settings.Connection_ID__c;
            }
        } catch (Exception e) {
            ApexPages.addMessage(new ApexPages.Message(
                ApexPages.Severity.ERROR, 
                'Error loading Pitcher settings: ' + e.getMessage()
            ));
        }
    }
    
    @RemoteAction
    public static RecordWithInfo getRecordInfo(Id recordId) {
        return PitcherActionController.getRecordWithInfo(recordId);
    }
    
    public class RecordWithInfo {
        public Id recordId;
        public String accountIdField;
        public String accountId;
        public String objectType;
    }
}

Test Class with Full Coverage ​

apex
@isTest
public class PitcherActionControllerTest {
    
    @testSetup
    static void setup() {
        // Create test custom settings
        Pitcher_Settings__c settings = new Pitcher_Settings__c();
        settings.Pitcher_Domain__c = 'test-domain';
        settings.Connection_ID__c = 'test-connection-123';
        insert settings;
        
        // Create test Account
        Account testAccount = new Account(Name = 'Test Account');
        insert testAccount;
        
        // Create test Opportunity
        Opportunity testOpp = new Opportunity(
            Name = 'Test Opportunity',
            AccountId = testAccount.Id,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30)
        );
        insert testOpp;
        
        // Create test Event
        Event testEvent = new Event(
            Subject = 'Test Event',
            WhatId = testAccount.Id,
            StartDateTime = DateTime.now(),
            EndDateTime = DateTime.now().addHours(1)
        );
        insert testEvent;
    }
    
    @isTest
    static void testGetPitcherSettings() {
        Test.startTest();
        Map<String, Object> result = PitcherActionController.getPitcherSettings();
        Test.stopTest();
        
        System.assertEquals('test-domain', result.get('domain'));
        System.assertEquals('test-connection-123', result.get('connectionId'));
        System.assertEquals(true, result.get('isConfigured'));
        System.assertNotEquals(null, result.get('orgId'));
    }
    
    @isTest
    static void testGetRecordWithInfoOpportunity() {
        Opportunity opp = [SELECT Id, AccountId FROM Opportunity LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(opp.Id);
        Test.stopTest();
        
        System.assertEquals(opp.Id, result.recordId);
        System.assertEquals('Opportunity', result.objectType);
        System.assertEquals(opp.AccountId, result.accountId);
        System.assertEquals('AccountId', result.accountIdField);
    }
    
    @isTest
    static void testGetRecordWithInfoAccount() {
        Account acc = [SELECT Id FROM Account LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(acc.Id);
        Test.stopTest();
        
        System.assertEquals(acc.Id, result.recordId);
        System.assertEquals('Account', result.objectType);
        System.assertEquals(acc.Id, result.accountId);
        System.assertEquals('Id', result.accountIdField);
    }
    
    @isTest
    static void testGetRecordWithInfoEvent() {
        Event evt = [SELECT Id, WhatId FROM Event LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(evt.Id);
        Test.stopTest();
        
        System.assertEquals(evt.Id, result.recordId);
        System.assertEquals('Event', result.objectType);
        System.assertEquals(evt.WhatId, result.accountId);
        System.assertEquals('WhatId', result.accountIdField);
    }
}

URL Format with Complete Parameters ​

Prepare URL Example ​

https://demo.my.pitcher.com/?connection=abc123&next=/pitcherai?PostcallId=0061234567890AB&AccountId=0011234567890AB&Type=Opportunity&orgId=00D1234567890AB&timestamp=1704067200000&start=false

Present URL Example ​

https://demo.my.pitcher.com/?connection=abc123&next=/start_call?PostcallId=0061234567890AB&AccountId=0011234567890AB&Type=Opportunity&orgId=00D1234567890AB&timestamp=1704067200000&start=true

URL Parameters Reference ​

ParameterDescriptionExample
PostcallIdCurrent record ID0061234567890AB
AccountIdRelated Account ID0011234567890AB
TypeObject type of current recordOpportunity, Lead, Account, Event
orgIdSalesforce Organization ID00D1234567890AB
timestampCurrent timestamp in milliseconds1704067200000
startBoolean for Present actiontrue/false

Deployment Checklist ​

  • [ ] Create Pitcher_Settings__c custom setting
  • [ ] Deploy PitcherActionController.cls with test class
  • [ ] Deploy Lightning Web Component files
  • [ ] Configure custom settings with Pitcher domain and connection ID
  • [ ] Add Remote Site Settings for *.my.pitcher.com
  • [ ] Add CSP Trusted Sites for *.my.pitcher.com
  • [ ] Add component to page layouts
  • [ ] Test with different record types (Account, Opportunity, Event)
  • [ ] Verify popup blocker settings
  • [ ] Validate URL parameters in browser developer tools

Security Considerations ​

  1. Field Level Security: Ensure users have read access to:

    • Pitcher_Settings__c custom setting
    • Account lookup fields on objects
    • Record IDs they're accessing
  2. Apex Security: Classes use with sharing to respect record access

  3. Remote Sites: Configure for HTTPS only

  4. Parameter Validation: All parameters are encoded to prevent injection

Troubleshooting Guide ​

Debug JavaScript Console ​

javascript
// Add to LWC for debugging
connectedCallback() {
    console.log('=== Pitcher Debug Info ===');
    console.log('Org ID:', this.orgId);
    console.log('Record ID:', this.recordId);
    console.log('Pitcher Domain:', this.pitcherDomain);
    console.log('Connection ID:', this.connectionId);
    console.log('Record Info:', this.recordInfo);
    console.log('Is Configured:', this.isConfigured);
    console.log('Is Ready:', this.isReady);
}

Common Issues and Solutions ​

  1. Buttons Disabled

    • Check if custom settings are populated
    • Verify record ID is being passed
    • Check browser console for errors
  2. Invalid Integration Error

    • Verify connection ID with Pitcher support
    • Check domain spelling and format
    • Test URL directly in browser
  3. Missing Account ID

    • Verify object has Account lookup field
    • Check field-level security
    • Review special cases (Event, Account objects)