diff --git a/commerce_license_billing.install b/commerce_license_billing.install
index cd66ce1..b40a350 100644
--- a/commerce_license_billing.install
+++ b/commerce_license_billing.install
@@ -102,7 +102,7 @@ function commerce_license_billing_schema() {
     'primary key' => array('billing_cycle_type_id'),
   );
   $schema['cl_billing_usage'] = array(
-    'description' => 'The base table for tracking license usage.',
+    'description' => 'Tracks license usage.',
     'fields' => array(
       'usage_id' => array(
         'description' => 'The primary key.',
@@ -150,6 +150,44 @@ function commerce_license_billing_schema() {
     ),
     'primary key' => array('usage_id'),
   );
+  $schema['cl_scheduled_change'] = array(
+    'description' => 'Tracks license changes scheduled for the end of the billing cycle.',
+    'fields' => array(
+      'change_id' => array(
+        'description' => 'The primary key.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'license_id' => array(
+        'description' => 'The id of the license.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'property' => array(
+        'description' => 'The property that will be changed, usually product_id or status.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The new value of the property.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the change was scheduled.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('change_id'),
+  );
 
   return $schema;
 }
@@ -175,3 +213,11 @@ function commerce_license_billing_uninstall() {
   field_attach_delete_bundle('commerce_line_item', 'recurring');
   field_attach_delete_bundle('commerce_order', 'recurring');
 }
+
+/**
+ * Creates the 'cl_scheduled_change' table.
+ */
+function commerce_license_billing_update_7100() {
+  $schema = commerce_license_billing_schema();
+  db_create_table('cl_scheduled_change', $schema['cl_scheduled_change']);
+}
diff --git a/commerce_license_billing.module b/commerce_license_billing.module
index c96275d..ed721a0 100644
--- a/commerce_license_billing.module
+++ b/commerce_license_billing.module
@@ -289,21 +289,35 @@ function commerce_license_billing_cycle_expiration_queue_process($item) {
     // Close the order.
     $order->status = 'recurring_payment_pending';
     commerce_order_save($order);
+    // Clear all closed usage, it's now irrelevant.
+    commerce_license_billing_usage_clear(array_keys($licenses));
 
-    // Open a new billing cycle just after the end of the previous one.
-    $billing_cycle_type = entity_load_single('cl_billing_cycle_type', $billing_cycle->type);
-    $next_billing_cycle = $billing_cycle_type->getNextBillingCycle($billing_cycle);
-    // Renew the licenses.
-    $license_ids = array();
     foreach ($licenses as $license) {
-      $license->renew($next_billing_cycle->end);
-      $license_ids[] = $license->license_id;
+      // Apply scheduled changes.
+      $changes = commerce_license_billing_schedule_changes_list($license);
+      if ($changes) {
+        commerce_license_billing_apply_changes($license, $changes);
+      }
+
+      // Filter out non-active (revoked / expired) licenses, only the active
+      // ones can be charged in the next billing cycle.
+      if ($license->status != COMMERCE_LICENSE_ACTIVE) {
+        unset($licenses[$license->license_id]);
+        continue;
+      }
     }
-    // Clear all closed usage, it's now irrelevant.
-    commerce_license_billing_usage_clear($license_ids);
 
-    // Generate a new recurring order.
-    commerce_license_billing_generate_recurring_order($order, $next_billing_cycle, $licenses);
+    if ($licenses) {
+      // Open a new billing cycle for the active licenses, and renew each one.
+      $billing_cycle_type = entity_load_single('cl_billing_cycle_type', $billing_cycle->type);
+      $next_billing_cycle = $billing_cycle_type->getNextBillingCycle($billing_cycle);
+      foreach ($licenses as $license) {
+        $license->renew($next_billing_cycle->end);
+      }
+
+      // Generate a new recurring order.
+      commerce_license_billing_generate_recurring_order($order, $next_billing_cycle, $licenses);
+    }
 
     return array(
       'status' => ADVANCEDQUEUE_STATUS_SUCCESS,
@@ -443,16 +457,8 @@ function commerce_license_billing_generate_recurring_order($previous_order, $bil
   $recurring_order_wrapper->commerce_customer_billing = $previous_order_wrapper->commerce_customer_billing->value();
   $recurring_order_wrapper->cl_billing_cycle = $billing_cycle;
   // Generate a base line item for each license.
-  // If this is the first recurring order, licenses of all statuses are taken,
-  // since they haven't been activated yet. However, if the previous order was
-  // also recurring, only active licenses are taken since the revoked / expired
-  // ones have already been charged for.
-  $active_only = ($previous_order->type == 'recurring');
   $line_items = array();
   foreach ($licenses as $license) {
-    if ($active_only && $license->status != COMMERCE_LICENSE_ACTIVE) {
-      continue;
-    }
     $line_items[] = commerce_license_billing_generate_base_line_item($license, $billing_cycle, $recurring_order->order_id);
   }
   $recurring_order_wrapper->commerce_line_items = $line_items;
@@ -945,10 +951,6 @@ function commerce_license_billing_commerce_license_update($license) {
 /**
  * Changes the license plan.
  *
- * @todo
- * Allow the plan change to be queued until the end of the cycle, and do it
- * by default for prepaid cycles.
- *
  * @param $license
  *   The license entity.
  * @param $product_id
@@ -956,7 +958,7 @@ function commerce_license_billing_commerce_license_update($license) {
  */
 function commerce_license_billing_change_plan($license, $product_id) {
   if ($license->wrapper->product->cl_billing_type->value() == 'prepaid') {
-    // We don't support changing plans for prepaid products for now.
+    commerce_license_billing_schedule_change($license, 'product_id', $product_id);
     return;
   }
 
@@ -1108,6 +1110,77 @@ function commerce_license_billing_usage_clear($license_ids, $group = NULL) {
 }
 
 /**
+ * Schedules a change of a license property for the end of the billing cycle.
+ *
+ * Usually used to schedule plan changes or license cancellations.
+ *
+ * @param $license
+ *   The license entity.
+ * @param $property
+ *   The property that will be changed, usually product_id or status.
+ * @param $value
+ *   The new value of the property.
+ */
+function commerce_license_billing_schedule_change($license, $property, $value) {
+  db_insert('cl_scheduled_change')
+    ->fields(array(
+      'license_id' => $license->license_id,
+      'property' => $property,
+      'value' => $value,
+      'created' => REQUEST_TIME,
+    ))
+    ->execute();
+}
+
+/**
+ * Returns the license changes scheduled for the end of the billing cycle.
+ *
+ * @param $license
+ *   The license entity.
+ *
+ * @return
+ *   An array of change records with the following keys:
+ *   - property: The property that will be changed.
+ *   - value: The new value of the property.
+ *   - created: The change request creation timestamp.
+ */
+function commerce_license_billing_schedule_changes_list($license) {
+  $scheduled_changes = &drupal_static(__FUNCTION__, array());
+  if (!isset($scheduled_changes[$license->license_id])) {
+    $scheduled_changes[$license->license_id] = array();
+    $data = array(
+      ':license_id' => $license->license_id,
+    );
+    $result = db_query("SELECT * FROM {cl_scheduled_change}
+                          WHERE license_id = :license_id
+                            ORDER BY created ASC", $data);
+    $scheduled_changes[$license->license_id] = $result->fetchAll(PDO::FETCH_ASSOC);
+  }
+
+  return $scheduled_changes[$license->license_id];
+}
+
+/**
+ * Applies the provided changes to the provided license.
+ *
+ * @param $license
+ *   The license entity.
+ * @param $changes
+ *   An array of change records.
+ */
+function commerce_license_billing_apply_changes($license, $changes) {
+  // Apply each change to the license.
+  foreach ($changes as $change) {
+    $license->wrapper->{$change['property']} = $change['value'];
+  }
+  // Save the license and trigger a synchronization if needed.
+  $license->save();
+  if ($license instanceof CommerceLicenseSynchronizableInterface) {
+    commerce_license_enqueue_sync($license);
+  }
+}
+
+/**
  * Implements hook_field_access().
  *
  * Prevents the cl_billing_cycle field from showing up on the order edit form.
