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.
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.
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.
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.
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.
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.
add_filter( 'craftor/form/confirmation_response', function ( $response, $post_id, $element_id, $clean ) {
$response['submittedAt'] = current_time( 'mysql' );
return $response;
}, 10, 4 );craftor/email/recipientfilter
($recipient, $post_id, $data) — redirect a notification to a different inbox based on the submission.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).