diff --git a/includes/modal.inc b/includes/modal.inc
index 2eacedd18181fd32807c3d00feb004fc522bc522..311f0ba6d3c09ec3e6b88970a3feb4025eb634a2 100644
--- a/includes/modal.inc
+++ b/includes/modal.inc
@@ -142,20 +142,22 @@ function ctools_modal_form_wrapper($form_id, &$form_state) {
 
   $output = ctools_build_form($form_id, $form_state);
   if (!empty($form_state['ajax']) && empty($form_state['executed'])) {
-    $title = empty($form_state['title']) ? '' : $form_state['title'];
+    return ctools_modal_form_render($form_state, $output);
+  }
 
-    // If there are messages for the form, render them.
-    if ($messages = theme('status_messages')) {
-      $output = '<div class="views-messages">' . $messages . '</div>' . $output;
-    }
+  return $output;
+}
 
-    $output = array(ctools_modal_command_display($title, $output));
-  }
+/**
+ * Render a form into an AJAX display.
+ */
+function ctools_modal_form_render($form_state, $output) {
+  $title = empty($form_state['title']) ? '' : $form_state['title'];
 
-  // These forms have the title built in, so set the title here:
-  if (empty($form_state['ajax']) && !empty($form_state['title'])) {
-    drupal_set_title($form_state['title']);
+  // If there are messages for the form, render them.
+  if ($messages = theme('status_messages')) {
+    $output = '<div class="messages">' . $messages . '</div>' . $output;
   }
 
-  return $output;
+  return array(ctools_modal_command_display($title, $output));
 }
diff --git a/includes/wizard.inc b/includes/wizard.inc
index 939ffd3b0bf375c0901e3d3b4f51d336d8d6ed16..6a1f6eb415a9d3e607e121d1c97c9608972eeb2f 100644
--- a/includes/wizard.inc
+++ b/includes/wizard.inc
@@ -13,9 +13,16 @@
  * The wizard can also be friendly to ajax forms, such as when used
  * with the modal tool.
  *
- * TODO: should the wizard also perform object caching? We'll see
- *       as we develop this if it should happen within the wizard
- *       or outside the wizard.
+ * The wizard provides callbacks throughout the process, allowing the
+ * owner to control the flow. The general flow of what happens is:
+ *
+ * Generate a form
+ * submit a form
+ * based upon button clicked, 'finished', 'next form', 'cancel' or 'return'.
+ *
+ * Each action has its own callback, so cached objects can be modifed and or
+ * turned into real objects. Each callback can make decisions about where to
+ * go next if it wishes to override the default flow.
  */
 
 /**
@@ -72,7 +79,12 @@ function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
   ctools_include('form');
   $output = ctools_build_form($info['form id'], $form_state);
 
-  if (!$output) {
+  if (!empty($form_state['ajax render']) && empty($form_state['executed'])) {
+    // Any include files should already be included by this point:
+    return $form_state['ajax render']($form_state, $output);
+  }
+
+  if (!empty($form_state['executed'])) {
     // We use the plugins get_function format because it's powerful and
     // not limited to just functions.
     ctools_include('plugins');
@@ -85,8 +97,16 @@ function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
       }
     }
 
-    // redirect, if one is set.
-    drupal_redirect_form(array(), $form_state['redirect']);
+    if (empty($form_state['ajax'])) {
+      // redirect, if one is set.
+      return drupal_redirect_form(array(), $form_state['redirect']);
+    }
+    else if (isset($form_state['commands'])) {
+      // If the callbacks wanted to do something besides go to the next form,
+      // it needs to have set $form_state['commands'] with something that can
+      // be rendered.
+      return ctools_ajax_render($form_state['commands']);
+    }
   }
 
   return $output;
@@ -131,7 +151,6 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
   // Display the trail if instructed to do so.
   if (!empty($form_info['show trail'])) {
     ctools_add_css('wizard');
-//    drupal_add_css(drupal_get_path('module', 'delegator') . '/css/task-handlers.css');
     $form['ctools_trail'] = array(
       '#value' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), $trail),
       '#weight' => -1000,
@@ -220,11 +239,20 @@ function ctools_wizard_submit(&$form, &$form_state) {
   if (isset($form_state['clicked_button']['#wizard type'])) {
     $type = $form_state['clicked_button']['#wizard type'];
     if ($type == 'return' || $type == 'finish') {
-      $form_state['redirect'] = $form_state['form_info']['return path'];
-      // Do we need to do something here or just let it go?
+      if (empty($form_state['ajax']) && isset($form_state['form_info']['return path'])) {
+        // Do we need to do something here or just let it go?
+        $form_state['redirect'] = $form_state['form_info']['return path'];
+      }
+      // Calling functions using AJAX need to to provide commands
+      // for the 'finished' and 'cancel' operations.
     }
     else {
-      $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
+      if (!empty($form_state['ajax'])) {
+        $form_state['ajax next'] = $form_state['clicked_button']['#next'];
+      }
+      else {
+        $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
+      }
     }
   }
 }