Plugin Development Guide

Learn how to extend QuikForms with custom validators, submission handlers, and field types.

Overview

QuikForms provides a powerful plugin framework that allows you to extend form functionality with custom business logic, integrations, and field types without modifying core code.

What You Can Build

  • Field Validators - Custom validation logic for fields
  • Submission Handlers - Pre/post-submission business logic
  • Callout Handlers - External API integrations
  • Custom Field Types - New field types with custom rendering

No Core Modifications Required

Plugins are registered via Custom Metadata Types, allowing you to add/remove functionality without touching the QuikForms codebase.

Plugin Architecture

The plugin system consists of three main components:

  1. Plugin Interfaces - Define the contract for plugins
  2. Plugin Registry - Discovers and loads plugins from Custom Metadata
  3. Plugin Execution - Orchestrates plugin execution with error handling
Plugin architecture diagram showing interface, registry, and execution flow

Plugin Architecture

Plugin Lifecycle

  1. Plugin registered in QuikForms_Plugin__mdt
  2. Registry loads active plugins on initialization
  3. Plugin class instantiated and initialize() called
  4. Plugin executes when triggered (field validation, form submission, etc.)
  5. Results returned to caller

Plugin Types

Field Validators

Validate field values with custom business logic.

Use Cases

  • Email domain whitelist/blacklist
  • Credit card validation
  • Phone number formatting
  • Custom regex patterns
  • Cross-field validation
  • External API validation (address lookup, etc.)

Validation Modes

  • Synchronous - Immediate validation (client-side)
  • Asynchronous - Server-side validation with callouts
  • Both - Validate on both client and server

Submission Handlers

Execute custom logic before or after form submission.

Use Cases

  • Data enrichment (lookup additional data)
  • External system integration
  • Email notifications
  • Platform Events
  • Custom object creation
  • Workflow triggering

Execution Points

  • Before Submit - Modify field values, add computed fields, validate
  • After Submit - Post-processing, notifications, integrations

Callout Handlers

Handle server-side callouts from JavaScript plugins.

Use Cases

  • Address autocomplete
  • Real-time data lookup
  • Third-party API integration
  • Dynamic field population

Custom Field Types

Create new field types with custom rendering and behavior.

Use Cases

  • Signature capture
  • File upload with preview
  • Rich text editor
  • Date range picker
  • Custom visualizations

Creating Plugins

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 plugin type.

Base Interface (Required for All)

public interface IQuikFormsPlugin {
    QuikFormsPluginInfo getPluginInfo();
    void initialize(Map<String, Object> configuration);
    Boolean isReady();
}

Field Validator Interface

public interface IQuikFormsFieldValidator extends IQuikFormsPlugin {
    QuikFormsValidationResult validateSync(QuikFormsValidationContext context);
    QuikFormsValidationResult validateAsync(QuikFormsValidationContext context);
    String getValidationMode(); // 'Sync', 'Async', or 'Both'
}

Submission Handler Interface

public interface IQuikFormsSubmissionHandler extends IQuikFormsPlugin {
    QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context);
    QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context);
}

Step 3: Register Plugin

Create a Custom Metadata record for your plugin:

  1. Navigate to Setup → Custom Metadata Types
  2. Click Manage Records next to QuikForms_Plugin
  3. Click New
  4. Configure the plugin record
Field Value
Label Email Domain Validator
Plugin Name Email_Domain_Validator
Plugin Type FieldValidator
Apex Class Name EmailDomainValidator
Is Active ✓ (checked)
Execution Order 10
Validation Mode Sync
Error Behavior Fatal

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

Field Validator Example

Complete example of a field validator plugin:

public class EmailDomainValidator implements IQuikFormsFieldValidator {

    private List<String> allowedDomains;
    private List<String> blockedDomains;

    // IQuikFormsPlugin methods
    public QuikFormsPluginInfo getPluginInfo() {
        return new QuikFormsPluginInfo(
            'Email Domain Validator',
            '1.0',
            'Validates email addresses against allowed/blocked domain lists'
        );
    }

    public void initialize(Map<String, Object> configuration) {
        // Load configuration
        this.allowedDomains = (List<String>)configuration.get('allowedDomains');
        this.blockedDomains = (List<String>)configuration.get('blockedDomains');

        // Default to empty lists if not configured
        if (this.allowedDomains == null) {
            this.allowedDomains = new List<String>();
        }
        if (this.blockedDomains == null) {
            this.blockedDomains = new List<String>();
        }
    }

    public Boolean isReady() {
        // Plugin is ready if at least one domain list is configured
        return !this.allowedDomains.isEmpty() || !this.blockedDomains.isEmpty();
    }

    // IQuikFormsFieldValidator methods
    public String getValidationMode() {
        return 'Sync'; // Client-side validation
    }

    public QuikFormsValidationResult validateSync(QuikFormsValidationContext context) {
        QuikFormsValidationResult result = new QuikFormsValidationResult();
        result.isValid = true;

        // Get email value
        String email = (String)context.fieldValue;

        // Skip validation if empty (handled by required field validation)
        if (String.isBlank(email)) {
            return result;
        }

        // Extract domain from email
        String domain = email.substringAfter('@').toLowerCase();

        // Check blocked domains first
        if (!this.blockedDomains.isEmpty() && this.blockedDomains.contains(domain)) {
            result.isValid = false;
            result.errorMessages = new List<String>{
                'Email addresses from ' + domain + ' are not allowed'
            };
            return result;
        }

        // Check allowed domains
        if (!this.allowedDomains.isEmpty() && !this.allowedDomains.contains(domain)) {
            result.isValid = false;
            result.errorMessages = new List<String>{
                'Only email addresses from approved domains are permitted'
            };
            result.warningMessages = new List<String>{
                'Allowed domains: ' + String.join(this.allowedDomains, ', ')
            };
        }

        return result;
    }

    public QuikFormsValidationResult validateAsync(QuikFormsValidationContext context) {
        // Not used for this validator (synchronous only)
        return null;
    }
}

Configuration

Configure the validator using JSON in the Custom Metadata record:

{
  "allowedDomains": ["company.com", "partner.com", "example.com"],
  "blockedDomains": ["tempmail.com", "throwaway.email"]
}

Submission Handler Example

Complete example of a submission handler plugin:

public class LeadEnrichmentHandler implements IQuikFormsSubmissionHandler {

    private String enrichmentApiEndpoint;
    private String apiKey;

    // IQuikFormsPlugin methods
    public QuikFormsPluginInfo getPluginInfo() {
        return new QuikFormsPluginInfo(
            'Lead Enrichment Handler',
            '1.0',
            'Enriches lead data with additional information from external API'
        );
    }

    public void initialize(Map<String, Object> configuration) {
        this.enrichmentApiEndpoint = (String)configuration.get('apiEndpoint');
        this.apiKey = (String)configuration.get('apiKey');
    }

    public Boolean isReady() {
        return String.isNotBlank(this.enrichmentApiEndpoint)
            && String.isNotBlank(this.apiKey);
    }

    // IQuikFormsSubmissionHandler methods
    public QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context) {
        QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
        result.success = true;
        result.shouldContinue = true;

        try {
            // Get company name from form data
            String companyName = (String)context.fieldValues.get('company_field');

            if (String.isNotBlank(companyName)) {
                // Call enrichment API
                Map<String, Object> enrichmentData = callEnrichmentAPI(companyName);

                // Add enriched data to field values
                if (enrichmentData != null) {
                    result.modifiedFieldValues = new Map<String, Object>();
                    result.modifiedFieldValues.put('industry_field',
                        enrichmentData.get('industry'));
                    result.modifiedFieldValues.put('employee_count_field',
                        enrichmentData.get('employeeCount'));
                    result.modifiedFieldValues.put('annual_revenue_field',
                        enrichmentData.get('annualRevenue'));
                }
            }
        } catch (Exception e) {
            // Log error but don't fail submission
            QuikExceptionLogger.log(e, 'Lead Enrichment', context.formConfigId);
        }

        return result;
    }

    public QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context) {
        QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
        result.success = true;
        result.shouldContinue = true;

        try {
            // Send notification to sales team
            sendSalesNotification(context.recordId);

            // Create follow-up task
            createFollowUpTask(context.recordId);
        } catch (Exception e) {
            QuikExceptionLogger.log(e, 'Post-Submission Handler', context.formConfigId);
        }

        return result;
    }

    // Helper methods
    private Map<String, Object> callEnrichmentAPI(String companyName) {
        // Make HTTP callout to enrichment API
        HttpRequest req = new HttpRequest();
        req.setEndpoint(this.enrichmentApiEndpoint + '?company=' +
            EncodingUtil.urlEncode(companyName, 'UTF-8'));
        req.setMethod('GET');
        req.setHeader('Authorization', 'Bearer ' + this.apiKey);
        req.setTimeout(10000);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
        }

        return null;
    }

    private void sendSalesNotification(Id recordId) {
        // Send email or platform event
    }

    private void createFollowUpTask(Id recordId) {
        // Create Task record
    }
}

Best Practices

Error Handling

  • Always use try-catch blocks
  • Log errors using QuikExceptionLogger
  • Return graceful error messages to users
  • Don't fail submission for non-critical errors

Performance

  • Keep validation logic lightweight
  • Cache expensive computations
  • Use async validation for callouts
  • Implement timeouts for external calls
  • Avoid SOQL queries in loops

Configuration

  • Make plugins configurable via Custom Metadata
  • Validate configuration in initialize()
  • Provide sensible defaults
  • Document configuration options in JSON schema

Security

  • Validate all input data
  • Sanitize user-provided values
  • Use with sharing when appropriate
  • Respect field-level security
  • Don't log sensitive data

Testing Plugins

Unit Tests

Create comprehensive unit tests for your plugins:

@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'}
        };
        validator.initialize(config);

        // Create context
        QuikFormsValidationContext context = new QuikFormsValidationContext();
        context.fieldId = 'email_field';
        context.fieldValue = '[email protected]';

        // Test
        Test.startTest();
        QuikFormsValidationResult result = validator.validateSync(context);
        Test.stopTest();

        // Verify
        System.assert(result.isValid, 'Email from allowed domain should be valid');
    }

    @isTest
    static void testBlockedDomain() {
        // Setup
        EmailDomainValidator validator = new EmailDomainValidator();
        Map<String, Object> config = new Map<String, Object>{
            'blockedDomains' => new List<String>{'tempmail.com'}
        };
        validator.initialize(config);

        // Create context
        QuikFormsValidationContext context = new QuikFormsValidationContext();
        context.fieldId = 'email_field';
        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');
    }
}

Integration Tests

Test plugins in the context of form submission:

@isTest
private class LeadEnrichmentHandlerTest {

    @isTest
    static void testEnrichmentIntegration() {
        // Setup mock HTTP callout
        Test.setMock(HttpCalloutMock.class, new EnrichmentAPIMock());

        // Create form configuration
        FormConfigVersion__c formConfig = TestDataFactory.createFormConfig();

        // Create submission context
        QuikFormsSubmissionContext context = new QuikFormsSubmissionContext();
        context.formConfigId = formConfig.Id;
        context.fieldValues = new Map<String, Object>{
            'company_field' => 'Acme Corp'
        };

        // Test
        Test.startTest();
        LeadEnrichmentHandler handler = new LeadEnrichmentHandler();
        handler.initialize(getTestConfig());
        QuikFormsSubmissionResult result = handler.onBeforeSubmit(context);
        Test.stopTest();

        // Verify
        System.assert(result.success, 'Enrichment should succeed');
        System.assert(result.modifiedFieldValues != null, 'Should have enriched data');
        System.assert(result.modifiedFieldValues.containsKey('industry_field'));
    }
}

Plugin Development Complete!

You now have everything you need to create powerful plugins for QuikForms. For more examples and detailed API documentation, see the API Reference.

Additional Resources