hook_webform_submission_insert: A Practical Drupal Tutorial

If you've ever needed to react immediately after a webform submission is saved — trigger an external API, write to a custom table, send a tailored notification — hook_webform_submission_insert is the hook you want. This post covers what it does, when it fires, and three real-world patterns you can drop into your custom module today.

What is hook_webform_submission_insert?

hook_webform_submission_insert is a Drupal hook provided by the Webform module. It fires once, after a new submission entity has been persisted to the database for the first time. At this point the submission has a valid ID, all field values are finalized, and you are free to read or react to them — but not to alter the saved data (for that, use hook_webform_submission_presave).

Key distinction: _insert fires only on creation. For updates use hook_webform_submission_update. For both, use hook_webform_submission_postsave.

Hook Signature

Place this in your custom module's .module file (or a relevant .inc):

<?php

use Drupal\webform\WebformSubmissionInterface;

/**
 * Implements hook_webform_submission_insert().
 *
 * Fires after a new webform submission is saved for the first time.
 *
 * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
 *   The webform submission entity that was just created.
 */
function mymodule_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  // Your logic here.
}

Reading Submission Data

The most common first step: read the submitted values.

<?php

function mymodule_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  // Get the webform ID to act only on a specific form.
  $webform_id = $webform_submission->getWebform()->id();
  if ($webform_id !== 'contact_form') {
    return;
  }

  // Read individual field values.
  $data    = $webform_submission->getData();
  $name    = $data['full_name'] ?? '';
  $email   = $data['email']     ?? '';
  $message = $data['message']   ?? '';

  // Read submission metadata.
  $sid  = $webform_submission->id();
  $uid  = $webform_submission->getOwnerId();
  $time = $webform_submission->getCreatedTime(); // Unix timestamp.
}

Example 1 — Send a Custom Slack Notification

A common pattern: ping a Slack channel whenever a high-priority contact form is submitted, without relying on the Webform module's built-in email handler.

<?php

function mymodule_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  if ($webform_submission->getWebform()->id() !== 'urgent_request') {
    return;
  }

  $data    = $webform_submission->getData();
  $subject = $data['subject'] ?? '(no subject)';
  $from    = $data['email']   ?? 'unknown';

  $payload = json_encode([
    'text' => "New urgent request from {$from}: {$subject}",
  ]);

  /** @var \GuzzleHttp\ClientInterface $http */
  $http = \Drupal::httpClient();

  try {
    $http->post('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', [
      'body'    => $payload,
      'headers' => ['Content-Type' => 'application/json'],
    ]);
  }
  catch (\Exception $e) {
    \Drupal::logger('mymodule')->error('Slack notification failed: @msg', [
      '@msg' => $e->getMessage(),
    ]);
  }
}

Example 2 — Write to a Custom Database Table

Sometimes you need data in your own schema — for reporting, external syncs, or avoiding Webform's own query layer.

<?php

function mymodule_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  if ($webform_submission->getWebform()->id() !== 'newsletter_signup') {
    return;
  }

  $data  = $webform_submission->getData();
  $email = $data['email'] ?? '';

  if (empty($email)) {
    return;
  }

  \Drupal::database()->insert('mymodule_newsletter_queue')
    ->fields([
      'email'      => $email,
      'sid'        => $webform_submission->id(),
      'created'    => \Drupal::time()->getRequestTime(),
      'status'     => 'pending',
    ])
    ->execute();
}

Example 3 — Create a Related Entity on Submission

This is where it gets powerful. Automatically create a node, a custom entity, or a flag the moment someone submits a form.

<?php

use Drupal\node\Entity\Node;

function mymodule_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  if ($webform_submission->getWebform()->id() !== 'project_request') {
    return;
  }

  $data  = $webform_submission->getData();
  $title = $data['project_name'] ?? 'Untitled project';

  $node = Node::create([
    'type'                => 'project',
    'title'               => $title,
    'field_requestor_sid' => $webform_submission->id(),
    'status'              => 0, // Draft until reviewed.
    'uid'                 => $webform_submission->getOwnerId(),
  ]);
  $node->save();
}
Watch out: entity creation inside a hook runs inside the same request. For heavy processing (API calls with retries, file processing), push the work into a Drupal Queue using \Drupal::queue('mymodule_queue')->createItem($data) and process it with a worker. This keeps form submission fast and resilient.

Common Gotchas

  • Draft submissions: hook_webform_submission_insert fires for draft saves too. Check $webform_submission->isDraft() if you only want to act on final submissions.
  • Anonymous submissions: getOwnerId() returns 0 for anonymous users. Guard accordingly.
  • Bulk imports: if submissions are imported via Drush or migrations, this hook fires for each one. If that's a problem, check the source or add a flag field to skip processing.
  • Don't re-save the submission here. Calling $webform_submission->save() inside hook_webform_submission_insert triggers another insert cycle. Use hook_webform_submission_presave if you need to modify data before it hits the DB.
Hook When it fires Can modify data?
hook_webform_submission_presave Before any save (insert or update) Yes
hook_webform_submission_insert After first save only No
hook_webform_submission_update After subsequent saves No
hook_webform_submission_postsave After any save (insert or update) No

Conclusion

hook_webform_submission_insert is the cleanest place to react to a brand-new webform submission: the data is committed, the ID exists, and you're not blocking the save transaction. Keep the hook lean — offload anything slow to a queue — and scope it to the webform IDs you actually care about.

Have a tricky Drupal hook scenario you're wrestling with? I've also written about AI-generated code colliding with legacy Drupal and hooks firing at the wrong time in complex form workflows.


Questions or edge cases I missed? Reach out via LinkedIn or email — always happy to dig into a Drupal puzzle.