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

  1. Navigate to Setup → Custom Metadata Types
  2. Click Manage Records next to QuikForms_Plugin
  3. 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 QuikExceptionLogger for 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.