Skip to main content

Hooks Reference

Core Forms provides filters and actions throughout the form lifecycle for customization.

Filters

cf_validate_form

Validate form data before processing. Return an error code string to block submission.

add_filter( 'cf_validate_form', function( $error_code, $form, $data ) {
    // Pass through if another validator set an error
    if ( '' !== $error_code ) {
        return $error_code;
    }

    // Custom validation
    if ( empty( $data['agree_terms'] ) ) {
        return 'terms_required';
    }

    return ''; // Empty string = valid
}, 10, 3 );

Parameters: $error_code (string), $form (Form), $data (array)

cf_form_html

Modify the complete form HTML output (outer wrapper included).

add_filter( 'cf_form_html', function( $html, $form ) {
    return '<div class="custom-wrapper">' . $html . '</div>';
}, 10, 2 );

Parameters: $html (string), $form (Form)

cf_form_markup

Modify the inner form markup (the HTML between <form> tags).

add_filter( 'cf_form_markup', function( $markup, $form ) {
    // Append a disclaimer before the submit button
    $disclaimer = '<p class="disclaimer">By submitting, you agree to our terms.</p>';
    $markup = str_replace( '</form>', $disclaimer . '</form>', $markup );
    return $markup;
}, 10, 2 );

Parameters: $markup (string), $form (Form)

cf_schema_markup

Modify the HTML generated from the form schema before it is used as form markup.

add_filter( 'cf_schema_markup', function( $markup, $schema, $form ) {
    return $markup;
}, 10, 3 );

Parameters: $markup (string), $schema (FormSchema), $form (Form)

cf_form_default_settings

Set default values for form settings.

add_filter( 'cf_form_default_settings', function( $settings ) {
    $settings['save_submissions'] = true;
    $settings['redirect_url']     = '';
    $settings['display_mode']     = 'normal';
    return $settings;
} );

cf_form_default_messages

Set default form messages (success, error, validation).

add_filter( 'cf_form_default_messages', function( $messages ) {
    $messages['success'] = 'Thank you! We will be in touch.';
    $messages['error']   = 'Something went wrong. Please try again.';
    return $messages;
} );

cf_form_field_length_limits

Filter server-enforced field character limits. Email fields remain capped at 254 characters after this filter runs.

add_filter( 'cf_form_field_length_limits', function ( $limits, $form ) {
    $limits['message'] = 5000;
    return $limits;
}, 10, 2 );

cf_max_submission_bytes

Lower the maximum encoded submission size. The default and hard ceiling are 60 KiB so stored JSON remains below the submission table's TEXT limit.

add_filter( 'cf_max_submission_bytes', function ( $bytes, $form, $data ) {
    return 32 * 1024;
}, 10, 3 );

cf_process_form_action_{type}

Process a custom action type. This is an action hook (despite the filter-like name) fired via do_action.

add_action( 'cf_process_form_action_my_crm', function( $settings, $submission, $form ) {
    // Send data to your CRM
    wp_remote_post( $settings['api_url'], [
        'body' => wp_json_encode( $submission->data ),
    ] );
}, 10, 3 );

cf_webhook_request_args

Modify the wp_remote_post arguments for webhook requests.

add_filter( 'cf_webhook_request_args', function( $args, $url, $submission ) {
    $args['headers']['Authorization'] = 'Bearer ' . MY_API_KEY;
    $args['timeout'] = 30;
    return $args;
}, 10, 3 );

cf_upload_max_filesize

Change the maximum upload file size (default: 8,000,000 bytes / 8 MB).

add_filter( 'cf_upload_max_filesize', function() {
    return 25 * 1024 * 1024; // 25 MB
} );

cf_create_user_allowed_roles

Restrict which roles the Create User action can assign.

add_filter( 'cf_create_user_allowed_roles', function( $roles ) {
    return [ 'subscriber', 'contributor' ];
} );

cf_template_tags

Add custom template tags for {{tag.field}} syntax.

add_filter( 'cf_template_tags', function( $tags ) {
    $tags['company'] = function( $field ) {
        return get_option( "company_{$field}", '' );
    };
    return $tags;
} );
// Usage: {{company.name}}, {{company.phone}}

cf_duplicate_window_seconds

Since 4.9.0. How long the Prevent Duplicates setting remembers a submitted value-set. Defaults to one hour; return 0 to disable the check.

add_filter( 'cf_duplicate_window_seconds', function( $seconds, $form ) {
    return 'support-request' === $form->slug ? 5 * MINUTE_IN_SECONDS : $seconds;
}, 10, 2 );

cf_fullscreen_is_supported

Since 4.9.0. Fullscreen mode rebuilds the rendered form as a one-question-at-a-time flow and has no awareness of payment fields or multi-step panels, so it refuses those forms rather than breaking them. Use this to refuse others.

add_filter( 'cf_fullscreen_is_supported', function( $supported, $form ) {
    return $supported && empty( $form->settings['my_custom_widget'] );
}, 10, 2 );

cf_template_user_fields / cf_template_post_fields

Since 4.9.0. Template tags such as {{user.user_email}} and {{post.title}} resolve against an allowlist. Anything not on the list returns an empty string.

This is a security boundary, not a convenience: cf_template() also runs over submitted values, so before 4.9.0 a logged-in visitor who typed {{user.user_pass}} into any field had their password hash written into the submission, the CSV export and the notification email. Add fields with care.

add_filter( 'cf_template_user_fields', function( $fields ) {
    $fields[] = 'billing_company';
    return $fields;
} );

cf_available_form_actions

Register custom action types in the Actions dropdown.

add_filter( 'cf_available_form_actions', function( $actions ) {
    $actions['my_action'] = 'My Custom Action';
    return $actions;
} );

Actions

cf_form_success

Fires after a successful, non-spam form submission. This is the primary hook for post-submission processing.

add_action( 'cf_form_success', function( $submission, $form ) {
    // Log, notify, integrate, etc.
    error_log( "Form {$form->ID} submitted by {$submission->data['email']}" );
}, 10, 2 );

Parameters: $submission (Submission), $form (Form)

cf_submission_inserted

Fires immediately after a submission is inserted into the database, before actions run.

add_action( 'cf_submission_inserted', function( $submission, $form ) {
    // $submission->id is now available
}, 10, 2 );

cf_submission_after_write

Since 4.9.0. Fires after any write to wp_cf_submissions, from every path that touches the table — the submit pipeline, admin edits, draft autosave, spam toggles, GDPR erasure, REST, payment status changes, WP-CLI, the importers and the data-management tools.

cf_submission_inserted only covers new rows from the front-end submit. Use this one when you need to stay consistent with the table itself — a cache, a counter, or a secondary index.

add_action( 'cf_submission_after_write', function( $submission_id, $context, $submission, $form ) {
    // $context: submit | admin_edit | draft | delete | status | spam
    //           payment | gdpr | rest | cli | import | data_management | license_commerce
    my_reindex_submission( $submission_id );
}, 10, 4 );

Parameters: $submission_id (int), $context (string), $submission (Submission|null), $form (Form|null)

Deleted rows fire this too — check whether the row still exists before reading it.

cf_submission_blocked

Since 4.9.0. Fires when a submission attempt is refused before it becomes a row. Carries a reason code only — never field values.

add_action( 'cf_submission_blocked', function( $form, $reason ) {
    // $reason: honeypot_missing | honeypot_filled
    // honeypot_missing usually means a page cache is stripping hidden
    // fields from real visitors' forms, not that spam was stopped.
}, 10, 2 );

cf_submission_insert_failed

Since 4.9.0. Fires when the database write for a submission fails. Before 4.9.0 the failure was swallowed and the visitor still saw a success message.

add_action( 'cf_submission_insert_failed', function( $submission, $form, $error ) {
    error_log( 'Core Forms lost a submission: ' . $error );
}, 10, 3 );

cf_action_skipped

Since 4.9.0. Fires when a configured action is deliberately not run.

add_action( 'cf_action_skipped', function( $form, $action_settings, $submission, $reason ) {
    // $reason: conditions_not_met
}, 10, 4 );

cf_form_response

Fires when the form response is being prepared, including for spam.

add_action( 'cf_form_response', function( $form, $submission, $error_code ) {
    if ( $error_code === 'spam' ) {
        // Track spam attempt
    }
}, 10, 3 );

cf_process_form

Fires during form processing, before validation.

add_action( 'cf_process_form', function( $form, $submission ) {
    // Modify submission data before processing
}, 10, 2 );

cf_fullscreen_head

Output content in the <head> of fullscreen form pages.

cf_fullscreen_footer

Output content before </body> of fullscreen form pages.

4.1 additions

cf_can_view_form_analytics

Gate per-form analytics REST endpoints on something stricter than the edit_forms capability — for example, post-author ownership.

add_filter( 'cf_can_view_form_analytics', function ( $allowed, $form_id ) {
    $post = get_post( $form_id );
    return $post && (int) $post->post_author === get_current_user_id();
}, 10, 2 );

Parameters: $allowed (bool), $form_id (int), $form (Form|null)

cf_submit_rate_limit

Override the per-IP-per-hour submission cap for a form. Returning 0 disables the limit; the default is 0 (unlimited) unless the form's rate_limit setting is non-zero.

add_filter( 'cf_submit_rate_limit', function ( $limit, $form ) {
    return $form->slug === 'contact' ? 5 : $limit;
}, 10, 2 );

cf_draft_ttl_days

Days before an unfinished draft is purged by the daily cf_cleanup_drafts cron. Default 30. Return 0 to disable cleanup.

cf_analytics_retention_days

Days to keep tracker rows before the daily cf_cleanup_analytics cron deletes them. Default 365. Return 0 to disable retention.

cf_action_queue_stale_minutes

Minutes a row may sit in the workflow queue's processing status before it's considered stuck and requeued by ActionQueue::recover_stale(). Default 10.

cf_payments_orchestrator

Returns the singleton Payments orchestrator so you can reach the registered gateways from elsewhere in the codebase. Returns null if called before init.

$payments = apply_filters( 'cf_payments_orchestrator', null );
$stripe   = $payments?->get_gateway( 'stripe' );

cf_payment_completed

Fires once a webhook confirms a payment for a submission. Use it for behaviour that must only happen on paid submissions (free submissions fire cf_form_success directly, without going through this hook).

add_action( 'cf_payment_completed', function ( $submission, $form ) {
    // ... your code here ...
}, 10, 2 );

Related