This guide explains how to extend QuikForms with custom functionality using the Plugin Framework.
Plugin Framework Overview
The QuikForms Plugin Framework enables you to:
- Create custom field types (signature pads, address lookup, rich text editors)
- Perform external integrations (CRM sync, payment gateways, webhooks)
- Implement custom validation logic
- Execute business logic before/after form submission
- Work with both low-code (Flow) and pro-code (Apex/JavaScript) approaches
Architecture
The framework consists of four layers:
- Apex Interfaces - Server-side plugin logic
- JavaScript API - Client-side rendering and interactions
- Platform Events - Async integrations
- Custom Metadata - Plugin registration and configuration
Quick Start Example
Follow these steps to create a custom field type plugin.
Step 1: Create the Apex Class
global class MyCustomPlugin implements IQuikFormsPlugin, IQuikFormsCalloutHandler {
global QuikFormsPluginInfo getPluginInfo() {
return new QuikFormsPluginInfo(
'my-custom-plugin',
'1.0.0',
'My Custom Plugin',
'Description of what this plugin does'
);
}
global void initialize(Map<String, Object> configuration) {
// Initialize plugin with configuration
}
global Boolean isReady() {
return true;
}
global QuikFormsCalloutResponse executeCallout(QuikFormsCalloutRequest request) {
// Handle server-side callouts from JavaScript
QuikFormsCalloutResponse response = new QuikFormsCalloutResponse();
response.success = true;
response.data = new Map<String, Object>{'result' => 'success'};
return response;
}
}
Step 2: Create the JavaScript Static Resource
Create a Static Resource named MyCustomPlugin with this content:
(function(global) {
'use strict';
global.initMyCustomPlugin = function(QuikFormsPlugins) {
QuikFormsPlugins.register('my-custom-plugin', {
name: 'my-custom-plugin',
version: '1.0.0',
fieldTypes: {
'my-field-type': {
render: function(field, context) {
var config = field.pluginConfiguration || {};
return '<div class="my-custom-field">' +
' <input type="text" id="field-' + field.id + '" />' +
'</div>';
},
initialize: function(fieldId, container) {
// Set up event listeners, third-party libraries, etc.
var input = container.querySelector('#field-' + fieldId);
if (input) {
input.addEventListener('change', function() {
// Handle changes
});
}
},
getValue: function(fieldId) {
var input = document.getElementById('field-' + fieldId);
return input ? input.value : '';
},
setValue: function(fieldId, value) {
var input = document.getElementById('field-' + fieldId);
if (input) input.value = value;
},
validate: function(fieldId, field) {
var value = this.getValue(fieldId);
if (field.isRequired && !value) {
return {
isValid: false,
errors: ['This field is required']
};
}
return { isValid: true, errors: [] };
}
}
},
hooks: {
onFormLoad: function(context) {
console.log('Form loaded:', context.formConfigId);
},
onFieldChange: function(context) {
console.log('Field changed:', context.fieldId, context.value);
},
onBeforeSubmit: async function(context) {
// Return { abort: true, errors: ['message'] } to stop submission
return { abort: false, errors: [] };
},
onAfterSubmit: async function(context) {
console.log('Form submitted, record ID:', context.recordId);
}
}
});
};
})(window);
Create a QuikForms_Plugin__mdt record:
| Field |
Value |
| Label |
My Custom Plugin |
| DeveloperName |
MyCustomPlugin |
| Plugin_Type__c |
FieldType |
| Apex_Class_Name__c |
MyCustomPlugin |
| JS_Static_Resource__c |
MyCustomPlugin |
| JS_Entry_Point__c |
initMyCustomPlugin |
| Is_Active__c |
true |
| Execution_Order__c |
10 |
| Error_Behavior__c |
Fatal |
| Validation_Mode__c |
Sync |
Create a QuikForms_Custom_Field_Type__mdt record:
| Field |
Value |
| Label |
My Field Type |
| DeveloperName |
MyFieldType |
| Plugin__c |
MyCustomPlugin |
| Field_Type_Name__c |
my-field-type |
| Display_Label__c |
My Custom Field |
| Icon__c |
utility:custom_apps |
| Default_Configuration__c |
{"setting1": "value1"} |
Step 4: Use in a Form
Set a FormField__c record's Plugin_Field_Type__c to my-field-type and optionally provide Plugin_Configuration__c as JSON.
Apex Interfaces
Base interface all plugins must implement:
global interface IQuikFormsPlugin {
QuikFormsPluginInfo getPluginInfo();
void initialize(Map<String, Object> configuration);
Boolean isReady();
}
For custom validation logic:
global interface IQuikFormsFieldValidator extends IQuikFormsPlugin {
QuikFormsValidationResult validateSync(QuikFormsValidationContext context);
QuikFormsValidationResult validateAsync(QuikFormsValidationContext context);
QuikFormsValidationMode getValidationMode();
}
Validation Modes
SYNC_ONLY - Validate immediately on client
ASYNC_ONLY - Validate on server only
SYNC_THEN_ASYNC - Client validation first, then server
ASYNC_ON_BLUR - Server validation when field loses focus
For before/after submission logic:
global interface IQuikFormsSubmissionHandler extends IQuikFormsPlugin {
QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context);
QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context);
}
For server-side HTTP callouts:
global interface IQuikFormsCalloutHandler extends IQuikFormsPlugin {
QuikFormsCalloutResponse executeCallout(QuikFormsCalloutRequest request);
}
JavaScript API
Registering a Plugin
QuikFormsPlugins.register('plugin-name', {
name: 'plugin-name',
version: '1.0.0',
fieldTypes: { /* ... */ },
hooks: { /* ... */ }
});
Available 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 |
onFieldRender |
After field rendered |
fieldId, container |
onValidate |
Before submission validation |
fields, values |
onBeforeSubmit |
Before form submission |
fields, values |
onAfterSubmit |
After successful submission |
recordId, formConfigId |
onFormReset |
When form is reset |
formConfigId |
onFileUpload |
When file is uploaded |
fieldId, file, base64 |
Making Server Callouts
const response = await QuikFormsPlugins.callout({
pluginName: 'my-plugin',
action: 'validateAddress',
data: { address: '123 Main St' }
});
if (response.success) {
console.log('Result:', response.data);
} else {
console.error('Error:', response.errorMessage);
}
Field Type Interface
fieldTypes: {
'field-type-name': {
// Required: Return HTML for the field
render: function(field, context) {
return '<input type="text" id="field-' + field.id + '" />';
},
// Optional: Called after render to set up the field
initialize: function(fieldId, container) {
// Set up event listeners, initialize libraries
},
// Required: Get the current field value
getValue: function(fieldId) {
return document.getElementById('field-' + fieldId).value;
},
// Optional: Set the field value
setValue: function(fieldId, value) {
document.getElementById('field-' + fieldId).value = value;
},
// Optional: Validate the field
validate: function(fieldId, field) {
return { isValid: true, errors: [] };
}
}
}
Data Classes
QuikFormsPluginInfo
global class QuikFormsPluginInfo {
global String name;
global String version;
global String displayName;
global String description;
}
QuikFormsValidationContext
global class QuikFormsValidationContext {
global String fieldId;
global String fieldApiName;
global Object fieldValue;
global String formConfigId;
global Map<String, Object> allFieldValues;
global String locale;
}
QuikFormsValidationResult
global class QuikFormsValidationResult {
global Boolean isValid;
global List<String> errors;
global List<String> warnings;
}
QuikFormsSubmissionContext
global class QuikFormsSubmissionContext {
global String formConfigId;
global String targetObject;
global Id recordId;
global Map<String, Object> fieldValues;
global String locale;
global String submitterIp;
global Boolean isUpdate;
}
QuikFormsSubmissionResult
global class QuikFormsSubmissionResult {
global Boolean abort;
global List<String> errors;
global Map<String, Object> modifiedFieldValues;
}
QuikFormsCalloutRequest
global class QuikFormsCalloutRequest {
global String pluginName;
global String action;
global Map<String, Object> data;
global String formConfigId;
global String locale;
}
QuikFormsCalloutResponse
global class QuikFormsCalloutResponse {
global Boolean success;
global Map<String, Object> data;
global String errorMessage;
global Integer statusCode;
}
QuikForms_Plugin__mdt
Register and configure plugins using this custom metadata type:
| Field |
Description |
Plugin_Type__c |
Type of plugin: FieldType, Validator, SubmissionHandler, CalloutHandler |
Apex_Class_Name__c |
Name of the Apex class implementing the plugin interface |
JS_Static_Resource__c |
Name of the JavaScript Static Resource |
JS_Entry_Point__c |
JavaScript function name to initialize the plugin |
Is_Active__c |
Whether the plugin is enabled |
Execution_Order__c |
Order in which plugins are loaded (lower numbers first) |
Error_Behavior__c |
How errors are handled: Fatal, Warn, Silent |
Validation_Mode__c |
When validation runs: Sync, Async, SyncThenAsync, AsyncOnBlur |
Supports_Callout__c |
Whether the plugin can make server callouts |
QuikForms_Custom_Field_Type__mdt
Define custom field types that can be used in forms:
| Field |
Description |
Plugin__c |
Reference to the QuikForms_Plugin__mdt record |
Field_Type_Name__c |
Unique identifier for the field type (e.g., 'signature-pad') |
Display_Label__c |
User-friendly name shown in the form builder |
Icon__c |
SLDS icon name (e.g., 'utility:signature') |
Default_Configuration__c |
JSON string of default configuration options |
Flow Integration (Low-Code)
For admins who prefer Flow over Apex, use Flow Hooks:
Step 1: Create a Flow
Create an Autolaunched Flow with these input variables:
hookName (Text) - The lifecycle hook name
formConfigId (Text) - The form configuration ID
recordId (Text) - The created record ID (onAfterSubmit only)
fieldValuesJson (Text) - JSON string of field values
locale (Text) - Form locale
And these output variables:
shouldContinue (Boolean) - Whether to continue processing
errorMessage (Text) - Error message if shouldContinue is false
modifiedValuesJson (Text) - JSON of any modified field values
Step 2: Register the Flow Hook
Create a QuikForms_Flow_Hook__mdt record:
| Field |
Value |
| Hook_Name__c |
onAfterSubmit |
| Flow_API_Name__c |
MyQuikFormsHook |
| Form_Config_Filter__c |
(empty for all forms, or comma-separated IDs) |
| Is_Active__c |
true |
| Is_Blocking__c |
false |
| Execution_Order__c |
10 |
Subscribe to these Platform Events for decoupled async processing:
QuikForms_Submission_Complete__e
Published after successful form submission:
| Field |
Description |
Form_Config_Id__c |
The form configuration ID |
Record_Id__c |
The created/updated record ID |
Object_API_Name__c |
Target object API name |
Field_Values_JSON__c |
Submitted values as JSON |
Submission_Timestamp__c |
When the form was submitted |
User_IP__c |
Submitter's IP address |
Locale__c |
Form locale |
QuikForms_Validation_Failed__e
Published when validation fails:
| Field |
Description |
Form_Config_Id__c |
The form configuration ID |
Field_Id__c |
The field that failed validation |
Validation_Errors_JSON__c |
Error details as JSON |
Submitted_Value__c |
The value that failed validation |