GeneratorsCoreActivation Hooks

Activation Hooks Generator

The three lifecycle routines, in the order they really run — with the version gate that makes upgrades possible and the uninstall file that keeps the database clean.

acme.php
Saved just now
The plugin
Version 1.2.0 is stored in acme_version. install() re-runs on plugins_loaded whenever the code is ahead of the stored version.
On activation
PHP and WordPress guard
Deactivates itself with a readable message instead of a fatal error.
Deferred rewrite flush
Flags on activation, flushes on wp_loaded once post types exist.
Schedule cron events
Guarded with wp_next_scheduled so activation cannot double-book.
Version-gated upgrades
Compares the stored version with the constant and re-runs install().
Multisite aware
Loops every site with switch_to_blog on a network activation.
Options and tables to manage
1 option · 1 table · 1 cron
On uninstall
Options, site options and scheduled events go. Custom tables and content are left alone — reversible, and usually the right default.
The main plugin file — register_activation_hook needs __FILE__ to be this file.
<?php
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io).
/**
 * Plugin Name:       Acme plugin
 * Description:       Lifecycle routines: activation, deactivation and upgrades.
 * Version:           1.2.0
 * Requires PHP:      7.4
 * Requires at least: 6.0
 */

defined( 'ABSPATH' ) || exit;

define( 'ACME_VERSION', '1.2.0' );

/**
 * Fires once when the plugin is activated.
 */
function acme_activate() {
	if ( version_compare( PHP_VERSION, '7.4', '<' ) || version_compare( get_bloginfo( 'version' ), '6.0', '<' ) ) {
		deactivate_plugins( plugin_basename( __FILE__ ) );
		wp_die(
			esc_html__( 'This plugin needs PHP 7.4 and WordPress 6.0 or newer.', 'acme' ),
			'',
			array( 'back_link' => true )
		);
	}

	acme_install();
}

/**
 * Seed everything this plugin needs. Safe to run more than once.
 */
function acme_install() {
	$defaults = array(
		'acme_settings' => array(),
	);

	foreach ( $defaults as $option => $value ) {
		add_option( $option, $value );
	}

	global $wpdb;

	$table   = $wpdb->prefix . 'acme_events';
	$charset = $wpdb->get_charset_collate();

	require_once ABSPATH . 'wp-admin/includes/upgrade.php';

	dbDelta(
		"CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			created datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
			post_id bigint(20) unsigned NOT NULL DEFAULT 0,
			payload longtext NOT NULL,
			PRIMARY KEY  (id),
			KEY post_id (post_id)
		) {$charset}"
	);

	// Rewrite rules cannot be built here — the post type is not registered yet.
	// Flag it and flush on the next load, once init has run.
	update_option( 'acme_flush_rules', 1 );

	if ( ! wp_next_scheduled( 'acme_sync_products' ) ) {
		wp_schedule_event( strtotime( 'tomorrow 03:00' ), 'daily', 'acme_sync_products' );
	}

	update_option( 'acme_version', ACME_VERSION );
}

/**
 * Fires when the plugin is deactivated — including on every update.
 */
function acme_deactivate() {
	wp_clear_scheduled_hook( 'acme_sync_products' );

	flush_rewrite_rules();

	// Nothing is deleted here. Deactivation happens on every update.
}

/**
 * Run install again when the stored version is behind the code.
 */
function acme_maybe_upgrade() {
	$stored = get_option( 'acme_version', '0' );

	if ( version_compare( $stored, ACME_VERSION, '>=' ) ) {
		return;
	}

	acme_install();
}
add_action( 'plugins_loaded', 'acme_maybe_upgrade' );

/**
 * Flush rewrite rules once, after everything is registered.
 */
function acme_maybe_flush_rules() {
	if ( ! get_option( 'acme_flush_rules' ) ) {
		return;
	}

	delete_option( 'acme_flush_rules' );
	flush_rewrite_rules();
}
add_action( 'wp_loaded', 'acme_maybe_flush_rules' );

register_activation_hook( __FILE__, 'acme_activate' );
register_deactivation_hook( __FILE__, 'acme_deactivate' );

// Deletion is handled by uninstall.php — see the other output mode.

About this generator

Activation Hooks Generator Online

Every WordPress activation hook you need, in the order they really run. register_activation_hook() seeds options, creates tables through dbDelta() and schedules cron; register_deactivation_hook() clears schedules without touching data; and a separate uninstall.php does the deleting, because that is the only file WordPress runs when a plugin is actually deleted.

It also handles the ordering problem nobody warns you about: rewrite rules cannot be flushed during activation, because your post types are not registered yet. The generator flags a flush during activation and performs it on the next wp_loaded, which is the only sequence that produces working permalinks. Free, no account, generated in your browser.

Reviewed Output tested on WordPress 6.8 · PHP 8.3

Why generated lifecycle routines beat a hand-rolled activation function

Activation runs exactly once, in a request where almost nothing is loaded, and the mistakes it causes surface weeks later: permalinks that 404, a cron event that outlives the plugin, a database column that never arrives on existing installs because install() is never called again. Each of those is a structural problem, so the generator writes the structure rather than a single function.

Three routines that do their own job

Activation checks requirements and calls install(); install() is written to be safe to run more than once; deactivation clears scheduled events and flushes rules but deletes nothing, because deactivation also happens on every update.

A requirement gate that fails politely

Optional PHP and WordPress version checks use version_compare() against PHP_VERSION and get_bloginfo( 'version' ), then call deactivate_plugins( plugin_basename( __FILE__ ) ) and wp_die() with a back link — a message instead of a fatal error on an old host.

Rewrite rules flushed at the only moment that works

Activation sets a flag option; a wp_loaded callback sees the flag, deletes it and calls flush_rewrite_rules() once. Flushing during activation itself would run before your post type exists, which is why so many plugins 404 until someone opens Settings → Permalinks.

An upgrade routine keyed to a version option

The plugin version is written to an option, and a plugins_loaded check re-runs install() whenever the constant is ahead of the stored value. Without it, new options and new dbDelta() columns never reach sites that already had the plugin — a warning the generator raises if you turn it off.

A custom table built with dbDelta()

Naming a table produces the $wpdb->prefix lookup, get_charset_collate(), the require_once ABSPATH . 'wp-admin/includes/upgrade.php' line and a CREATE TABLE in the exact shape dbDelta() insists on — one field per line, two spaces after PRIMARY KEY, no backticks.

A real uninstall.php, at three levels

Its own output mode, guarded by defined( 'WP_UNINSTALL_PLUGIN' ) || exit. Leave everything, remove options and schedules, or take the custom table and the plugin's own posts too — with warnings on the irreversible choices, plus role migration so nobody is left with no role at all.

How does the Activation Hooks Generator work?

Four steps that produce two files: the lifecycle block for your main plugin file, and uninstall.php beside it.

  1. 01

    Identify the plugin

    Set the function prefix, the current version and the minimum PHP and WordPress versions the requirement gate should enforce.

  2. 02

    List what it creates

    Name the options to seed, the custom table to build with dbDelta(), the cron hooks to schedule and any custom roles. Everything listed here is also what uninstall knows to remove.

  3. 03

    Choose the behaviours

    Toggle requirement checks, the deferred rewrite flush, cron scheduling, the version-gated upgrade routine and multisite-aware network activation.

  4. 04

    Set the uninstall level and export

    Pick how much deletion is appropriate, then export the plugin file and switch the output mode to save uninstall.php alongside it.

Worked example — activation that defers the rewrite flush

The part of the output that solves the ordering problem: activation only records that a flush is needed, and wp_loaded performs it once, after every post type has been registered on init.

/**
 * Seed everything this plugin needs. Safe to run more than once.
 */
function acme_install() {
	// Rewrite rules cannot be built here — the post type is not registered yet.
	// Flag it and flush on the next load, once init has run.
	update_option( 'acme_flush_rules', 1 );

	update_option( 'acme_version', ACME_VERSION );
}

/**
 * Flush rewrite rules once, after everything is registered.
 */
function acme_maybe_flush_rules() {
	if ( ! get_option( 'acme_flush_rules' ) ) {
		return;
	}

	delete_option( 'acme_flush_rules' );
	flush_rewrite_rules();
}
add_action( 'wp_loaded', 'acme_maybe_flush_rules' );

register_activation_hook( __FILE__, 'acme_activate' );
register_deactivation_hook( __FILE__, 'acme_deactivate' );

Both register_*_hook() calls must sit in the main plugin file, because __FILE__ is how WordPress identifies which plugin they belong to. Move them into an include and the path no longer matches the plugin file, so the hooks never fire.

Plugin activation & uninstall — frequently asked questions

The lifecycle questions that come up when a plugin has to install something.

Where do I call register_activation_hook()?

In the main plugin file — the one with the plugin header — passing __FILE__ as the first argument. WordPress uses that path to work out which plugin the hook belongs to. Called from an include, __FILE__ resolves to the include instead, the path does not match any known plugin, and the hook silently never fires.

Should I call flush_rewrite_rules() on activation?

Not directly. During activation your post types and taxonomies have not been registered yet, because init has not run in that request, so a flush there rebuilds rules that do not include them. The reliable pattern is to set a flag option during activation and call flush_rewrite_rules() once on a later init or wp_loaded, then delete the flag. It is an expensive call, so it must never run on every page load.

What is the difference between deactivation and uninstall?

Deactivation happens whenever the plugin is switched off, which includes every automatic update and every debugging session. Uninstall happens only when someone deletes the plugin from the Plugins screen. So deactivation should stop things — clear scheduled events, drop transients — and uninstall is the only correct place to delete options, tables or content.

Should I use uninstall.php or register_uninstall_hook()?

uninstall.php in the plugin folder is the recommended option and takes precedence when both exist. It runs in isolation with the plugin's own code not loaded, so it must not call your plugin's functions, and it has to begin with defined( 'WP_UNINSTALL_PLUGIN' ) || exit; to stop anyone requesting it directly.

How do I run code when my plugin is updated?

There is no update hook. Activation does not re-fire on an update, so the standard approach is a version option: store the version during install, then compare it against your version constant on plugins_loaded and re-run the install routine when the code is ahead. That also covers sites updated by pushing files over the top, which never triggers any hook at all.

Does the activation hook run for every site on multisite?

No. A network activation fires the hook once, with $network_wide set to true, not once per site. To seed every site you have to loop get_sites() with switch_to_blog() and restore_current_blog() yourself. On a network with hundreds of sites that loop can exceed the request timeout, so a batched upgrade routine is safer than doing it all during activation.

Where do I paste the code from the Activation Hooks generator?

You have three good options and one bad one. Best is a small site-specific plugin — the generator can output one for you, and it keeps working when you change theme. A code-snippets plugin is fine too. A child theme's functions.php works but ties the code to that theme. Never edit the parent theme: the next update overwrites it and your code is gone.

Can I use the generated code in client work or a commercial plugin?

Yes. The output is ordinary WordPress API calls that you configured — there is no licence attached to it and no attribution required. Use it in client sites, commercial plugins and themes, or products you sell. The generated file carries a short credit comment you are free to delete.

The other Core generators in the library, plus the WordPress tools most often used alongside this one.