Integrations

Building an Automation Integration from Scratch

Formie ships automation integrations out of the box — Web Request, Zapier, Make, and others — but you can register your own when you need a custom endpoint, payload shape, or authentication header. This walkthrough builds an automation integration from a Craft module: editors configure a URL per form, test the payload from the builder, and Formie POSTs submission data when someone submits.

We also cover extending the built-in Web Request integration when you only need to tweak the payload.

Read the Automation Integration reference first if you have not built an integration before — this guide walks through a complete example and explains how the pieces connect.

Create Your Module

First, you need a Craft module (opens new window). All of the PHP in this guide lives in that module.

When creating your module, set the namespace to modules\formieintegration and the module ID to formie-integration. Create the following directory and file structure:

my-project/
├── modules/
│   └── formieintegration/
│       └── src/
│           ├── integrations/
│           │   └── ExampleAutomation.php
│           └── FormieIntegration.php
└── ...

Unlike CRM or email marketing integrations, automation integrations usually store their connection details per form (URL, HTTP method, request type) rather than in global plugin settings — so you typically do not need a _plugin-settings.html template.

Register the module in your project config, then wire it up to Formie in FormieIntegration.php:

modules/formieintegration/src/FormieIntegration.php
<?php
namespace modules\formieintegration;

use Craft;
use modules\formieintegration\integrations\ExampleAutomation;
use verbb\formie\events\RegisterIntegrationsEvent;
use verbb\formie\services\Integrations;
use yii\base\Event;
use yii\base\Module;

class FormieIntegration extends Module
{
    public function init(): void
    {
        parent::init();

        Craft::setAlias('@formie-integration', __DIR__);

        Event::on(Integrations::class, Integrations::EVENT_REGISTER_INTEGRATIONS, function(RegisterIntegrationsEvent $event) {
            $event->automations[] = ExampleAutomation::class;
        });
    }
}

The module file stays deliberately small. You tell Formie which integration class to load by pushing it onto $event->automations[]. Automation integrations register under Automations, not CRM or Email Marketing.

The bulk of the work lives in ExampleAutomation.php.

The Integration Class

Create modules/formieintegration/src/integrations/ExampleAutomation.php. The class extends Formie's Automation base class and implements three moments editors care about:

  1. Configure — URL, HTTP method, and request type on the form's Integrations tab
  2. Test — send a sample payload while editing the form
  3. Submit — POST real submission data when the form is submitted

Here is the full class. We walk through each method in the sections below.

modules/formieintegration/src/integrations/ExampleAutomation.php
<?php
namespace modules\formieintegration\integrations;

use Craft;
use craft\helpers\App;
use GuzzleHttp\Client;
use Throwable;
use verbb\formie\base\Automation;
use verbb\formie\base\FormInterface;
use verbb\formie\base\Integration;
use verbb\formie\elements\Submission;
use verbb\formie\helpers\SchemaHelper;

class ExampleAutomation extends Automation
{
    public static function displayName(): string
    {
        return Craft::t('formie', 'Example Automation Integration');
    }

    #[\verbb\formie\attributes\FormIntegrationSetting]
    #[\verbb\formie\attributes\Sensitive]
    public ?string $url = null;

    #[\verbb\formie\attributes\FormIntegrationSetting]
    public string $method = 'POST';

    #[\verbb\formie\attributes\FormIntegrationSetting]
    public string $requestType = 'json';

    public static function supportsConnection(): bool
    {
        return false;
    }

    public function getDescription(): string
    {
        return Craft::t('formie', 'Send your form content to any URL you provide.');
    }

    protected function defineRules(): array
    {
        $rules = parent::defineRules();
        $rules[] = [['url'], 'required', 'on' => [Integration::SCENARIO_FORM]];

        return $rules;
    }



    protected function executePayload(Submission $submission): \verbb\formie\models\IntegrationResult
    {
        $this->beginPayloadDelivery($submission);
        try {
            $payload = $this->generatePayloadValues($submission);

            $response = $this->deliverPayload(
                $submission,
                $this->getEndpointUrl($this->url, $submission),
                $payload,
                $this->method,
                $this->requestType,
            );

            return $this->resultForPayload($response !== false);
        } catch (Throwable $e) {
            Integration::apiError($this, $e);

            return $this->resultForPayload(false);
        }
    }

    protected function defineClient(): Client
    {
        return Craft::createGuzzleClient();
    }

    protected function defineFormSettingsSchema(FormInterface $form): array
    {
        $schema = parent::defineFormSettingsSchema($form);

        $schema[] = SchemaHelper::textField([
            'label' => Craft::t('formie', 'URL'),
            'instructions' => Craft::t('formie', 'Enter the URL that will be triggered when a submission is made.'),
            'name' => 'url',
            'required' => true,
        ]);

        $schema[] = SchemaHelper::selectField([
            'label' => Craft::t('formie', 'HTTP Method'),
            'instructions' => Craft::t('formie', 'Select the HTTP method used to send data.'),
            'name' => 'method',
            'required' => true,
            'defaultValue' => $this->method ?: 'POST',
            'options' => [
                ['label' => 'GET', 'value' => 'GET'],
                ['label' => 'POST', 'value' => 'POST'],
                ['label' => 'PUT', 'value' => 'PUT'],
                ['label' => 'PATCH', 'value' => 'PATCH'],
                ['label' => 'DELETE', 'value' => 'DELETE'],
            ],
        ]);

        $schema[] = SchemaHelper::selectField([
            'label' => Craft::t('formie', 'Request Type'),
            'instructions' => Craft::t('formie', 'Select the request type used to send data.'),
            'name' => 'requestType',
            'required' => true,
            'defaultValue' => $this->requestType ?: 'json',
            'options' => [
                ['label' => Craft::t('formie', 'JSON Body'), 'value' => 'json'],
                ['label' => Craft::t('formie', 'Raw Body'), 'value' => 'body'],
                ['label' => Craft::t('formie', 'Query String'), 'value' => 'query'],
                ['label' => Craft::t('formie', 'Form Params'), 'value' => 'form_params'],
                ['label' => Craft::t('formie', 'Multipart'), 'value' => 'multipart'],
            ],
        ]);

        $schema[] = SchemaHelper::integrationSendTestPayloadButtonField();

        return $schema;
    }
}

This follows the same pattern as Formie's built-in Web Request integration — a useful reference while you adapt URLs and payload handling to your endpoint.

Every endpoint has different requirements — treat method names and payload shapes as patterns to adapt.

Per-Form Settings

Automation integrations store connection details as public properties on the class ($url, $method, $requestType). Editors configure them per form on the Integrations tab — not in global plugin settings.

Define the form builder UI in defineFormSettingsSchema(). Start with parent::defineFormSettingsSchema($form) — the parent schema includes Enabled, Conditions, and Opt-in field.

Mark the URL as required when saving form settings using Integration::SCENARIO_FORM in defineRules().

URLs can use Craft environment variables. The Automation base class resolves them through getEndpointUrl() — pass $MY_AUTOMATION_URL in the control panel and read the resolved value at send time with App::parseEnv() if you build custom URL logic.

Per-form integration UI is declared as schema nodes — see Everything you need to know about Formie schemas for helper details.

Guzzle Client

Implement defineClient() when you need custom headers, authentication, or timeouts. The example uses a plain Guzzle client; add default headers for bearer tokens or API keys if your endpoint requires them.

Prefer $this->deliverPayload() over calling the client directly — it fires payload events, respects opt-in settings, and integrates with Formie's error logging.

Test Payload from the Builder

The explicit Send Test Payload action calls sendTestPayload() with a generated, unsaved submission. The inherited implementation invokes executePayload(), so this example needs only one payload implementation for test and real submissions. The control panel reports the returned IntegrationResult; it does not cache the test response as metadata.

Formie checks form-integration permissions and CSRF protection before creating the sample. Sample requests use the same hardened HTTP transport, but an unsaved sample is not a production delivery and cannot be automatically retried. fetchConfig() remains available for actual builder metadata and must not send payloads.

Sending the Payload on Submit

executePayload() runs when a real submission completes and this integration is dispatched.

  1. Build the payload with generatePayloadValues($submission) — Formie's standard shape with field handles, labels, and values — or override that method for a custom structure.
  2. Call deliverPayload() with the form's URL, method, and request type.

Log failures with Integration::error() for detail in logs, and Integration::apiError() so the submission's integration run shows failed in the control panel.

Return false on failure so Formie records the integration run correctly.

Extending an Existing Integration

When Formie's built-in Web Request integration is close to what you need, extend it and override payload generation:

modules/formieintegration/src/integrations/WebRequestCustom.php
<?php
namespace modules\formieintegration\integrations;

use verbb\formie\elements\Submission;
use verbb\formie\integrations\automations\WebRequest;

class WebRequestCustom extends WebRequest
{
    protected function generatePayloadValues(Submission $submission): array
    {
        $payload = parent::generatePayloadValues($submission);

        // Adjust payload shape for your endpoint.
        return $payload;
    }
}

Register WebRequestCustom::class on $event->automations[] instead of ExampleAutomation::class.

Finishing Up

With the module in place:

  1. Go to Formie → Integrations → Automations and create a new Example Automation Integration instance.
  2. Edit a form, open the Integrations tab, attach the integration, enter the URL, and send a test payload.
  3. Submit the form on the front end and confirm your endpoint receives the request.

Integrations run as part of the submission workflow. For execution order, queued and synchronous execution, and conditions, see Integration dispatch and policies.