Docs / Developers

Hooks & filters reference

Every action and filter Craftor fires for third-party code, with an example for each.

All of these are plain WordPress actions/filters — use add_action() / add_filter() exactly as you would for any WordPress or plugin hook. None of them require Craftor's classes to be loaded to register the callback, only to reference Craftor's own classes (like ActionRegistry) inside one.

Registration

craftor/register_widgetsaction

Fires once, after Craftor's built-in widgets register. Passes the WidgetRegistry instance.

php
add_action( 'craftor/register_widgets', function ( $registry ) {
	$registry->register( new Acme_Callout_Widget() );
} );

craftor/register_workflow_actionsaction

Fires once, after Craftor's built-in automation actions register.

php
add_action( 'craftor/register_workflow_actions', function () {
	\Craftor\Workflows\Actions\ActionRegistry::register( new Acme_Log_Action() );
} );

craftor/register_dynamic_tagsaction

Fires once, after Craftor's built-in dynamic tags register. Passes the DynamicTagsRegistry instance.

php
add_action( 'craftor/register_dynamic_tags', function ( $registry ) {
	// $registry->register( new Acme_Custom_Tag() );
} );

Form submission

craftor/form/before_submitaction

($post_id, $element_id, $clean, $element) — fires after validation/sanitization (and payment verification, if any), before saving or notifying.

php
add_action( 'craftor/form/before_submit', function ( $post_id, $element_id, $clean, $element ) {
	error_log( "Form {$element_id} on post {$post_id} passed validation." );
}, 10, 4 );

craftor/form/after_submitaction

($post_id, $element_id, $clean, $labelled, $payment, $element) — fires after saving, legacy notifications, and workflows have all run.

php
add_action( 'craftor/form/after_submit', function ( $post_id, $element_id, $clean, $labelled, $payment, $element ) {
	if ( null !== $payment ) {
		error_log( "Payment of {$payment['amount']} {$payment['currency']} confirmed." );
	}
}, 10, 6 );

craftor/form/confirmation_responsefilter

($response, $post_id, $element_id, $clean) — the final {message} or {redirect} JSON body sent to the browser.

php
add_filter( 'craftor/form/confirmation_response', function ( $response, $post_id, $element_id, $clean ) {
	$response['submittedAt'] = current_time( 'mysql' );
	return $response;
}, 10, 4 );

Email

craftor/email/recipientfilter

($recipient, $post_id, $data) — redirect a notification to a different inbox based on the submission.

php
add_filter( 'craftor/email/recipient', function ( $recipient, $post_id, $data ) {
	return ! empty( $data['department'] ) && 'sales' === $data['department']
		? 'sales@example.com'
		: $recipient;
}, 10, 3 );

craftor/email/subjectfilter

($subject, $post_id, $data).

php
add_filter( 'craftor/email/subject', function ( $subject, $post_id, $data ) {
	return '[' . get_bloginfo( 'name' ) . '] ' . $subject;
}, 10, 3 );

craftor/email/headersfilter

($headers, $post_id, $data) — an array of wp_mail() header strings; append your own, e.g. a Bcc.

php
add_filter( 'craftor/email/headers', function ( $headers, $post_id, $data ) {
	$headers[] = 'Bcc: archive@example.com';
	return $headers;
}, 10, 3 );

craftor/email/before_sendaction

($recipient, $subject, $body, $headers, $post_id, $data) — fires immediately before wp_mail() is called, after the three filters above have already run.

php
add_action( 'craftor/email/before_send', function ( $recipient, $subject, $body, $headers, $post_id, $data ) {
	error_log( "Sending \"{$subject}\" to {$recipient}" );
}, 10, 6 );

Workflows

craftor/workflow/before_runaction

($workflow, $context) — fires once a matched, active workflow's own trigger-level conditions have passed, right before its steps start running.

php
add_action( 'craftor/workflow/before_run', function ( $workflow, $context ) {
	error_log( "Workflow #{$workflow['id']} ({$workflow['title']}) starting." );
}, 10, 2 );

craftor/workflow/after_actionaction

($step_type, $config, $context, $outcome) — fires after every individual step, whether it succeeded, failed, or was skipped by its own condition.

php
add_action( 'craftor/workflow/after_action', function ( $step_type, $config, $context, $outcome ) {
	if ( 'error' === $outcome['status'] ) {
		error_log( "Step {$step_type} failed: {$outcome['reason']}" );
	}
}, 10, 4 );

craftor/workflow/after_runaction

($workflow, $context, $results, $status) — fires once, after every step has run (or the workflow stopped early on an unrecoverable failure) and its log row is saved.

php
add_action( 'craftor/workflow/after_run', function ( $workflow, $context, $results, $status ) {
	if ( 'error' === $status ) {
		wp_mail( get_option( 'admin_email' ), 'A workflow failed', "Workflow #{$workflow['id']} finished with errors." );
	}
}, 10, 4 );

Form entries

Fired from Craftor → Entries — editing a saved entry, or deleting one (both the single "Delete" row action and bulk delete fire the same two hooks).

craftor/entry/before_updateaction

($id, $updated, $existing, $row) — fires right before an edited entry is saved, from the "Edit Entry" screen.

php
add_action( 'craftor/entry/before_update', function ( $id, $updated, $existing, $row ) {
	error_log( "Entry #{$id} is about to be updated." );
}, 10, 4 );

craftor/entry/after_updateaction

($id, $updated, $existing, $row) — fires right after an edited entry has been saved, before the entry_updated workflow trigger runs.

php
add_action( 'craftor/entry/after_update', function ( $id, $updated, $existing, $row ) {
	if ( ( $existing['status'] ?? '' ) !== ( $updated['status'] ?? '' ) ) {
		error_log( "Entry #{$id} status changed to {$updated['status']}." );
	}
}, 10, 4 );

craftor/entry/before_deleteaction

($id, $row) — fires right before an entry is deleted — from the single "Delete" row action or a bulk delete.

php
add_action( 'craftor/entry/before_delete', function ( $id, $row ) {
	error_log( "Entry #{$id} (post {$row['post_id']}) is about to be deleted." );
}, 10, 2 );

craftor/entry/after_deleteaction

($id, $row) — fires right after an entry has been deleted from the database.

php
add_action( 'craftor/entry/after_delete', function ( $id, $row ) {
	error_log( "Entry #{$id} deleted." );
}, 10, 2 );

$row is always just the raw submissions-table row — it doesn't include who made the change. For that, Entry Updated and Entry Deleted also fire as Workflow triggers with an extra 'actor' key on the trigger context (id, name, email, ip, time) — see "Workflows & automations" in the Forms docs for the {actor_name} / {actor_email} / {actor_ip} / {action_time} merge tags built from it. Craftor\Support\ClientIp::resolve() is the same IP-lookup helper used to build it, if you need the current request's IP in your own code (it prefers X-Forwarded-For's first hop, falling back to REMOTE_ADDR).