diff --git a/bulk-actions-cron-offload.php b/bulk-actions-cron-offload.php
index 630b03a78226bbec8d062b031567f99b542312ae..bea57ae64b065c754f6cbc76b68bbd2c571b6c4f 100644
--- a/bulk-actions-cron-offload.php
+++ b/bulk-actions-cron-offload.php
@@ -21,5 +21,6 @@ require __DIR__ . '/includes/utils.php';
 require __DIR__ . '/includes/class-main.php';
 require __DIR__ . '/includes/class-delete-all.php';
 require __DIR__ . '/includes/class-delete-permanently.php';
+require __DIR__ . '/includes/class-edit.php';
 require __DIR__ . '/includes/class-move-to-trash.php';
 require __DIR__ . '/includes/class-restore-from-trash.php';
diff --git a/includes/class-edit.php b/includes/class-edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..c802041cfe62b94cd5e93a95a787a059fd496e4a
--- /dev/null
+++ b/includes/class-edit.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * Offload "Edit"
+ *
+ * @package Bulk_Actions_Cron_Offload
+ */
+
+namespace Automattic\WP\Bulk_Actions_Cron_Offload;
+
+/**
+ * Class Edit
+ */
+class Edit {
+	/**
+	 * Class constants
+	 */
+	const ACTION = 'edit';
+
+	const ADMIN_NOTICE_KEY = 'bulk_actions_cron_offload_edit';
+
+	/**
+	 * Register this bulk process' hooks
+	 */
+	public static function register_hooks() {
+		add_action( Main::build_hook( self::ACTION ), array( __CLASS__, 'process' ) );
+		add_action( Main::build_cron_hook( self::ACTION ), 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 edit some posts
+	 *
+	 * @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 edit requested items
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function process_via_cron( $vars ) {
+		// Nothing to edit.
+		if ( ! is_array( $vars->posts ) || empty( $vars->posts ) ) {
+			do_action( 'bulk_actions_cron_offload_edit_request_no_posts', $vars->posts, $vars );
+			return;
+		}
+
+		// We want to use `bulk_edit_posts()`.
+		require_once ABSPATH . '/wp-admin/includes/post.php';
+
+		// `bulk_edit_posts()` takes an array, normally `$_REQUEST`, so we convert back.
+		$request_array = get_object_vars( $vars );
+		unset( $request_array['action'] );
+		unset( $request_array['user_id'] );
+
+		// Modify some keys to match `bulk_edit_post()`'s expectations.
+		$request_array['post'] = $request_array['posts'];
+		unset( $request_array['posts'] );
+
+		if ( ! is_null( $request_array['post_sticky'] ) ) {
+			$request_array['sticky'] = $request_array['post_sticky'];
+		}
+		unset( $request_array['post_sticky'] );
+
+		if ( is_null( $request_array['post_status'] ) || 'all' === $request_array['post_status'] ) {
+			$request_array['_status'] = -1;
+		} else {
+			$request_array['_status'] = $request_array['post_status'];
+		}
+		unset( $request_array['post_status'] );
+
+		// `bulk_edit_posts()` calls `current_user_can()`, so we make sure it can.
+		wp_set_current_user( $vars->user_id );
+
+		// Perform bulk edit.
+		$results = bulk_edit_posts( $request_array );
+		$edited  = $results['updated'];
+		$error   = $results['skipped'];
+		$locked  = $results['locked'];
+
+		// `bulk_edit_posts()` mixes these without indicating which it was.
+		$auth_error = $error;
+
+		$results = compact( 'edited', 'locked', 'auth_error', 'error' );
+		do_action( 'bulk_actions_cron_offload_edit_request_completed', $results, $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 edited shortly.', 'bulk-actions-cron-offload' );
+			} else {
+				$type    = 'error';
+				$message = __( 'The requested edits are already pending for the chosen posts.', 'bulk-actions-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';
+			$pending = Main::get_post_ids_for_pending_events( self::ACTION, $screen->post_type, $status );
+
+			if ( ! empty( $pending ) ) {
+				$type    = 'warning';
+				$message = __( 'Some items that would normally be shown here are waiting to be edited. These items are hidden until they are processed.', 'bulk-actions-cron-offload' );
+			}
+		}
+
+		Main::render_admin_notice( $type, $message );
+	}
+
+	/**
+	 * When an edit 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 = Main::get_post_ids_for_pending_events( self::ACTION, $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;
+	}
+}
+
+Edit::register_hooks();
diff --git a/includes/class-main.php b/includes/class-main.php
index 750887f86bcbd1f249d6a77742e34f489337e438..f29b3373282c099ba90b5d6d6d375e8bef49b7fb 100644
--- a/includes/class-main.php
+++ b/includes/class-main.php
@@ -87,7 +87,8 @@ class Main {
 	 * Capture relevant variables
 	 */
 	private static function capture_vars() {
-		$vars = (object) array_fill_keys( array( 'user_id', 'action', 'post_type', 'posts', 'tax_input', 'post_author', 'comment_status', 'ping_status', 'post_status', 'post_sticky', 'post_format' ), null );
+		$vars = array_merge( array( 'action', 'user_id' ), self::get_supported_vars() );
+		$vars = (object) array_fill_keys( $vars, null );
 
 		$vars->user_id = get_current_user_id();
 
@@ -111,6 +112,10 @@ class Main {
 			$vars->tax_input = $_REQUEST['tax_input'];
 		}
 
+		if ( isset( $_REQUEST['post_category'] ) && is_array( $_REQUEST['post_category'] ) ) {
+			$vars->post_category = $_REQUEST['post_category'];
+		}
+
 		if ( isset( $_REQUEST['post_author'] ) && -1 !== (int) $_REQUEST['post_author'] ) {
 			$vars->post_author = (int) $_REQUEST['post_author'];
 		}
@@ -135,14 +140,55 @@ class Main {
 			$vars->post_format = $_REQUEST['post_format'];
 		}
 
+		if ( isset( $_REQUEST['post_parent'] ) && '-1' !== $_REQUEST['post_parent'] ) {
+			$vars->post_parent = (int) $_REQUEST['post_parent'];
+		}
+
+		if ( isset( $_REQUEST['page_template'] ) && '-1' !== $_REQUEST['page_template'] ) {
+			$vars->page_template = $_REQUEST['page_template'];
+		}
+
+		if ( isset( $_REQUEST['post_password'] ) && ! empty( $_REQUEST['post_password'] ) ) {
+			$vars->post_password = $_REQUEST['post_password'];
+		}
+
 		// Post status is special.
 		if ( is_null( $vars->post_status ) && isset( $_REQUEST['post_status'] ) && ! empty( $_REQUEST['post_status'] ) ) {
 			$vars->post_status = $_REQUEST['post_status'];
 		}
 
+		// Another special case, dependent on post status.
+		if ( isset( $_REQUEST['keep_private'] ) && 'private' === $vars->post_status ) {
+			$vars->keep_private = true;
+		}
+
 		return $vars;
 	}
 
+	/**
+	 * List allowed $_REQUEST variables
+	 *
+	 * @return array
+	 */
+	private static function get_supported_vars() {
+		return array(
+			'comment_status',
+			'keep_private',
+			'page_template',
+			'ping_status',
+			'post_author',
+			'post_category',
+			'post_format',
+			'post_parent',
+			'post_password',
+			'post_status',
+			'post_sticky',
+			'post_type',
+			'posts',
+			'tax_input',
+		);
+	}
+
 	/**
 	 * Validate action
 	 *
@@ -153,7 +199,7 @@ class Main {
 		$allowed_actions = array(
 			'delete', // class Delete_Permanently.
 			'delete_all', // class Delete_All.
-			'edit',
+			'edit', // class Edit.
 			'trash', // class Move_To_Trash.
 			'untrash', // class Restore_From_Trash.
 		);
diff --git a/languages/bulk-actions-cron-offload.pot b/languages/bulk-actions-cron-offload.pot
index 860c4d37f7cbbf73e6a11fdd0ac2d78ce3561701..1900db3b0dd004d532d4d1cbe0b477129310fcff 100644
--- a/languages/bulk-actions-cron-offload.pot
+++ b/languages/bulk-actions-cron-offload.pot
@@ -5,7 +5,7 @@ msgstr ""
 "Project-Id-Version: Bulk Actions Cron Offload 1.0\n"
 "Report-Msgid-Bugs-To: "
 "https://wordpress.org/support/plugin/bulk-actions-cron-offload\n"
-"POT-Creation-Date: 2017-09-14 18:55:07+00:00\n"
+"POT-Creation-Date: 2017-09-14 22:18:30+00:00\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -51,6 +51,20 @@ msgid ""
 "permanently. These items are hidden until then."
 msgstr ""
 
+#: includes/class-edit.php:112
+msgid "Success! The selected posts will be edited shortly."
+msgstr ""
+
+#: includes/class-edit.php:115
+msgid "The requested edits are already pending for the chosen posts."
+msgstr ""
+
+#: includes/class-edit.php:127
+msgid ""
+"Some items that would normally be shown here are waiting to be edited. "
+"These items are hidden until they are processed."
+msgstr ""
+
 #: includes/class-move-to-trash.php:111
 msgid "Success! The selected posts will be moved to the trash shortly."
 msgstr ""