Skip to content

[6.x] Forms 2: Connections#15063

Draft
duncanmcclean wants to merge 38 commits into
forms-2from
forms-2-connections
Draft

[6.x] Forms 2: Connections#15063
duncanmcclean wants to merge 38 commits into
forms-2from
forms-2-connections

Conversation

@duncanmcclean

@duncanmcclean duncanmcclean commented Jul 23, 2026

Copy link
Copy Markdown
Member

This pull request implements the concept of "Connections" for forms.

Connections let a form talk to the outside world when submissions come in — starting with Emails and Webhooks, and paving the way for third-party integrations.

Forms now have a "Connect" area in the Control Panel, listing the available connections along with how many of each are configured.

Emails

Emails mostly work like they did before, they've just moved to the "Connect" area. We have added a few niceities though:

  • Emails now be triggered based on conditions.
  • The email body can now be written in the Control Panel. You can use @ to insert form fields.
  • The Recipient/CC/BCC/Sender/Reply-to fields now suggest form fields to avoid end-users needing to write Antlers.

TODO: Video of configuring email (waiting on #15055)

Existing email configs in form YAML are automatically converted to email connections, saved under the new connections key. Form::email() has been deprecated in favour of Form::connections().

Webhooks

Upon submission, forms can now send webhooks — a POST request containing the form handle and submission data, sent to a URL of your choice.

SSL verification can be disabled per webhook, useful for local development or when sending requests to internal services.

Like emails, webhooks can be triggered based on conditions.

CleanShot 2026-07-23 at 11 28 50

Registering custom connections

Apps and addons can register their own connections, which will show up alongside the built-in ones in the "Connect" area.

Registering a connection

A connection is a class that extends Statamic\Forms\Connections\Connection. It provides a title, description and icon for the Connect index, an optional count() for the badge on the index table, and returns a Vue component from render():

<?php

namespace App\FormConnections;

use App\Http\Controllers\AcmeConnectionController;
use Statamic\Contracts\Forms\Form;
use Statamic\Forms\Connections\Connection;
use Statamic\Support\VueComponent;

class Acme extends Connection
{
    protected static $title = 'Acme';
    protected $description = 'Send submissions to Acme.';
    protected $icon = 'globe-arrow';

    public function count(Form $form): ?int
    {
        return count($form->connections()->get('acme', []));
    }

    public function render(Form $form): VueComponent
    {
        return VueComponent::render('acme-connection', [
            'action' => cp_route('forms.connect.acme.update', $form->handle()),
            'config' => $form->connections()->get('acme', []),
        ]);
    }

    public function routes($router): void
    {
        $router->patch('/', [AcmeConnectionController::class, 'update'])->name('update');
    }
}

Connections in the FormConnections directory of apps and addons are registered automatically. Addons can also register them explicitly via the $formConnections property in their service provider.

Connections need to register their own route for saving. Any routes registered via the routes() method are automatically wrapped in authorization.

Frontend

The render() method determines which Vue component gets rendered, along with its props.

If your connection supports multiple "rows" (eg. multiple emails per form), you can use the <ConnectionList> component to get a head start.

Simply pass it your array of configs via v-model, a header slot and a body slot for each row, and it takes care of the collapsible row UI, along with the add/duplicate/remove actions.

<script setup>
import { ref } from 'vue';
import { nanoid as uniqid } from 'nanoid';
import { ConnectionList } from '@statamic/cms';
import { Badge } from '@statamic/cms/ui';

const props = defineProps({ config: Array });

const notifications = ref([...props.config]);

const addNotification = () => notifications.value.push({ id: uniqid(), conditions: [] });
const duplicateNotification = (notification) => notifications.value.push({ ...notification, id: uniqid() });
const removeNotification = (notification) => (notifications.value = notifications.value.filter((item) => item !== notification));
</script>

<template>
    <ConnectionList
        v-model="notifications"
        :add-label="__('Add Notification')"
        @add="addNotification"
        @duplicate="duplicateNotification"
        @remove="removeNotification"
    >
        <template #header="{ item: notification, collapsed }">
            <Badge size="lg" pill>{{ notification.channel || __('New Notification') }}</Badge>
        </template>

        <template #default="{ item: notification, index }">
            <!-- Each row's fields go here... -->
        </template>
    </ConnectionList>
</template>

Logic

If you want your connection to support conditional logic, the <ConnectionLogic> component renders the logic builder. Simply bind your conditions with v-model:conditions and put whatever the conditions control inside its then slot.

<template #default="{ item: notification, index }">
    <ConnectionLogic
        v-model:conditions="notification.conditions"
        :always-label="__('Always send')"
        :if-label="__('Send if...')"
    >
        <template #then>
            <!-- The fields controlled by the conditions go here... -->
        </template>
    </ConnectionLogic>
</template>

On the PHP side, the Statamic\Forms\Connections\ConnectionLogic class handles the rest:

  • When saving, ConnectionLogic::normalize($conditions) strips out any incomplete conditions, and returns null when there's nothing to save.
  • When a submission comes in, ConnectionLogic::passes($config, $submission) evaluates the conditions against the submission, so you can decide whether to send anything or not.

Sending notifications

When a submission is finalized, Statamic dispatches a single job chain: file uploads are converted to assets, then each of the connection jobs run and finally temporary file uploads are deleted.

To hook into this process, you should return a job (or array of jobs) from the finalized() method:

public function finalized($submission)
{
	return new SendNotificationToThirdPartyService($submission);
}

Because we're using Laravel's [job chaining](https://laravel.com/docs/13.x/queues#job-chaining) feature, if you need to dispatch additional jobs within one of your jobs, call $this->prependToChain($job) (from Laravel's Queueable trait) so they stay part of the chain.


Depends on:

Closes statamic/ideas#1176
Closes statamic/ideas#1434

duncanmcclean and others added 30 commits July 21, 2026 12:33
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…need to use antlers

Using Antlers in email address fields is still supported, but this is a slightly easier approach for end-users.
@duncanmcclean duncanmcclean mentioned this pull request Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant