The Drupal Webform module is extraordinarily powerful. The built-in email handler covers 80% of use cases — but when you need conditional logic, file attachments, multi-recipient routing, or integration with external systems, you need a custom handler. Here are the patterns I keep reaching for.
The handler base class
All custom handlers extend WebformHandlerBase and override the relevant hook methods. The most useful are postSave (after submission is saved), preSave (for validation and data manipulation), and confirmForm (for post-submission redirects).
/**
* @WebformHandler(
* id = "grant_application",
* label = @Translation("Grant Application Handler"),
* category = @Translation("Custom"),
* description = @Translation("Handles grant application submissions."),
* cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_SINGLE,
* results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
* submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_REQUIRED,
* )
*/
class GrantApplicationHandler extends WebformHandlerBase {
public function postSave(
WebformSubmissionInterface $submission,
bool $update = TRUE
): void {
if ($update) return; // Only fire on initial submission
$this->processNewApplication($submission);
}
}Conditional email routing
The Fish Fund application routes emails to different reviewers depending on the applicant's country. Rather than hardcoding this logic, we store the routing table in Drupal config and load it dynamically in the handler.
Use webform tokens for dynamic values. The webform token system gives you access to any submission value in email subjects and bodies. Combine this with conditional handler configuration and you can handle complex routing without any PHP for the simple cases.
File attachment handling
Webform managed file fields save files to Drupal's file system with a temporary status. In your handler's postSave, call $file->setPermanent() and $file->save() on every attached file — otherwise they get deleted by Drupal's file cleanup cron.
private function makePermanent(WebformSubmissionInterface $submission): void {
$data = $submission->getData();
foreach (['supporting_doc', 'budget_spreadsheet'] as $field) {
if (!empty($data[$field])) {
$file = File::load($data[$field]);
if ($file) {
$file->setPermanent();
$file->save();
}
}
}
}