diff --git a/bulk-edit-cron-offload.php b/bulk-edit-cron-offload.php
index 3610dc89521a217146aa55d36689a32ef5b56833..ccfb942f93e8729e001dbb3b0ee1e64da7bb147d 100644
--- a/bulk-edit-cron-offload.php
+++ b/bulk-edit-cron-offload.php
@@ -20,3 +20,4 @@ require __DIR__ . '/includes/utils.php';
 // Plugin functionality.
 require __DIR__ . '/includes/class-main.php';
 require __DIR__ . '/includes/class-delete-all.php';
+require __DIR__ . '/includes/class-move-to-trash.php';
diff --git a/includes/class-delete-all.php b/includes/class-delete-all.php
index 3f683f4ff424c7a86cd2ab8fae0fe5483a6dedef..dc540b562f8cf40ff73e5ede0018735b675c34e2 100644
--- a/includes/class-delete-all.php
+++ b/includes/class-delete-all.php
@@ -14,16 +14,14 @@ class Delete_All {
 	/**
 	 * Class constants
 	 */
-	const CRON_EVENT = 'a8c_bulk_edit_delete_all';
-
-	const ADMIN_NOTICE_KEY = 'a8c_bulk_edit_deleted_all';
+	const ADMIN_NOTICE_KEY = 'bulk_edit_cron_offload_deleted_all';
 
 	/**
 	 * Register this bulk process' hooks
 	 */
 	public static function register_hooks() {
 		add_action( Main::build_hook( 'delete_all' ), array( __CLASS__, 'process' ) );
-		add_action( self::CRON_EVENT, array( __CLASS__, 'process_via_cron' ) );
+		add_action( Main::build_cron_hook( 'delete_all' ), array( __CLASS__, 'process_via_cron' ) );
 
 		add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
 		add_filter( 'posts_where', array( __CLASS__, 'hide_posts_pending_delete' ), 999, 2 );
@@ -43,11 +41,10 @@ class Delete_All {
 		// Special keys are used to trigger this request, and we need to remove them on redirect.
 		$extra_keys = array( 'delete_all', 'delete_all2' );
 
-		$action_scheduled = self::action_next_scheduled( self::CRON_EVENT, $vars->post_type );
+		$action_scheduled = self::action_next_scheduled( $vars->post_type );
 
 		if ( empty( $action_scheduled ) ) {
-			wp_schedule_single_event( time(), self::CRON_EVENT, array( $vars ) );
-
+			Main::schedule_processing( $vars );
 			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, true, $extra_keys );
 		} else {
 			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, false, $extra_keys );
@@ -115,31 +112,25 @@ class Delete_All {
 	public static function admin_notices() {
 		$screen = get_current_screen();
 
+		$type    = '';
+		$message = '';
+
 		if ( isset( $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) ) {
 			if ( 1 === (int) $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) {
-				$class = 'notice-success';
-				$message = __( 'Success! The trash will be emptied soon.', 'bulk-edit-cron-offload' );
+				$type    = 'success';
+				$message = __( 'Success! The trash will be emptied shortly.', 'bulk-edit-cron-offload' );
 			} else {
-				$class = 'notice-error';
+				$type    = 'error';
 				$message = __( 'A request to empty the trash is already pending for this post type.', 'bulk-edit-cron-offload' );
 			}
 		} elseif ( 'edit' === $screen->base && isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) {
-			if ( self::action_next_scheduled( self::CRON_EVENT, $screen->post_type ) ) {
-				$class   = 'notice-warning';
+			if ( self::action_next_scheduled( $screen->post_type ) ) {
+				$type    = 'warning';
 				$message = __( 'A pending request to empty the trash will be processed soon.', 'bulk-edit-cron-offload' );
 			}
 		}
 
-		// Nothing to display.
-		if ( ! isset( $class ) || ! isset( $message ) ) {
-			return;
-		}
-
-		?>
-		<div class="notice <?php echo esc_attr( $class ); ?>">
-			<p><?php echo esc_html( $message ); ?></p>
-		</div>
-		<?php
+		Main::render_admin_notice( $type, $message );
 	}
 
 	/**
@@ -162,7 +153,7 @@ class Delete_All {
 			return $where;
 		}
 
-		if ( self::action_next_scheduled( self::CRON_EVENT, $q->get( 'post_type' ) ) ) {
+		if ( self::action_next_scheduled( $q->get( 'post_type' ) ) ) {
 			$where .= ' AND 0=1';
 		}
 
@@ -196,7 +187,7 @@ class Delete_All {
 		}
 
 		// There isn't a pending purge, so one should be permitted.
-		if ( ! self::action_next_scheduled( self::CRON_EVENT, $screen->post_type ) ) {
+		if ( ! self::action_next_scheduled( $screen->post_type ) ) {
 			return $caps;
 		}
 
@@ -209,11 +200,10 @@ class Delete_All {
 	/**
 	 * Find the next scheduled instance of a given action, regardless of arguments
 	 *
-	 * @param  string $action_to_check Hook to search for.
-	 * @param  string $post_type       Post type hook is scheduled for.
+	 * @param  string $post_type Post type hook is scheduled for.
 	 * @return array
 	 */
-	private static function action_next_scheduled( $action_to_check, $post_type ) {
+	private static function action_next_scheduled( $post_type ) {
 		$events = get_option( 'cron' );
 
 		if ( ! is_array( $events ) ) {
@@ -227,14 +217,14 @@ class Delete_All {
 			}
 
 			foreach ( $timestamp_events as $action => $action_instances ) {
-				if ( $action !== $action_to_check ) {
+				if ( Main::CRON_EVENT !== $action ) {
 					continue;
 				}
 
 				foreach ( $action_instances as $instance => $instance_args ) {
 					$vars = array_shift( $instance_args['args'] );
 
-					if ( $post_type === $vars->post_type ) {
+					if ( 'delete_all' === $vars->action && $post_type === $vars->post_type ) {
 						return array(
 							'timestamp' => $timestamp,
 							'args'      => $vars,
diff --git a/includes/class-main.php b/includes/class-main.php
index aa0d618d084a96a8970b5bb3e8efe01b47a96614..1a477327800daa97a31fd669039702d4dcb2d617 100644
--- a/includes/class-main.php
+++ b/includes/class-main.php
@@ -17,12 +17,28 @@ class Main {
 	const ACTION = 'a8c_bulk_edit_cron_';
 
 	/**
-	 * Register action
+	 * Common cron action
+	 */
+	const CRON_EVENT = 'bulk_edit_cron_offload';
+
+	/**
+	 * Register actions
 	 */
 	public static function load() {
+		add_action( self::CRON_EVENT, array( __CLASS__, 'do_cron' ) );
+
 		add_action( 'load-edit.php', array( __CLASS__, 'intercept' ) );
 	}
 
+	/**
+	 * Run appropriate cron callback
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function do_cron( $vars ) {
+		do_action( self::build_cron_hook( $vars->action ), $vars );
+	}
+
 	/**
 	 * Call appropriate handler
 	 */
@@ -77,8 +93,6 @@ class Main {
 
 		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
 			$vars->action = 'delete_all';
-
-			$vars->post_status = $_REQUEST['post_status'];
 		} elseif ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) {
 			$vars->action = $_REQUEST['action'];
 		} elseif ( isset( $_REQUEST['action2'] ) && '-1' !== $_REQUEST['action2'] ) {
@@ -121,6 +135,11 @@ class Main {
 			$vars->post_format = $_REQUEST['post_format'];
 		}
 
+		// Post status is special.
+		if ( is_null( $vars->post_status ) && isset( $_REQUEST['post_status'] ) && ! empty( $_REQUEST['post_status'] ) ) {
+			$vars->post_status = $_REQUEST['post_status'];
+		}
+
 		return $vars;
 	}
 
@@ -152,6 +171,16 @@ class Main {
 		return self::ACTION . $action;
 	}
 
+	/**
+	 * Build a cron hook specific to a bulk request
+	 *
+	 * @param  string $action Bulk action to register cron callback for.
+	 * @return string
+	 */
+	public static function build_cron_hook( $action ) {
+		return self::ACTION . $action . '_callback';
+	}
+
 	/**
 	 * Unset flags Core uses to trigger bulk processing
 	 */
@@ -162,6 +191,26 @@ class Main {
 		unset( $_REQUEST['delete_all2'] );
 	}
 
+	/**
+	 * Create cron event
+	 *
+	 * @param object $vars Bulk-request variables.
+	 * @return bool
+	 */
+	public static function schedule_processing( $vars ) {
+		return false !== wp_schedule_single_event( time(), self::CRON_EVENT, array( $vars ) );
+	}
+
+	/**
+	 * Retrieve timestamp for next scheduled event with given vars
+	 *
+	 * @param object $vars Bulk-request variables.
+	 * @return int
+	 */
+	public static function next_scheduled( $vars ) {
+		return (int) wp_next_scheduled( self::CRON_EVENT, array( $vars ) );
+	}
+
 	/**
 	 * Redirect, including a flag to indicate if the bulk process was scheduled successfully
 	 *
@@ -184,6 +233,26 @@ class Main {
 		wp_safe_redirect( $redirect );
 		exit;
 	}
+
+	/**
+	 * Render an admin message of a given type
+	 *
+	 * @param string $type Message type.
+	 * @param string $message Message to output.
+	 * @return void
+	 */
+	public static function render_admin_notice( $type, $message ) {
+		// Lacking what's required.
+		if ( empty( $type ) || empty( $message ) ) {
+			return;
+		}
+
+		?>
+		<div class="notice <?php echo esc_attr( 'notice-' . $type ); ?>">
+			<p><?php echo esc_html( $message ); ?></p>
+		</div>
+		<?php
+	}
 }
 
 Main::load();
diff --git a/includes/class-move-to-trash.php b/includes/class-move-to-trash.php
new file mode 100644
index 0000000000000000000000000000000000000000..edcd11eed855a9833ba16d1c60c369c08e4a74a9
--- /dev/null
+++ b/includes/class-move-to-trash.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * Offload "Move to Trash"
+ *
+ * @package Bulk_Edit_Cron_Offload
+ */
+
+namespace Automattic\WP\Bulk_Edit_Cron_Offload;
+
+/**
+ * Class Move_To_Trash
+ */
+class Move_To_Trash {
+	/**
+	 * Class constants
+	 */
+	const ADMIN_NOTICE_KEY = 'bulk_edit_cron_offload_move_to_trash';
+
+	/**
+	 * Register this bulk process' hooks
+	 */
+	public static function register_hooks() {
+		add_action( Main::build_hook( 'trash' ), array( __CLASS__, 'process' ) );
+		add_action( Main::build_cron_hook( 'trash' ), array( __CLASS__, 'process_via_cron' ) );
+
+		add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
+		add_filter( 'posts_where', array( __CLASS__, 'hide_posts_pending_move' ), 999, 2 );
+	}
+
+	/**
+	 * Handle a request to move some posts to the trash
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function process( $vars ) {
+		$action_scheduled = Main::next_scheduled( $vars );
+
+		if ( empty( $action_scheduled ) ) {
+			Main::schedule_processing( $vars );
+			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, true );
+		} else {
+			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, false );
+		}
+	}
+
+	/**
+	 * Cron callback to move requested items to trash
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function process_via_cron( $vars ) {
+		$count = 0;
+
+		if ( is_array( $vars->posts ) && ! empty( $vars->posts ) ) {
+			require_once ABSPATH . '/wp-admin/includes/post.php';
+
+			$trashed    = array();
+			$locked     = array();
+			$auth_error = array();
+			$error      = array();
+
+			foreach ( $vars->posts as $post_id ) {
+				// Can the user trash this post?
+				if ( ! user_can( $vars->user_id, 'delete_post', $post_id ) ) {
+					$auth_error[] = $post_id;
+					continue;
+				}
+
+				// Post is locked by someone, so leave it alone.
+				if ( false !== wp_check_post_lock( $post_id ) ) {
+					$locked[] = $post_id;
+					continue;
+				}
+
+				// Try trashing.
+				$post_trashed = wp_trash_post( $post_id );
+				if ( $post_trashed ) {
+					$trashed[] = $post_id;
+				} else {
+					$error[] = $post_id;
+				}
+
+				// Take a break periodically.
+				if ( 0 === $count++ % 50 ) {
+					stop_the_insanity();
+					sleep( 3 );
+				}
+			}
+
+			$results = compact( 'trashed', 'locked', 'auth_error', 'error' );
+			do_action( 'bulk_edit_cron_offload_move_to_trash_request_completed', $results, $vars );
+		} else {
+			do_action( 'bulk_edit_cron_offload_move_to_trash_request_no_posts', $vars->posts, $vars );
+		}
+	}
+
+	/**
+	 * Let the user know what's going on
+	 */
+	public static function admin_notices() {
+		$screen = get_current_screen();
+
+		$type   = '';
+		$message = '';
+
+		if ( isset( $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) ) {
+			if ( 1 === (int) $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) {
+				$type    = 'success';
+				$message = __( 'Success! The selected posts will be moved to the trash shortly.', 'bulk-edit-cron-offload' );
+			} else {
+				$type    = 'error';
+				$message = __( 'The selected posts are already scheduled to be moved to the trash.', 'bulk-edit-cron-offload' );
+			}
+		} elseif ( 'edit' === $screen->base ) {
+			if ( isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) {
+				return;
+			}
+
+			$status = isset( $_REQUEST['post_status'] ) ? $_REQUEST['post_status'] : 'all';
+
+			if ( self::get_all_pending_actions( $screen->post_type, $status ) ) {
+				$type    = 'warning';
+				$message = __( 'Some items that would normally be shown here are waiting to be moved to the trash. These items are hidden until they are moved.', 'bulk-edit-cron-offload' );
+			}
+		}
+
+		Main::render_admin_notice( $type, $message );
+	}
+
+	/**
+	 * When a move is pending for a given post type, hide those posts in the admin
+	 *
+	 * @param string $where Posts' WHERE clause.
+	 * @param object $q WP_Query object.
+	 * @return string
+	 */
+	public static function hide_posts_pending_move( $where, $q ) {
+		if ( ! is_admin() || ! $q->is_main_query() ) {
+			return $where;
+		}
+
+		if ( 'edit' !== get_current_screen()->base ) {
+			return $where;
+		}
+
+		if ( 'trash' === $q->get( 'post_status' ) ) {
+			return $where;
+		}
+
+		$post__not_in = self::get_post_ids_pending_move( $q->get( 'post_type' ), $q->get( 'post_status' ) );
+
+		if ( ! empty( $post__not_in ) ) {
+			$post__not_in = implode( ',', $post__not_in );
+			$where       .= ' AND ID NOT IN(' . $post__not_in . ')';
+		}
+
+		return $where;
+	}
+
+	/**
+	 * Gather all pending events for a given post type
+	 *
+	 * @param string $post_type Post type needing exclusion.
+	 * @param string $post_status Post status to filter by.
+	 * @return array
+	 */
+	private static function get_all_pending_actions( $post_type, $post_status ) {
+		$events = get_option( 'cron' );
+
+		if ( ! is_array( $events ) ) {
+			return array();
+		}
+
+		$ids = array();
+
+		foreach ( $events as $timestamp => $timestamp_events ) {
+			// Skip non-event data that Core includes in the option.
+			if ( ! is_numeric( $timestamp ) ) {
+				continue;
+			}
+
+			foreach ( $timestamp_events as $action => $action_instances ) {
+				if ( Main::CRON_EVENT !== $action ) {
+					continue;
+				}
+
+				foreach ( $action_instances as $instance => $instance_args ) {
+					$vars = array_shift( $instance_args['args'] );
+
+					if ( 'trash' === $vars->action && $post_type === $vars->post_type ) {
+						if ( $post_status === $vars->post_status || 'all' === $vars->post_status || 'all' === $post_status ) {
+							$ids[] = array(
+								'timestamp' => $timestamp,
+								'args'      => $vars,
+							);
+						}
+					}
+				}
+			}
+		}
+
+		return $ids;
+	}
+
+	/**
+	 * Gather IDs of objects pending move to trash, with given post type
+	 *
+	 * @param string $post_type Post type needing exclusion.
+	 * @param string $post_status Post status to filter by.
+	 * @return array
+	 */
+	private static function get_post_ids_pending_move( $post_type, $post_status ) {
+		$events = wp_list_pluck( self::get_all_pending_actions( $post_type, $post_status ), 'args' );
+		$events = wp_list_pluck( $events, 'posts' );
+
+		$ids = array();
+
+		foreach ( $events as $ids_to_merge ) {
+			$ids = array_merge( $ids, $ids_to_merge );
+		}
+
+		if ( ! empty( $ids ) ) {
+			$ids = array_map( 'absint', $ids );
+			$ids = array_unique( $ids );
+		}
+
+		return $ids;
+	}
+}
+
+Move_To_Trash::register_hooks();
diff --git a/languages/bulk-edit-cron-offload.pot b/languages/bulk-edit-cron-offload.pot
index ba8d037a21ee446652f324099a4d8509acb919b8..00f39df29866b2f7d3de38def2161bca21a7ddcc 100644
--- a/languages/bulk-edit-cron-offload.pot
+++ b/languages/bulk-edit-cron-offload.pot
@@ -5,7 +5,7 @@ msgstr ""
 "Project-Id-Version: Bulk Edit Cron Offload 1.0\n"
 "Report-Msgid-Bugs-To: "
 "https://wordpress.org/support/plugin/bulk-edit-cron-offload\n"
-"POT-Creation-Date: 2017-09-13 01:24:54+00:00\n"
+"POT-Creation-Date: 2017-09-13 05:28:23+00:00\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -25,18 +25,32 @@ msgstr ""
 "X-Poedit-Bookmarks: \n"
 "X-Textdomain-Support: yes\n"
 
-#: includes/class-delete-all.php:124
-msgid "Success! The trash will be emptied soon."
+#: includes/class-delete-all.php:121
+msgid "Success! The trash will be emptied shortly."
 msgstr ""
 
-#: includes/class-delete-all.php:127
+#: includes/class-delete-all.php:124
 msgid "A request to empty the trash is already pending for this post type."
 msgstr ""
 
-#: includes/class-delete-all.php:132
+#: includes/class-delete-all.php:129
 msgid "A pending request to empty the trash will be processed soon."
 msgstr ""
 
+#: includes/class-move-to-trash.php:109
+msgid "Success! The selected posts will be moved to the trash shortly."
+msgstr ""
+
+#: includes/class-move-to-trash.php:112
+msgid "The selected posts are already scheduled to be moved to the trash."
+msgstr ""
+
+#: includes/class-move-to-trash.php:123
+msgid ""
+"Some items that would normally be shown here are waiting to be moved to the "
+"trash. These items are hidden until they are moved."
+msgstr ""
+
 #. Plugin Name of the plugin/theme
 msgid "Bulk Edit Cron Offload"
 msgstr ""