Documentation on how to setup a connection between Salesforce and AgentQL with an example Apex Class (short version) that will ready to copy + paste in your org.
To the full blog post: https://salesforcepanda.com/46-2/
To the youtube video: https://www.youtube.com/watch?v=wpQBVCzved0
Steps to connect to AgentQL:
- Create a named credential with this endpoint:
- https://api.agentql.com/v1/query-data
- set ‘X-API-KEY’ as custom header in the named credential
- Get a free API key here:
- Create an apex class for the callout
Ressources:
- My Named Credential is called: AgentQL_v2
- My custom metadata is called: AgentQLQuery__mdt
- the field is called Query__c
My prompt to get company information from a website:
{
company_information {
company_name
street (of the company)
housenumber (of the company)
zipcode (of the company)
city (of the company)
state (of the company)
country (of the company)
phone(Contact number)
email(Company email)
firstname (Name of contact person)
lastname (Name of contact person)
website(Company website)
industry(Industry the company operates in)
opening_hours(Business hours)
social_media_links
}
}
The apex class will be able to get called inside a screen flow. Here are some things to look out for when applying / transfering it in you org:
- I created a custom object called ‘AgentQL_Logs__c’* where I save the request and response (lines 60 – 65 and 88 – 89)
- The named credential I am using to connect is called: AgentQL_v2 (you do want to switch this one out for the one you created – line 18)
- The custom metadata I query: AgentQLQuery__mdt (you do want to switch this one out for the one you created – lines 128 – 131)
- I created a custom fields on Lead object:
- Opening_hours__c
- social_media_links__c
- I wanted to be able to process the response inside my screen flow so I added quite some code at the end (lines 146 – 218)
- The mapping (lines 43 – 57) has to match your prompt as the prompt will shape the fields in the json response. If you add a new field to the prompt you will also have to add it to the mapping
*Editor’s Note: Never, ever, ever name anything after a service provider!
The full apex class: (shorter version down below)
public with sharing class AgentQLCallout {
// Invocable method to fetch job data from AgentQL API
@InvocableMethod(label='Call Agent QL' description='Fetches data from AgentQL API')
public static List<AgentQLResponseWrapper> fetchCompanyData(List<RequestWrapper> requests) {
List<AgentQLResponseWrapper> responses = new List<AgentQLResponseWrapper>();
// Fetch the AgentQL query from Custom Metadata once to avoid redundant SOQL queries
String query = getAgentQLQuery();
if (String.isBlank(query)) {
return new List<AgentQLResponseWrapper>{ new AgentQLResponseWrapper('Error', 'Query not found', null) };
}
Http http = new Http(); // Create an instance of Http for making callouts
for (RequestWrapper req : requests) {
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:AgentQL_v2'); // Named Credential is used here for authentication
request.setMethod('POST'); // HTTP POST method is used to send data
request.setHeader('Content-Type', 'application/json'); // Define content type as JSON
request.setTimeout(60000); // Set timeout to 60 seconds to handle longer responses
// Clean query string to remove unwanted characters
String cleanQuery = cleanString(query);
// Construct request body as a JSON object
Map<String, Object> requestBody = new Map<String, Object> {
'url' => req.url, // URL to be crawled (from input parameter)
'query' => cleanQuery // Query string fetched from metadata
};
request.setBody(JSON.serialize(requestBody)); // Convert request body map to JSON string
try {
// Send the HTTP request and receive response
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) { // Check if response status is OK (200)
// Deserialize JSON response body into a Map structure
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
Map<String, Object> companyData = (Map<String, Object>) ((Map<String, Object>) responseMap.get('data')).get('company_information');
// Extract company information from response
String companyName = (String) companyData.get('company_name');
String companyStreet = (String) companyData.get('street');
String companyHousenumber = (String) companyData.get('housenumber');
String companyZipcode = (String) companyData.get('zipcode');
String companyCity = (String) companyData.get('city');
String companyState = (String) companyData.get('state');
String companyCountry = (String) companyData.get('country');
String companyPhone = (String) companyData.get('phone');
String companyWebsite = (String) companyData.get('website');
String companyEmail = (String) companyData.get('email');
String companyFirstName = (String) companyData.get('firstname');
String companyLastName = (String) companyData.get('lastname');
String companyIndustry = (String) companyData.get('industry');
String companyOpeningHours = (String) companyData.get('opening_hours');
String companySocialMedia = (String) companyData.get('social_media_links');
// Log response details in a custom object (AgentQL_Logs__c)
AgentQL_Logs__c newLog = new AgentQL_Logs__c(
responseBody__c = response.getBody(), // Store full response body
requestBody__c = request.getBody().replace('"url":"' + req.url + '",', ''), // Store request body without the URL
url__c = req.url // Store URL separately
);
insert newLog; // Insert log record into Salesforce
// Create new Lead record based on company data if companyName is not blank
Lead newLead = new Lead(
LastName = String.isBlank(companyLastName) ? 'Unknown' : companyLastName, // Use "Unknown" if companyLastName is empty
Company = companyName,
Street = companyStreet,
PostalCode = companyZipcode,
City = companyCity,
State = companyState,
Country = companyCountry,
Email = companyEmail,
FirstName = companyFirstName,
Industry = companyIndustry,
LeadSource = 'Website Crawl',
Phone = companyPhone,
Website = companyWebsite,
Opening_hours__c = companyOpeningHours,
social_media_links__c = companySocialMedia
);
insert newLead; // Insert lead into Salesforce
// Update the AgentQL_Logs__c record to store the Lead ID
newLog.created_recordId__c = newLead.Id; // Set the created Lead ID in the log record
update newLog; // Update the log record with the Lead ID
// Add successful response to list with log record ID
responses.add(new AgentQLResponseWrapper(
'Success',
response.getBody(),
newLog.Id,
companyName,
companyStreet,
companyHousenumber,
companyZipcode,
companyCity,
companyState,
companyCountry,
companyPhone,
companyWebsite,
companyEmail,
companyFirstName,
companyLastName,
companyIndustry,
companyOpeningHours,
companySocialMedia
));
} else {
// Handle non-200 response codes
responses.add(new AgentQLResponseWrapper('Error: ' + response.getStatusCode(), response.getBody(), null));
}
} catch (CalloutException e) {
// Handle timeout or network issues separately
responses.add(new AgentQLResponseWrapper('Callout Error', 'Read timed out: ' + e.getMessage(), null));
} catch (Exception e) {
// Handle any other unexpected exceptions
responses.add(new AgentQLResponseWrapper('Exception', e.getMessage(), null));
}
}
return responses; // Return list of responses for each request
}
// Helper method to fetch query string from Custom Metadata
private static String getAgentQLQuery() {
List<AgentQLQuery__mdt> queries = [SELECT Query__c FROM AgentQLQuery__mdt LIMIT 1];
return queries.isEmpty() ? '' : queries[0].Query__c; // Return query string or empty if no record found
}
// Helper method to clean query string by removing unwanted whitespace characters
private static String cleanString(String inputText) {
return String.isBlank(inputText) ? '' : inputText.trim().replaceAll('[\r\n\t]', '');
}
// Wrapper class for input parameters
public class RequestWrapper {
@InvocableVariable(label='url' description='URL to be crawled')
public String url;
}
// Wrapper class for API response
public class AgentQLResponseWrapper {
@InvocableVariable(label='Status')
public String status;
@InvocableVariable(label='Response Body')
public String responseBody;
@InvocableVariable(label='Record ID')
public String recordId;
// Added fields for all company data
@InvocableVariable(label='Company Name')
public String companyName;
@InvocableVariable(label='Street')
public String companyStreet;
@InvocableVariable(label='House Number')
public String companyHousenumber;
@InvocableVariable(label='Zipcode')
public String companyZipcode;
@InvocableVariable(label='City')
public String companyCity;
@InvocableVariable(label='State')
public String companyState;
@InvocableVariable(label='Country')
public String companyCountry;
@InvocableVariable(label='Phone')
public String companyPhone;
@InvocableVariable(label='Website')
public String companyWebsite;
@InvocableVariable(label='Email')
public String companyEmail;
@InvocableVariable(label='First Name')
public String companyFirstName;
@InvocableVariable(label='Last Name')
public String companyLastName;
@InvocableVariable(label='Industry')
public String companyIndustry;
@InvocableVariable(label='Opening Hours')
public String companyOpeningHours;
@InvocableVariable(label='social_media_links')
public String companySocialMedia;
// Constructor to initialize response wrapper
public AgentQLResponseWrapper(String status, String responseBody, String recordId) {
this.status = status;
this.responseBody = responseBody;
this.recordId = recordId;
}
// Overloaded constructor to handle additional fields
public AgentQLResponseWrapper(String status, String responseBody, String recordId,
String companyName, String companyStreet, String companyHousenumber,
String companyZipcode, String companyCity, String companyState,
String companyCountry, String companyPhone, String companyWebsite,
String companyEmail, String companyFirstName, String companyLastName,
String companyIndustry, String companyOpeningHours, String companySocialMedia) {
this.status = status;
this.responseBody = responseBody;
this.recordId = recordId;
this.companyName = companyName;
this.companyStreet = companyStreet;
this.companyHousenumber = companyHousenumber;
this.companyZipcode = companyZipcode;
this.companyCity = companyCity;
this.companyState = companyState;
this.companyCountry = companyCountry;
this.companyPhone = companyPhone;
this.companyWebsite = companyWebsite;
this.companyEmail = companyEmail;
this.companyFirstName = companyFirstName;
this.companyLastName = companyLastName;
this.companyIndustry = companyIndustry;
this.companyOpeningHours = companyOpeningHours;
this.companySocialMedia = companySocialMedia;
}
}
}
Code language: JavaScript (javascript)
The short version (you will still have to change the named credential in line 15 and custom metadata in line 64 – 65)
- no custom object
- no custom fields
- no retrieving the fields to process inside flow
- retrieving and mapping one step
Short version of the apex class
public with sharing class AgentQLCalloutMinimal {
@InvocableMethod(label='Call Agent QL' description='Fetches data from AgentQL API')
public static void fetchCompanyData(List<RequestWrapper> requests) { // Changed return type to void
String query = getAgentQLQuery();
if (String.isBlank(query)) {
return; // Exit early if query is missing
}
Http http = new Http();
for (RequestWrapper req : requests) {
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:AgentQL_v2');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
request.setTimeout(60000);
String cleanQuery = cleanString(query);
Map<String, Object> requestBody = new Map<String, Object> {
'url' => req.url,
'query' => cleanQuery
};
request.setBody(JSON.serialize(requestBody));
try {
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
Map<String, Object> companyData = (Map<String, Object>) ((Map<String, Object>) responseMap.get('data')).get('company_information');
String companyName = (String) companyData.get('company_name');
if (!String.isBlank(companyName)) {
Lead newLead = new Lead(
LastName = String.isBlank((String) companyData.get('lastname')) ? 'Unknown' : (String) companyData.get('lastname'),
Company = companyName,
Street = (String) companyData.get('street'),
PostalCode = (String) companyData.get('zipcode'),
City = (String) companyData.get('city'),
State = (String) companyData.get('state'),
Country = (String) companyData.get('country'),
Email = (String) companyData.get('email'),
FirstName = (String) companyData.get('firstname'),
Industry = (String) companyData.get('industry'),
LeadSource = 'Website Crawl',
Phone = (String) companyData.get('phone'),
Website = (String) companyData.get('website'),
Opening_hours__c = (String) companyData.get('opening_hours'),
social_media_links__c = (String) companyData.get('social_media_links')
);
insert newLead;
}
}
} catch (Exception e) {
// Do nothing, just fail silently
}
}
}
private static String getAgentQLQuery() {
List<AgentQLQuery__mdt> queries = [SELECT Query__c FROM AgentQLQuery__mdt LIMIT 1];
return queries.isEmpty() ? '' : queries[0].Query__c;
}
private static String cleanString(String inputText) {
return String.isBlank(inputText) ? '' : inputText.trim().replaceAll('[\r\n\t]', '');
}
public class RequestWrapper {
@InvocableVariable(label='url' description='URL to be crawled')
public String url;
}
}
Code language: JavaScript (javascript)
Leave a Reply