API Reference
Technical documentation for QuikForms plugin framework, custom metadata, integrations, and APIs.
Overview
QuikForms provides a comprehensive plugin framework for extending functionality through custom validators, submission handlers, callout handlers, and field types. Integration points include Flow actions, Platform Events, and a JavaScript API for embedded forms.
Plugin-First Architecture
All extensibility in QuikForms is built on a plugin framework that allows you to add custom behavior without modifying core code. Plugins are registered via Custom Metadata and can be configured per form or globally.
Custom Metadata
QuikForms uses Custom Metadata Types for configuration and plugin registration, enabling packaged deployment and environment-agnostic setup.
QuikForms_Setting__mdt
Core application settings for QuikForms. This custom metadata contains a single record that controls various QuikForms behaviors such as reCAPTCHA integration, rate limiting, analytics, survey functionality, and exception logging.
Fields
| Field | Type | Description |
|---|---|---|
Default_Form_Object__c |
Text(100) | Default Salesforce object for new forms (e.g., 'Case', 'Lead', 'Contact') |
Google_Analytics_Enabled__c |
Checkbox | Enable Google Analytics tracking for form submissions |
Google_Analytics_Site_ID__c |
Text(255) | Google Analytics tracking ID (e.g., 'UA-XXXXXXXXX-X' or 'G-XXXXXXXXXX') |
Rate_Limit_Per_Minute__c |
Number(18,0) | Number of form submissions allowed per minute per IP address (default: 60) |
Recaptcha_Site_Key__c |
Text(255) | Google reCAPTCHA v3 site key for bot protection |
Valid_Referrer_1__c |
Text(255) | First valid referrer URL. Requests must match one of the valid referrers if specified |
Valid_Referrer_2__c |
Text(255) | Second valid referrer URL for additional allowed sources |
Valid_Referrer_3__c |
Text(255) | Third valid referrer URL for additional allowed sources |
Valid_Survey_Days__c |
Number(3,0) | Number of days a survey or survey feedback link remains valid (default: 60) |
Public_Site_URL__c |
Text(255) | Public Salesforce Site URL where QuikForms are hosted (e.g., https://yoursite.force.com) |
Exception_Log_Level__c |
Picklist | Logging verbosity: None, Critical, Error (default), Warning, Info, Debug |
Exception_Log_Retention_Days__c |
Number(3,0) | Days to retain exception logs before auto-deletion (default: 7) |
Example Configuration
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>default</label>
<protected>false</protected>
<values>
<field>Default_Form_Object__c</field>
<value xsi:type="xsd:string">Case</value>
</values>
<values>
<field>Google_Analytics_Enabled__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>Google_Analytics_Site_ID__c</field>
<value xsi:type="xsd:string">G-XXXXXXXXXX</value>
</values>
<values>
<field>Rate_Limit_Per_Minute__c</field>
<value xsi:type="xsd:double">60.0</value>
</values>
<values>
<field>Recaptcha_Site_Key__c</field>
<value xsi:type="xsd:string">6LeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</value>
</values>
<values>
<field>Valid_Survey_Days__c</field>
<value xsi:type="xsd:double">60.0</value>
</values>
<values>
<field>Public_Site_URL__c</field>
<value xsi:type="xsd:string">https://yoursite.force.com</value>
</values>
<values>
<field>Exception_Log_Level__c</field>
<value xsi:type="xsd:string">Error</value>
</values>
<values>
<field>Exception_Log_Retention_Days__c</field>
<value xsi:type="xsd:double">7.0</value>
</values>
</CustomMetadata>
QuikForms_Plugin__mdt
Plugin registration for extending QuikForms with custom field types, validators, submission handlers, and integrations. Plugins combine Apex classes (server-side) with JavaScript modules (client-side) for rich functionality.
Fields
| Field | Type | Description |
|---|---|---|
Plugin_Type__c |
Picklist | Plugin category: FieldType, Validator, SubmissionHandler, Integration |
Apex_Class_Name__c |
Text(255) | Fully qualified name of Apex class implementing IQuikFormsPlugin |
JS_Static_Resource__c |
Text(255) | Static Resource name containing the JavaScript module |
JS_Entry_Point__c |
Text(255) | JavaScript entry point function name (e.g., 'initMyPlugin') |
Is_Active__c |
Checkbox | Whether this plugin is active and loaded on forms |
Execution_Order__c |
Number(18,0) | Execution order for plugins of same type (lower executes first) |
Error_Behavior__c |
Picklist | Error handling: Fatal (block submission), Skippable (log and continue), Warn (show warning) |
Validation_Mode__c |
Picklist | For validators: Sync (immediate), Async (on blur), Both |
Supports_Callout__c |
Checkbox | Whether plugin makes HTTP callouts (implements IQuikFormsCalloutHandler) |
Timeout_Ms__c |
Number(18,0) | Timeout in milliseconds for async operations (default: 10000) |
Configuration_Schema__c |
Long Text Area | JSON schema defining valid configuration options |
Description__c |
Text(255) | Description of plugin functionality |
Icon__c |
Text(255) | SLDS icon identifier (e.g., 'utility:signature') |
Version__c |
Text(50) | Plugin version (e.g., '1.0.0') |
Example Plugin Registration
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>Signature Capture</label>
<protected>false</protected>
<values>
<field>Apex_Class_Name__c</field>
<value xsi:type="xsd:string">SignatureCapturePlugin</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string">Canvas-based signature capture with validation</value>
</values>
<values>
<field>Error_Behavior__c</field>
<value xsi:type="xsd:string">Fatal</value>
</values>
<values>
<field>Execution_Order__c</field>
<value xsi:type="xsd:double">10.0</value>
</values>
<values>
<field>Icon__c</field>
<value xsi:type="xsd:string">utility:signature</value>
</values>
<values>
<field>Is_Active__c</field>
<value xsi:type="xsd:boolean">true</value>
</values>
<values>
<field>JS_Entry_Point__c</field>
<value xsi:type="xsd:string">initSignatureCapturePlugin</value>
</values>
<values>
<field>JS_Static_Resource__c</field>
<value xsi:type="xsd:string">QuikFormsSignaturePlugin</value>
</values>
<values>
<field>Plugin_Type__c</field>
<value xsi:type="xsd:string">FieldType</value>
</values>
<values>
<field>Validation_Mode__c</field>
<value xsi:type="xsd:string">Sync</value>
</values>
<values>
<field>Version__c</field>
<value xsi:type="xsd:string">1.0.0</value>
</values>
</CustomMetadata>
QuikForms_Custom_Field_Type__mdt
Defines custom field types provided by plugins. Each field type represents a unique UI component that can be added to forms (e.g., signature pad, rich text editor, address lookup).
Fields
| Field | Type | Description |
|---|---|---|
Plugin__c |
Metadata Relationship | Reference to the parent QuikForms_Plugin__mdt record |
Field_Type_Name__c |
Text(255) | Unique identifier for this field type (e.g., 'signature-capture') |
Display_Label__c |
Text(255) | User-friendly label shown in form builder |
Icon__c |
Text(255) | SLDS icon for form builder UI (e.g., 'utility:signature') |
Default_Configuration__c |
Long Text Area | Default JSON configuration for new fields of this type |
Render_Template__c |
Long Text Area | Optional HTML template for server-side rendering |
Category__c |
Text(100) | Category for grouping in form builder (e.g., 'Input', 'Specialized') |
Supported_Object_Fields__c |
Long Text Area | Comma-separated Salesforce field types this can map to |
Example Custom Field Type
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>Signature Pad</label>
<protected>false</protected>
<values>
<field>Category__c</field>
<value xsi:type="xsd:string">Specialized</value>
</values>
<values>
<field>Default_Configuration__c</field>
<value xsi:type="xsd:string">{
"strokeColor": "#000000",
"strokeWidth": 2,
"canvasHeight": 200
}</value>
</values>
<values>
<field>Display_Label__c</field>
<value xsi:type="xsd:string">Signature Pad</value>
</values>
<values>
<field>Field_Type_Name__c</field>
<value xsi:type="xsd:string">signature-capture</value>
</values>
<values>
<field>Icon__c</field>
<value xsi:type="xsd:string">utility:signature</value>
</values>
<values>
<field>Plugin__c</field>
<value xsi:type="xsd:string">QuikForms_Plugin.SignatureCapture</value>
</values>
<values>
<field>Supported_Object_Fields__c</field>
<value xsi:type="xsd:string">Text,LongTextArea</value>
</values>
</CustomMetadata>
QuikForms_Flow_Hook__mdt
Low-code integration allowing Flows to respond to QuikForms lifecycle events without writing Apex code. Ideal for admins who want to add custom logic using declarative tools.
Fields
| Field | Type | Description |
|---|---|---|
Hook_Name__c |
Text(255) | Lifecycle hook: onFormLoad, onBeforeSubmit, onAfterSubmit, etc. |
Flow_API_Name__c |
Text(255) | API name of the autolaunched Flow to execute |
Form_Config_Filter__c |
Long Text Area | Comma-separated form config IDs (empty = all forms) |
Is_Active__c |
Checkbox | Whether this Flow hook is active |
Is_Blocking__c |
Checkbox | If true, Flow can prevent submission by returning shouldContinue=false |
Execution_Order__c |
Number(18,0) | Order for multiple Flow hooks on same event |
Description__c |
Text(255) | Description of what this Flow hook does |
Example Flow Hook
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>Send Welcome Email After Contact Submit</label>
<protected>false</protected>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string">Sends welcome email to new contacts</value>
</values>
<values>
<field>Execution_Order__c</field>
<value xsi:type="xsd:double">10.0</value>
</values>
<values>
<field>Flow_API_Name__c</field>
<value xsi:type="xsd:string">QuikForms_Send_Welcome_Email</value>
</values>
<values>
<field>Hook_Name__c</field>
<value xsi:type="xsd:string">onAfterSubmit</value>
</values>
<values>
<field>Is_Active__c</field>
<value xsi:type="xsd:boolean">true</value>
</values>
<values>
<field>Is_Blocking__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
</CustomMetadata>
Plugin Framework
The QuikForms Plugin Framework enables extension through a combination of Apex interfaces (server-side) and JavaScript API (client-side). Plugins can create custom field types, add validation logic, handle form submissions, and integrate with external systems.
Architecture Overview
The framework consists of four layers:
- Apex Interfaces - Server-side plugin logic and validation
- JavaScript API - Client-side rendering, interactions, and hooks
- Platform Events - Asynchronous integrations and decoupled processing
- Custom Metadata - Plugin registration and configuration
Apex Interfaces
All plugins implement the base IQuikFormsPlugin interface and may implement additional specialized interfaces.
IQuikFormsPlugin (Required)
global interface IQuikFormsPlugin {
/**
* Returns plugin metadata (name, version, description)
* @return QuikFormsPluginInfo object
*/
QuikFormsPluginInfo getPluginInfo();
/**
* Initialize the plugin with configuration
* @param configuration Configuration map from plugin metadata
*/
void initialize(Map<String, Object> configuration);
/**
* Check if plugin is ready to execute
* @return true if plugin is initialized and ready
*/
Boolean isReady();
}
IQuikFormsFieldValidator
For custom validation logic on form fields.
global interface IQuikFormsFieldValidator extends IQuikFormsPlugin {
/**
* Synchronous validation (executes immediately)
* @param context Validation context with field data
* @return Validation result with errors/warnings
*/
QuikFormsValidationResult validateSync(QuikFormsValidationContext context);
/**
* Asynchronous validation (executes on field blur)
* @param context Validation context
* @return Validation result
*/
QuikFormsValidationResult validateAsync(QuikFormsValidationContext context);
/**
* Get validation mode for this validator
* @return Validation mode enum
*/
QuikFormsValidationMode getValidationMode();
}
IQuikFormsSubmissionHandler
For custom logic before/after form submission.
global interface IQuikFormsSubmissionHandler extends IQuikFormsPlugin {
/**
* Execute before form submission (can modify values or abort)
* @param context Submission context with form data
* @return Result indicating whether to continue
*/
QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context);
/**
* Execute after successful form submission
* @param context Submission context with record ID
* @return Result for logging or additional actions
*/
QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context);
}
IQuikFormsCalloutHandler
For plugins that make HTTP callouts to external services.
global interface IQuikFormsCalloutHandler extends IQuikFormsPlugin {
/**
* Execute a server-side HTTP callout
* @param request Callout request with action and data
* @return Response with success status and data
*/
QuikFormsCalloutResponse executeCallout(QuikFormsCalloutRequest request);
}
JavaScript API
Plugins register client-side functionality through the QuikFormsPlugins global object.
Plugin Registration
QuikFormsPlugins.register('plugin-name', {
name: 'plugin-name',
version: '1.0.0',
// Custom field types
fieldTypes: {
'field-type-name': {
render: function(field, context) { /* Return HTML */ },
initialize: function(fieldId, container) { /* Setup */ },
getValue: function(fieldId) { /* Return value */ },
setValue: function(fieldId, value) { /* Set value */ },
validate: function(fieldId, field) { /* Validate */ }
}
},
// Lifecycle hooks
hooks: {
onFormLoad: function(context) { /* ... */ },
onFieldChange: function(context) { /* ... */ },
onBeforeSubmit: async function(context) { /* ... */ },
onAfterSubmit: async function(context) { /* ... */ }
}
});
Available JavaScript Hooks
| Hook | When Called | Context Properties |
|---|---|---|
onFormLoad |
After form data loaded | formConfigId, locale, fields |
onFieldChange |
Field value changes | fieldId, value, previousValue |
onFieldBlur |
Field loses focus | fieldId, value |
onBeforeSubmit |
Before form submission | fields, values (return {abort: true} to block) |
onAfterSubmit |
After successful submission | recordId, formConfigId |
onFormReset |
When form is reset | formConfigId |
Making Server Callouts from JavaScript
const response = await QuikFormsPlugins.callout({
pluginName: 'my-plugin',
action: 'validateAddress',
data: { address: '123 Main St', zip: '12345' }
});
if (response.success) {
console.log('Valid address:', response.data);
} else {
console.error('Validation failed:', response.errorMessage);
}
Platform Events
QuikForms publishes Platform Events for asynchronous integration and decoupled processing.
QuikForms_Submission_Complete__e
Published after successful form submission. Subscribe to this event for post-submission integrations like CRM sync, email notifications, or analytics.
| Field | Type | Description |
|---|---|---|
Form_Config_Id__c |
Text(18) | The form configuration ID |
Record_Id__c |
Text(18) | The created/updated record ID |
Object_API_Name__c |
Text(255) | Target object API name (e.g., 'Case', 'Lead') |
Field_Values_JSON__c |
Long Text Area | Submitted field values as JSON |
Submission_Timestamp__c |
DateTime | When the form was submitted |
User_IP__c |
Text(45) | Submitter's IP address |
Locale__c |
Text(10) | Form locale (e.g., 'en_US') |
QuikForms_Validation_Failed__e
Published when validation fails. Use for analytics, security monitoring, or debugging.
| Field | Type | Description |
|---|---|---|
Form_Config_Id__c |
Text(18) | The form configuration ID |
Field_Id__c |
Text(18) | The field that failed validation |
Validation_Errors_JSON__c |
Long Text Area | Error details as JSON |
Submitted_Value__c |
Long Text Area | The value that failed validation |
User_IP__c |
Text(45) | Submitter's IP address |
Timestamp__c |
DateTime | When validation failed |
Developer Resources
For detailed plugin development examples, tutorials, and best practices, see the Plugin Developer Guide.
Custom Field Validators
Field validators provide custom validation logic that runs during form submission.
Validator Interface
public interface IQuikFormsFieldValidator extends IQuikFormsPlugin {
/**
* Synchronous validation (runs immediately on submission)
* @param context Validation context with field value and form data
* @return Validation result with pass/fail and error messages
*/
QuikFormsValidationResult validateSync(
QuikFormsValidationContext context
);
/**
* Asynchronous validation (runs in queueable for long-running checks)
* @param context Validation context
* @return Validation result
*/
QuikFormsValidationResult validateAsync(
QuikFormsValidationContext context
);
/**
* Validation mode supported by this validator
* @return 'Sync', 'Async', or 'Both'
*/
String getValidationMode();
}
Context Object
public class QuikFormsValidationContext {
public String fieldId; // Field being validated
public Object fieldValue; // Current field value
public String formConfigId; // Form identifier
public String locale; // User locale (e.g., 'en_US')
public Map<String, Object> allFieldValues; // All form field values
public Map<String, Object> metadata; // Additional metadata
}
Result Object
public class QuikFormsValidationResult {
public Boolean isValid; // Pass/fail status
public List<String> errorMessages; // Error messages to display
public List<String> warningMessages; // Warning messages
public Object transformedValue; // Transformed value (optional)
public Map<String, Object> metadata; // Additional result data
}
Validator Examples
Example 1: Email Domain Validator
public class EmailDomainValidator implements IQuikFormsFieldValidator {
private List<String> allowedDomains;
private String errorMessage;
public void initialize(Map<String, Object> config) {
this.allowedDomains = (List<String>)config.get('allowedDomains');
this.errorMessage = (String)config.get('errorMessage');
}
public QuikFormsValidationResult validateSync(
QuikFormsValidationContext context
) {
QuikFormsValidationResult result = new QuikFormsValidationResult();
String email = (String)context.fieldValue;
if (String.isBlank(email)) {
result.isValid = true;
return result;
}
String domain = email.substringAfter('@').toLowerCase();
result.isValid = allowedDomains.contains(domain);
if (!result.isValid) {
result.errorMessages = new List<String>{ errorMessage };
}
return result;
}
public QuikFormsValidationResult validateAsync(
QuikFormsValidationContext context
) {
return validateSync(context); // Not needed for this validator
}
public String getValidationMode() {
return 'Sync';
}
public String getPluginName() {
return 'Email Domain Validator';
}
public String getVersion() {
return '1.0.0';
}
}
Example 2: Duplicate Phone Checker (Async)
public class DuplicatePhoneValidator implements IQuikFormsFieldValidator {
private String sobjectType;
private String phoneField;
public void initialize(Map<String, Object> config) {
this.sobjectType = (String)config.get('sobjectType');
this.phoneField = (String)config.get('phoneField');
}
public QuikFormsValidationResult validateSync(
QuikFormsValidationContext context
) {
// Sync not supported - return valid to skip
return new QuikFormsValidationResult(true, null);
}
public QuikFormsValidationResult validateAsync(
QuikFormsValidationContext context
) {
QuikFormsValidationResult result = new QuikFormsValidationResult();
String phone = (String)context.fieldValue;
// Check for existing records with this phone number
String query = String.format(
'SELECT Id FROM {0} WHERE {1} = :phone LIMIT 1',
new List<String>{ sobjectType, phoneField }
);
List<SObject> existingRecords = Database.query(query);
result.isValid = existingRecords.isEmpty();
if (!result.isValid) {
result.errorMessages = new List<String>{
'A record with this phone number already exists'
};
}
return result;
}
public String getValidationMode() {
return 'Async';
}
public String getPluginName() {
return 'Duplicate Phone Checker';
}
public String getVersion() {
return '1.0.0';
}
}
Submission Handlers
Submission handlers allow you to execute custom logic before and after form submission.
Handler Interface
public interface IQuikFormsSubmissionHandler extends IQuikFormsPlugin {
/**
* Execute before form submission is processed
* @param context Submission context
* @return Result indicating success/failure and any modifications
*/
QuikFormsSubmissionResult onBeforeSubmit(
QuikFormsSubmissionContext context
);
/**
* Execute after form submission is processed
* @param context Submission context (includes created record IDs)
* @return Result indicating success/failure
*/
QuikFormsSubmissionResult onAfterSubmit(
QuikFormsSubmissionContext context
);
}
Context Object
public class QuikFormsSubmissionContext {
public String formConfigId; // Form identifier
public String submissionId; // Unique submission ID
public Map<String, Object> fieldValues; // All submitted field values
public List<Id> createdRecordIds; // Records created (after only)
public String userLocale; // User's locale
public String ipAddress; // Submitter's IP address
public Map<String, Object> metadata; // Additional context
}
Handler Examples
Example 1: Lead Enrichment Handler
public class LeadEnrichmentHandler implements IQuikFormsSubmissionHandler {
private String apiKey;
private String apiEndpoint;
public void initialize(Map<String, Object> config) {
this.apiKey = (String)config.get('apiKey');
this.apiEndpoint = (String)config.get('apiEndpoint');
}
public QuikFormsSubmissionResult onBeforeSubmit(
QuikFormsSubmissionContext context
) {
QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
try {
// Enrich lead data from external API
String email = (String)context.fieldValues.get('Email');
Map<String, Object> enrichedData = callEnrichmentAPI(email);
// Add enriched data to field values
if (enrichedData != null) {
context.fieldValues.put('Company', enrichedData.get('company'));
context.fieldValues.put('Title', enrichedData.get('title'));
context.fieldValues.put('Industry', enrichedData.get('industry'));
}
result.success = true;
result.modifiedFieldValues = context.fieldValues;
} catch (Exception e) {
// Log error but don't fail submission
System.debug('Lead enrichment failed: ' + e.getMessage());
result.success = true; // Continue submission anyway
}
return result;
}
public QuikFormsSubmissionResult onAfterSubmit(
QuikFormsSubmissionContext context
) {
// No post-processing needed
return new QuikFormsSubmissionResult(true);
}
private Map<String, Object> callEnrichmentAPI(String email) {
HttpRequest req = new HttpRequest();
req.setEndpoint(apiEndpoint + '?email=' + EncodingUtil.urlEncode(email, 'UTF-8'));
req.setMethod('GET');
req.setHeader('Authorization', 'Bearer ' + apiKey);
req.setTimeout(5000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
}
return null;
}
public String getPluginName() {
return 'Lead Enrichment Handler';
}
public String getVersion() {
return '1.0.0';
}
}
Example 2: Slack Notification Handler
public class SlackNotificationHandler implements IQuikFormsSubmissionHandler {
private String webhookUrl;
private String channelName;
public void initialize(Map<String, Object> config) {
this.webhookUrl = (String)config.get('webhookUrl');
this.channelName = (String)config.get('channelName');
}
public QuikFormsSubmissionResult onBeforeSubmit(
QuikFormsSubmissionContext context
) {
// No pre-processing needed
return new QuikFormsSubmissionResult(true);
}
public QuikFormsSubmissionResult onAfterSubmit(
QuikFormsSubmissionContext context
) {
QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
try {
// Send notification to Slack
String message = buildSlackMessage(context);
sendToSlack(message);
result.success = true;
} catch (Exception e) {
System.debug('Slack notification failed: ' + e.getMessage());
result.success = true; // Don't fail submission
}
return result;
}
private String buildSlackMessage(QuikFormsSubmissionContext context) {
Map<String, Object> payload = new Map<String, Object>{
'channel' => channelName,
'text' => '🎉 New form submission received!',
'attachments' => new List<Object>{
new Map<String, Object>{
'color' => '#1e5ba8',
'fields' => buildFieldsList(context.fieldValues)
}
}
};
return JSON.serialize(payload);
}
private List<Object> buildFieldsList(Map<String, Object> fieldValues) {
List<Object> fields = new List<Object>();
for (String key : fieldValues.keySet()) {
fields.add(new Map<String, Object>{
'title' => key,
'value' => String.valueOf(fieldValues.get(key)),
'short' => true
});
}
return fields;
}
@future(callout=true)
private static void sendToSlack(String message) {
HttpRequest req = new HttpRequest();
req.setEndpoint(webhookUrl);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(message);
Http http = new Http();
http.send(req);
}
public String getPluginName() {
return 'Slack Notification Handler';
}
public String getVersion() {
return '1.0.0';
}
}
Callout Handlers
Callout handlers enable integration with external systems during form processing.
Callout Interface
public interface IQuikFormsCalloutHandler extends IQuikFormsPlugin {
/**
* Execute HTTP callout to external system
* @param context Callout context
* @return Callout result with response data
*/
QuikFormsCalloutResult executeCallout(
QuikFormsCalloutContext context
);
/**
* Handle callout response and transform data
* @param response HTTP response
* @param context Original context
* @return Transformed result
*/
QuikFormsCalloutResult processResponse(
HttpResponse response,
QuikFormsCalloutContext context
);
}
Callout Examples
Example: Salesforce External Org Integration
public class SalesforceExternalOrgCallout implements IQuikFormsCalloutHandler {
private String namedCredential;
private String endpoint;
public void initialize(Map<String, Object> config) {
this.namedCredential = (String)config.get('namedCredential');
this.endpoint = (String)config.get('endpoint');
}
public QuikFormsCalloutResult executeCallout(
QuikFormsCalloutContext context
) {
QuikFormsCalloutResult result = new QuikFormsCalloutResult();
try {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:' + namedCredential + endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(context.fieldValues));
req.setTimeout(30000);
Http http = new Http();
HttpResponse res = http.send(req);
result = processResponse(res, context);
} catch (Exception e) {
result.success = false;
result.errorMessage = 'External system error: ' + e.getMessage();
}
return result;
}
public QuikFormsCalloutResult processResponse(
HttpResponse response,
QuikFormsCalloutContext context
) {
QuikFormsCalloutResult result = new QuikFormsCalloutResult();
if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) {
result.success = true;
result.responseData =
(Map<String, Object>)JSON.deserializeUntyped(response.getBody());
} else {
result.success = false;
result.errorMessage = 'HTTP ' + response.getStatusCode() +
': ' + response.getStatus();
}
return result;
}
public String getPluginName() {
return 'Salesforce External Org Integration';
}
public String getVersion() {
return '1.0.0';
}
}
Custom Field Types
Create custom field types with specialized rendering and validation logic.
Field Type Interface
public interface IQuikFormsCustomFieldType extends IQuikFormsPlugin {
/**
* Get Lightning Web Component name for rendering this field
* @return LWC component name (e.g., 'c:customSignatureField')
*/
String getLWCComponentName();
/**
* Get field configuration schema
* @return JSON schema for field configuration
*/
Map<String, Object> getConfigSchema();
/**
* Transform submitted value before saving
* @param value Raw submitted value
* @param config Field configuration
* @return Transformed value
*/
Object transformValue(Object value, Map<String, Object> config);
}
Field Type Example
Example: Signature Pad Field Type
public class SignaturePadFieldType implements IQuikFormsCustomFieldType {
public void initialize(Map<String, Object> config) {
// No initialization needed
}
public String getLWCComponentName() {
return 'c:quikFormsSignaturePad';
}
public Map<String, Object> getConfigSchema() {
return new Map<String, Object>{
'width' => new Map<String, Object>{
'type' => 'number',
'default' => 500,
'label' => 'Canvas Width'
},
'height' => new Map<String, Object>{
'type' => 'number',
'default' => 200,
'label' => 'Canvas Height'
},
'backgroundColor' => new Map<String, Object>{
'type' => 'string',
'default' => '#FFFFFF',
'label' => 'Background Color'
}
};
}
public Object transformValue(Object value, Map<String, Object> config) {
// Value is base64-encoded PNG image
String base64Data = (String)value;
// Store as ContentVersion for document storage
if (String.isNotBlank(base64Data)) {
ContentVersion cv = new ContentVersion();
cv.Title = 'Signature';
cv.PathOnClient = 'signature.png';
cv.VersionData = EncodingUtil.base64Decode(
base64Data.substringAfter('base64,')
);
cv.IsMajorVersion = true;
insert cv;
return cv.Id;
}
return null;
}
public String getPluginName() {
return 'Signature Pad Field Type';
}
public String getVersion() {
return '1.0.0';
}
}
Flow Integration
QuikForms provides Invocable Actions for use in Salesforce Flows.
Available Invocable Actions
1. Submit Form Data
@InvocableMethod(
label='QuikForms: Submit Form Data'
description='Submit form data programmatically'
category='QuikForms'
)
public static List<SubmitFormResult> submitFormData(
List<SubmitFormRequest> requests
) {
// Implementation
}
Input Parameters
| Parameter | Type | Description |
|---|---|---|
formConfigId |
String | Form configuration identifier |
fieldValuesJSON |
String | JSON map of field IDs to values |
skipValidation |
Boolean | Skip field validation (default: false) |
2. Get Form Configuration
@InvocableMethod(
label='QuikForms: Get Form Configuration'
description='Retrieve form configuration and field definitions'
category='QuikForms'
)
public static List<FormConfigResult> getFormConfiguration(
List<FormConfigRequest> requests
) {
// Implementation
}
Flow Examples
Example Flow: Auto-Submit Form from Account Record
Flow Screenshot: Record-Triggered Flow that auto-submits a form when Account is updated
{
"flowType": "Record-Triggered",
"triggerObject": "Account",
"triggerType": "Update",
"actions": [
{
"type": "InvocableAction",
"action": "QuikForms: Submit Form Data",
"inputs": {
"formConfigId": "account-change-notification",
"fieldValuesJSON": "{\"accountId\": \"{!$Record.Id}\", \"changes\": \"{!$Record.ChangeDescription__c}\"}"
}
}
]
}
Platform Events
QuikForms publishes Platform Events for real-time integrations.
Published Events
QuikForms_Submission__e
Published when a form is successfully submitted.
| Field | Type | Description |
|---|---|---|
Form_Config_Id__c |
Text(255) | Form configuration identifier |
Submission_Id__c |
Text(255) | Unique submission identifier |
Field_Values_JSON__c |
Long Text Area | Submitted field values as JSON |
Created_Record_Ids__c |
Long Text Area | Comma-separated IDs of created records |
User_Id__c |
Text(18) | Submitting user ID (if authenticated) |
Submission_Timestamp__c |
DateTime | Time of submission |
Platform Event Examples
Example 1: Subscribe to Form Submissions
public class FormSubmissionEventHandler {
@future
public static void handleSubmission(String eventJSON) {
QuikForms_Submission__e event =
(QuikForms_Submission__e)JSON.deserialize(
eventJSON,
QuikForms_Submission__e.class
);
// Process submission
Map<String, Object> fieldValues =
(Map<String, Object>)JSON.deserializeUntyped(
event.Field_Values_JSON__c
);
// Custom logic here
System.debug('Form submitted: ' + event.Form_Config_Id__c);
System.debug('Field values: ' + fieldValues);
}
}
Example 2: Platform Event Trigger
trigger QuikFormsSubmissionTrigger on QuikForms_Submission__e (after insert) {
List<String> highPriorityForms = new List<String>{
'contact-us', 'demo-request', 'enterprise-inquiry'
};
for (QuikForms_Submission__e event : Trigger.new) {
if (highPriorityForms.contains(event.Form_Config_Id__c)) {
// Send immediate notification for high-priority forms
FormSubmissionEventHandler.handleSubmission(
JSON.serialize(event)
);
}
}
}
JavaScript API
The QuikForms JavaScript API allows programmatic control of embedded forms.
Available Methods
Initialize Form
// Initialize QuikForms instance
const quikForm = new QuikForms({
formConfigId: 'contact-us',
containerId: 'form-container',
locale: 'en_US',
theme: 'light',
onSuccess: (response) => {
console.log('Form submitted successfully', response);
},
onError: (error) => {
console.error('Form submission failed', error);
}
});
Get Field Value
// Get value of a specific field
const email = quikForm.getFieldValue('email');
Set Field Value
// Set value programmatically
quikForm.setFieldValue('firstName', 'John');
quikForm.setFieldValue('lastName', 'Doe');
Validate Form
// Validate all fields
const validationResult = await quikForm.validate();
if (validationResult.isValid) {
console.log('Form is valid');
} else {
console.log('Validation errors:', validationResult.errors);
}
Submit Form
// Programmatically submit form
quikForm.submit()
.then(response => {
console.log('Submission successful', response);
})
.catch(error => {
console.error('Submission failed', error);
});
Reset Form
// Clear all field values
quikForm.reset();
JavaScript API Examples
Example 1: Pre-fill Form from URL Parameters
// Parse URL parameters and pre-fill form
const urlParams = new URLSearchParams(window.location.search);
const quikForm = new QuikForms({
formConfigId: 'lead-capture',
containerId: 'lead-form'
});
// Wait for form to load
quikForm.onReady(() => {
// Pre-fill from URL parameters
if (urlParams.has('email')) {
quikForm.setFieldValue('email', urlParams.get('email'));
}
if (urlParams.has('company')) {
quikForm.setFieldValue('company', urlParams.get('company'));
}
if (urlParams.has('source')) {
quikForm.setFieldValue('leadSource', urlParams.get('source'));
}
});
Example 2: Custom Validation Before Submit
const quikForm = new QuikForms({
formConfigId: 'enterprise-contact',
containerId: 'contact-form',
onBeforeSubmit: async (formData) => {
// Custom validation logic
const email = formData.email;
const domain = email.split('@')[1];
// Check if corporate email
const freeEmailProviders = ['gmail.com', 'yahoo.com', 'hotmail.com'];
if (freeEmailProviders.includes(domain)) {
return {
valid: false,
error: 'Please use your corporate email address'
};
}
// Additional async validation
const isValid = await validateEmailWithAPI(email);
return {
valid: isValid,
error: isValid ? null : 'Email validation failed'
};
}
});
Example 3: Multi-Step Form with Progress Tracking
const quikForm = new QuikForms({
formConfigId: 'multi-step-application',
containerId: 'application-form'
});
let currentStep = 1;
const totalSteps = 3;
// Show/hide fields based on current step
function updateFormStep(step) {
const allFields = quikForm.getAllFields();
allFields.forEach(field => {
const fieldStep = parseInt(field.dataset.step);
if (fieldStep === step) {
quikForm.showField(field.id);
} else {
quikForm.hideField(field.id);
}
});
updateProgressBar(step);
}
// Next button handler
document.getElementById('next-btn').addEventListener('click', async () => {
// Validate current step fields
const isValid = await quikForm.validateStep(currentStep);
if (isValid) {
currentStep++;
updateFormStep(currentStep);
}
});
// Previous button handler
document.getElementById('prev-btn').addEventListener('click', () => {
currentStep--;
updateFormStep(currentStep);
});
// Submit on final step
quikForm.on('submit', () => {
if (currentStep === totalSteps) {
quikForm.submit();
}
});
function updateProgressBar(step) {
const progress = (step / totalSteps) * 100;
document.getElementById('progress-bar').style.width = progress + '%';
}
Creating a Plugin
Follow these steps to create and deploy a custom plugin.
Step 1: Project Setup
Create a new Apex class in your Salesforce org:
# Using SFDX CLI
sfdx force:apex:class:create -n EmailDomainValidator -d force-app/main/default/classes
Step 2: Implement Interface
Choose the appropriate interface based on your plugin type and implement all required methods. All plugins must implement the base IQuikFormsPlugin interface.
Interface Reference
See the sections above for detailed interface definitions and examples for each plugin type.
Step 3: Register Plugin
Register your plugin using Custom Metadata:
Option 1: Setup UI
- Navigate to Setup → Custom Metadata Types
- Click Manage Records next to QuikForms_Plugin
- Click New and fill in the fields:
| Field | Example Value |
|---|---|
| Label | Email Domain Validator |
| Plugin Type | FieldValidator |
| Apex Class Name | EmailDomainValidator |
| Is Active | ✓ (checked) |
| Priority | 10 |
| Configuration JSON | {"allowedDomains": ["company.com"]} |
Option 2: Metadata XML
See the Custom Metadata section above for XML example.
Step 4: Deploy
Deploy your plugin class and Custom Metadata record:
# Deploy Apex class
sfdx force:source:deploy -p force-app/main/default/classes/EmailDomainValidator.cls
# Deploy Custom Metadata
sfdx force:source:deploy -p force-app/main/default/customMetadata
# Or deploy everything
sfdx force:source:deploy -p force-app
Best Practices
Error Handling
- Always use try-catch blocks - Wrap plugin logic in try-catch to prevent unhandled exceptions
- Log errors appropriately - Use
QuikExceptionLoggerfor consistent error logging - Return graceful error messages - Provide user-friendly error messages, not technical stack traces
- Don't fail submissions for non-critical errors - Log warnings instead of blocking submission when possible
public QuikFormsSubmissionResult onAfterSubmit(
QuikFormsSubmissionContext context
) {
QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
try {
// Plugin logic here
result.success = true;
} catch (Exception e) {
QuikExceptionLogger.log(e, 'Plugin execution failed');
// Don't fail submission for notification errors
result.success = true;
result.warnings.add('Notification failed but submission was successful');
}
return result;
}
Performance
- Keep validation logic lightweight - Minimize processing time for sync validators
- Cache expensive computations - Store results in static variables when appropriate
- Use async validation for callouts - External API calls should use async mode
- Implement timeouts for external calls - Set reasonable timeouts (5-30 seconds)
- Avoid SOQL queries in loops - Bulkify your code to handle multiple records
- Use selective queries - Add WHERE clauses and LIMIT statements
// ❌ Bad - Query in loop
for (String email : emails) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE Email = :email LIMIT 1];
}
// ✅ Good - Bulkified query
Set<String> emailSet = new Set<String>(emails);
Map<String, Contact> contactsByEmail = new Map<String, Contact>();
for (Contact c : [SELECT Id, Email FROM Contact WHERE Email IN :emailSet]) {
contactsByEmail.put(c.Email, c);
}
Configuration
- Make plugins configurable - Use Custom Metadata Configuration_JSON__c field
- Validate configuration in initialize() - Check for required config values
- Provide sensible defaults - Don't require configuration for every option
- Document configuration options - Add comments or use JSON schema
public void initialize(Map<String, Object> config) {
// Required configuration
if (!config.containsKey('apiKey')) {
throw new QuikFormsPluginException('API key is required');
}
this.apiKey = (String)config.get('apiKey');
// Optional configuration with defaults
this.timeout = config.containsKey('timeout')
? (Integer)config.get('timeout')
: 30000; // Default 30 seconds
this.retryAttempts = config.containsKey('retryAttempts')
? (Integer)config.get('retryAttempts')
: 3; // Default 3 retries
}
Security
- Validate all input data - Never trust user-provided values
- Sanitize user-provided values - Escape HTML, validate formats
- Use 'with sharing' when appropriate - Respect object and field-level security
- Respect field-level security - Check
isAccessible()before reading fields - Don't log sensitive data - Avoid logging passwords, tokens, PII
- Use Named Credentials - Store API credentials securely
public with sharing class SecureSubmissionHandler
implements IQuikFormsSubmissionHandler {
public QuikFormsSubmissionResult onBeforeSubmit(
QuikFormsSubmissionContext context
) {
// Validate input
String email = (String)context.fieldValues.get('email');
if (String.isBlank(email) || !email.contains('@')) {
throw new QuikFormsPluginException('Invalid email format');
}
// Sanitize HTML input
String comments = (String)context.fieldValues.get('comments');
if (String.isNotBlank(comments)) {
comments = comments.escapeHtml4();
context.fieldValues.put('comments', comments);
}
return new QuikFormsSubmissionResult(true);
}
}
Testing Plugins
Unit Tests
Create comprehensive unit tests for all plugin methods:
@isTest
private class EmailDomainValidatorTest {
@isTest
static void testAllowedDomain() {
// Setup
EmailDomainValidator validator = new EmailDomainValidator();
Map<String, Object> config = new Map<String, Object>{
'allowedDomains' => new List<String>{'company.com', 'partner.com'}
};
validator.initialize(config);
// Create context
QuikFormsValidationContext context = new QuikFormsValidationContext();
context.fieldId = 'email_field';
context.fieldValue = '[email protected]';
context.formConfigId = 'test-form';
// Test
Test.startTest();
QuikFormsValidationResult result = validator.validateSync(context);
Test.stopTest();
// Verify
System.assert(result.isValid, 'Email from allowed domain should be valid');
System.assertEquals(0, result.errorMessages.size(), 'Should have no errors');
}
@isTest
static void testBlockedDomain() {
// Setup
EmailDomainValidator validator = new EmailDomainValidator();
Map<String, Object> config = new Map<String, Object>{
'blockedDomains' => new List<String>{'tempmail.com', 'spam.com'}
};
validator.initialize(config);
// Create context
QuikFormsValidationContext context = new QuikFormsValidationContext();
context.fieldValue = '[email protected]';
// Test
Test.startTest();
QuikFormsValidationResult result = validator.validateSync(context);
Test.stopTest();
// Verify
System.assert(!result.isValid, 'Email from blocked domain should be invalid');
System.assert(result.errorMessages.size() > 0, 'Should have error message');
}
@isTest
static void testNullEmail() {
EmailDomainValidator validator = new EmailDomainValidator();
validator.initialize(new Map<String, Object>());
QuikFormsValidationContext context = new QuikFormsValidationContext();
context.fieldValue = null;
QuikFormsValidationResult result = validator.validateSync(context);
System.assert(result.isValid, 'Null email should pass validation');
}
}
Integration Tests
Test plugins in the context of form submission with mock callouts:
@isTest
private class LeadEnrichmentHandlerTest {
@isTest
static void testEnrichmentSuccess() {
// Setup mock HTTP callout
Test.setMock(HttpCalloutMock.class, new EnrichmentAPIMock());
// Create handler and initialize
LeadEnrichmentHandler handler = new LeadEnrichmentHandler();
Map<String, Object> config = new Map<String, Object>{
'apiKey' => 'test-key',
'apiEndpoint' => 'https://api.enrichment.example.com'
};
handler.initialize(config);
// Create submission context
QuikFormsSubmissionContext context = new QuikFormsSubmissionContext();
context.formConfigId = 'lead-form';
context.submissionId = 'test-123';
context.fieldValues = new Map<String, Object>{
'Email' => '[email protected]',
'Company' => 'Acme Corp'
};
// Test
Test.startTest();
QuikFormsSubmissionResult result = handler.onBeforeSubmit(context);
Test.stopTest();
// Verify
System.assert(result.success, 'Enrichment should succeed');
System.assert(
result.modifiedFieldValues.containsKey('Title'),
'Should have enriched title field'
);
System.assertEquals(
'CEO',
result.modifiedFieldValues.get('Title'),
'Should have correct enriched value'
);
}
@isTest
static void testEnrichmentFailureGraceful() {
// Setup mock that returns error
Test.setMock(HttpCalloutMock.class, new EnrichmentAPIErrorMock());
LeadEnrichmentHandler handler = new LeadEnrichmentHandler();
handler.initialize(getTestConfig());
QuikFormsSubmissionContext context = new QuikFormsSubmissionContext();
context.fieldValues = new Map<String, Object>{
'Email' => '[email protected]'
};
Test.startTest();
QuikFormsSubmissionResult result = handler.onBeforeSubmit(context);
Test.stopTest();
// Verify graceful failure
System.assert(result.success, 'Should not fail submission on enrichment error');
System.assert(
result.warnings != null && result.warnings.size() > 0,
'Should have warning message'
);
}
// Mock HTTP response for successful API call
private class EnrichmentAPIMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"title":"CEO","industry":"Technology"}');
return res;
}
}
// Mock HTTP response for API error
private class EnrichmentAPIErrorMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(500);
res.setStatus('Internal Server Error');
return res;
}
}
}
Test Coverage Best Practices
- Test all execution paths - Cover success, failure, and edge cases
- Test with null/empty values - Ensure plugins handle missing data
- Test configuration validation - Verify initialize() throws errors for invalid config
- Mock external dependencies - Use
Test.setMock()for HTTP callouts - Verify governor limits - Test with bulk data to ensure bulkification
- Test error handling - Verify graceful failures and error messages
You're Ready to Build!
You now have everything needed to create powerful, production-ready plugins for QuikForms. Start with a simple validator and gradually add more complex functionality as needed.