diff --git a/includes/functions.php b/includes/functions.php
index 051838291004dab67b33a20bb8051d7a17f3fc67..02c97785edda8df180ed4e290b915aad8968c5a6 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -9,7 +9,7 @@ namespace Automattic\WP\WP_CLI_Cron_Control_Offload;
  * @return bool
  */
 function is_subcommand_allowed( $subcommand ) {
-	return in_array( $subcommand, get_subcommand_whitelist(), true ) && ! in_array( $subcommand, get_subcommand_blacklist(), true );
+	return in_array( $subcommand, get_command_whitelist(), true ) && ! in_array( $subcommand, get_command_blacklist(), true );
 }
 
 /**
@@ -17,13 +17,12 @@ function is_subcommand_allowed( $subcommand ) {
  *
  * @return array
  */
-function get_subcommand_whitelist() {
+function get_command_whitelist() {
 	// Supported built-in commands
 	$whitelist = array(
 		'cache',
 		'cap',
 		'comment',
-		'import',
 		'media',
 		'menu',
 		'network',
@@ -52,7 +51,7 @@ function get_subcommand_whitelist() {
  *
  * @return array
  */
-function get_subcommand_blacklist() {
+function get_command_blacklist() {
 	return array(
 		'cli',
 		'config',
@@ -60,9 +59,50 @@ function get_subcommand_blacklist() {
 		'cron',
 		'db',
 		'eval',
+		'export',
 		'eval-file',
+		'import',
 		'package',
 		'scaffold',
 		'server',
 	);
 }
+
+/**
+ * Create cron event for a given WP-CLI command
+ *
+ * return bool|\WP_Error
+ */
+function schedule_cli_command( $args ) {
+	$event_args = validate_args( $args );
+
+	if ( is_wp_error( $event_args ) ) {
+		return $event_args;
+	}
+
+	$scheduled = wp_schedule_single_event( strtotime( '+30 seconds' ), ACTION, $event_args );
+
+	return false !== $scheduled;
+}
+
+/**
+ * Validate WP-CLI command to be scheduled
+ *
+ * @param array $args
+ * @return array|\WP_Error
+ */
+function validate_args( $args ) {
+	$validated_args = array();
+
+	// TODO: validate
+	// TODO: strip leading "wp"
+	// TODO: check first positional argument against `is_command_allowed()`
+
+	$validated_args['command'] = $args;
+
+	if ( empty( $validated_args ) ) {
+		return new \WP_Error( 'Arguments could not be parsed for validation.' );
+	}
+
+	return $validated_args;
+}
diff --git a/includes/run.php b/includes/run.php
index db46dc7223105717ca14e2e073ad53b5a5cff431..95d6ca4ad3d1c19b67fffa2d2574edb1c4226e52 100644
--- a/includes/run.php
+++ b/includes/run.php
@@ -3,14 +3,19 @@
 namespace Automattic\WP\WP_CLI_Cron_Control_Offload;
 
 /**
+ * Intended for non-interactive use, so all output ends up in the error log
  *
+ * @param array $args
+ * @return null
  */
 function run_event( $args ) {
 	if ( ! defined( 'WP_CLI' ) || ! \WP_CLI ) {
-		trigger_error( 'Attempted to run event without WP-CLI loaded. ' . compact( $args ), E_USER_WARNING );
+		// TODO: reschedule at least once or twice
+		trigger_error( 'Attempted to run event without WP-CLI loaded. ' . var_export( $args, true ), E_USER_WARNING );
 		return false;
 	}
 
 	// TODO: run event, sending output to error log
+	trigger_error( var_export( $args, true ), E_USER_NOTICE );
 }
 add_action( ACTION, __NAMESPACE__ . '\run_event' );
diff --git a/includes/schedule.php b/includes/schedule.php
index 8e2291daab61838b5ec3204041f77b59fc77a103..de831296096e005641f01c5a0d2d0563ad7d9a06 100644
--- a/includes/schedule.php
+++ b/includes/schedule.php
@@ -2,3 +2,8 @@
 
 namespace Automattic\WP\WP_CLI_Cron_Control_Offload;
 
+if ( ! defined( 'WP_CLI' ) || ! \WP_CLI ) {
+	return false;
+}
+
+// TODO: WP-CLI command to schedule an event