You are viewing beta documentation for Formie 4.x.
Submissions & Workflows

Adding a Custom Workflow Task from Scratch

This guide adds a project-specific validation rule after Formie's field validation and before spam checks or CAPTCHA. A task gives the rule an explicit place in the workflow and a typed way to stop processing.

Start with an installed Formie form containing a required Single-Line Text field with the handle orderReference. You also need a bootstrapped Craft module. The example uses the namespace modules\formieworkflow, mapped to modules/formieworkflow/src/ in your project's Composer autoload configuration. If you don't have a module yet, follow Craft's module setup guide (opens new window), including registering and bootstrapping the module, before continuing.

Create the Task

Create modules/formieworkflow/src/tasks/CheckOrderTask.php:

<?php
namespace modules\formieworkflow\tasks;

use verbb\formie\enums\SubmissionOutcomeType;
use verbb\formie\workflow\WorkflowContext;
use verbb\formie\workflow\tasks\TaskInterface;
use verbb\formie\workflow\tasks\TaskResult;

class CheckOrderTask implements TaskInterface
{
    public function execute(WorkflowContext $context): TaskResult
    {
        $submission = $context->command->submission;
        $reference = (string)$submission->getFieldValue('orderReference');

        if ($reference !== '' && !str_starts_with($reference, 'ORD-')) {
            $submission->addError('field:orderReference', 'Enter a reference beginning with ORD-.');
            return TaskResult::stop($context->result(SubmissionOutcomeType::VALIDATION_FAILED));
        }

        return TaskResult::continue();
    }
}

The task only executes work. Its name, stage placement and applicable operations belong to registration. TaskResult::continue() lets the next task run. A validation-failed outcome stops processing, preserves errors and leaves the request token available for corrected input. Place rejection checks before Persist: stopping later does not undo already committed work.

Register the Task

Add these imports to your module class:

use modules\formieworkflow\tasks\CheckOrderTask;
use verbb\formie\enums\SubmissionOperation;
use verbb\formie\enums\workflow\Stage;
use verbb\formie\enums\workflow\Task;
use verbb\formie\events\RegisterStageTasksEvent;
use verbb\formie\services\SubmissionWorkflow;
use verbb\formie\workflow\TaskDefinition;
use yii\base\Event;

Inside the module's init() method, after parent::init(), register the listener. Replace orders with your form's handle:

Event::on(SubmissionWorkflow::class, SubmissionWorkflow::EVENT_REGISTER_STAGE_TASKS, function(RegisterStageTasksEvent $event) {
    if ($event->stage !== Stage::VALIDATE) {
        return;
    }

    $event->insertTaskAfter(Task::VALIDATE_SUBMISSION, new TaskDefinition(
        'acme.validateOrder',
        new CheckOrderTask(),
        [SubmissionOperation::SUBMIT],
    ));
});

The registration runs for each workflow plan. To restrict this task to the orders form, add this at the start of CheckOrderTask::execute():

if ($context->command->form->handle !== 'orders') {
    return TaskResult::continue();
}

acme.validateOrder must be unique within the stage. The explicit Submit operation keeps the rule out of SaveDraft, Revise and PaymentReplay. Add Revise to the operation list if saved-record edits must follow the same rule.

Choose a Position

insertTaskBefore() and insertTaskAfter() take a public anchor and a TaskDefinition. prepend() and append() place a task at a stage boundary. An unknown or internal anchor throws an exception, so use the public anchor reference when choosing a position.

To check valid input before spam screening, the example's position after validate.submission is appropriate. To enqueue work for a completed submission before integrations, register a task in Dispatch before Task::DISPATCH_TRIGGER_INTEGRATIONS. The submission has been saved at that point. Queued work still needs its own delivery and retry policy; an after-task event does not prove that a remote request has completed.

Test the Result

Submit the form with an empty required field. Formie's validation should reject it before any screening runs. Next, enter INVALID-123; your rule should show its field error without saving or using CAPTCHA. Correct the value to ORD-123 and submit again. The normal workflow should continue.

If your form supports save-and-continue, save a draft containing INVALID-123. The draft should save because the registration declares only Submit. Test another form too: the handle check should leave it unaffected. If the task is missing, check that the module is bootstrapped, its Composer namespace resolves, and the stage comparison uses Stage::VALIDATE.