diff --git includes/common.inc includes/common.inc
index 71137e7..fae030e 100644
--- includes/common.inc
+++ includes/common.inc
@@ -3069,7 +3069,7 @@ function base_path() {
  * which on normal pages is up through the preprocess step of theme('html').
  * Adding a link will overwrite a prior link with the exact same 'rel' and
  * 'href' attributes.
- * 
+ *
  * @param $attributes
  *   Associative array of element attributes including 'href' and 'rel'.
  * @param $header
@@ -3475,7 +3475,7 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
     $contents = preg_replace('{
       (?<=\\\\\*/)([^/\*]+/\*)([^\*/]+\*/)  # Add a backslash also at the end ie-mac hack comment, so the next pass will not touch it.
                                             # The added backshlash does not affect the effectiveness of the hack.
-      }x', '\1\\\\\2', $contents);    
+      }x', '\1\\\\\2', $contents);
     $contents = preg_replace('<
       \s*([@{}:;,]|\)\s|\s\()\s* |          # Remove whitespace around separators, but keep space around parentheses.
       /\*[^*\\\\]*\*+([^/*][^*]*\*+)*/ |    # Remove comments that are not CSS hacks.
@@ -4476,6 +4476,7 @@ function _drupal_bootstrap_full() {
   require_once DRUPAL_ROOT . '/includes/unicode.inc';
   require_once DRUPAL_ROOT . '/includes/image.inc';
   require_once DRUPAL_ROOT . '/includes/form.inc';
+  require_once DRUPAL_ROOT . '/includes/entity.inc';
   require_once DRUPAL_ROOT . '/includes/mail.inc';
   require_once DRUPAL_ROOT . '/includes/actions.inc';
   require_once DRUPAL_ROOT . '/includes/ajax.inc';
@@ -6330,11 +6331,15 @@ function entity_extract_ids($entity_type, $entity) {
  * @return
  *   An entity structure, initialized with the ids provided.
  */
-function entity_create_stub_entity($entity_type, $ids) {
-  $entity = new stdClass();
+function entity_create_stub_entity($entity_type, $ids = array()) {
   $info = entity_get_info($entity_type);
-  $entity->{$info['object keys']['id']} = $ids[0];
-  if (isset($info['object keys']['revision']) && !is_null($ids[1])) {
+  //TODO: property needs documentation
+  $class = isset($info['entity class']) ? $info['entity class'] : 'stdClass';
+  $entity = new $class();
+  if (isset($ids[0])) {
+    $entity->{$info['object keys']['id']} = $ids[0];
+  }
+  if (isset($info['object keys']['revision']) && isset($ids[1])) {
     $entity->{$info['object keys']['revision']} = $ids[1];
   }
   if ($info['object keys']['bundle']) {
diff --git includes/entity.inc includes/entity.inc
index 059cd1d..3ceb7f2 100644
--- includes/entity.inc
+++ includes/entity.inc
@@ -298,3 +298,195 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     $this->entityCache += $entities;
   }
 }
+
+/**
+ * Wrapper for drupal_build_form() to use for fieldable entity forms.
+ *
+ * This is similar to drupal_get_form(), but additionally
+ * - stores the entity type in $form_state['storage']['entity']; e.g. 'node' or
+ *   'user'.
+ * - stores the initial entity object in $form_state['storage']['object'] upon
+ *   first build of the form (but not on subsequent builds).
+ * - defines entity_form_wrapper() as 'wrapper_callback' to be invoked before
+ *   the form constructor is invoked, which is the heart of the special
+ *   fieldable entity form workflow.
+ *
+ * @see entity_form_wrapper()
+ *
+ * @param $obj_type
+ *   The type of $object; e.g. 'node' or 'user'.
+ * @param $form_id
+ *   The unique string identifying the desired form. See drupal_get_form() and
+ *   drupal_build_form() for details.
+ * @param $object
+ *   The object for which to build a form.
+ * @param ...
+ *   Any additional arguments to pass to form constructor functions.
+ *
+ * @see drupal_build_form()
+ */
+function entity_get_form($obj_type, $form_id, $object = NULL) {
+  // Handle additional arguments to pass to form constructor, i.e.
+  // remove $obj_type and $form_id, but pass on everything else.
+  $args = func_get_args();
+  array_shift($args);
+  array_shift($args);
+  $form_state['build_info']['args'] = $args;
+
+  // Prepare initial form storage.
+  if (empty($form_state['storage'])) {
+    // Store the name of the entity we are processing.
+    $form_state['storage']['entity'] = $obj_type;
+
+    // Prepare the initial object.
+    if (!isset($object)) {
+      // @todo After creating a stub entity, we need to update $args, which is
+      //   currently not possible, because drupal_retrieve_form() uses private
+      //   $args instead of $form_state['build_info']['args'].
+      $object = entity_create_stub_entity($obj_type);
+    }
+    $form_state['storage']['object'] = &$object;
+  }
+
+  // Invoke the common wrapper callback common for all fieldable entities.
+  $form_state['wrapper_callback'] = 'entity_form_wrapper';
+
+  return drupal_build_form($form_id, $form_state);
+}
+
+/**
+ * Form wrapper callback for fieldable entity forms.
+ *
+ * This function is invoked before the form constructor function (usually
+ * $form_id) to set up $form and $form_state for an fieldable entity form
+ * workflow:
+ * - form caching is enabled to cache all data in $form_state['storage']. It is
+ *   important that form caching must not be disabled by any function in the
+ *   entire form workflow. Otherwise, previously submitted form values will get
+ *   lost.
+ * - field widgets are attached to the form.
+ * - entity_form_validate() is added as form validation handler to validate
+ *   the submitted form values for fields and invoke an entity-specific
+ *   ENTITY_validate() function, if existent.
+ * - entity_form_submit() is added as form submit handler to update the object
+ *   in the form storage with the submitted form values, invoke an
+ *   entity-specific ENTITY_submit() function (if existent), and process field
+ *   values for attached fields.
+ *
+ * @see entity_get_form()
+ * @see entity_form_validate()
+ * @see entity_form_submit()
+ */
+function entity_form_wrapper($form, &$form_state) {
+  // Extract information that was initially setup by entity_get_form() or was
+  // reloaded from cache.
+  $obj_type = $form_state['storage']['entity'];
+  $object = $form_state['storage']['object'];
+
+  // Attach fields.
+  field_attach_form($obj_type, $object, $form, $form_state, (isset($object->language) ? $object->language : NULL));
+
+  // Add form validation handler for attached fields.
+  $form['#validate'][] = 'entity_form_validate';
+  // Add default form-level validation handler, i.e. CALLBACK_validate().
+  // Form buttons can override this by assigning custom validation handlers in
+  // their own #validate property.
+  // @todo drupal_prepare_form() actually adds FORMID_validate(), not callback.
+  if (function_exists($form_state['build_info']['callback'] . '_validate')) {
+    $form['#validate'][] = $form_state['build_info']['callback'] . '_validate';
+  }
+
+  // Add form submit handler for attached fields.
+  $form['#submit'][] = 'entity_form_submit';
+  // Add default form-level submit handler, i.e. CALLBACK_submit().
+  // Form buttons can override this by assigning custom submit handlers in
+  // their own #submit property.
+  // @todo drupal_prepare_form() actually adds FORMID_submit(), not callback.
+  if (function_exists($form_state['build_info']['callback'] . '_submit')) {
+    $form['#submit'][] = $form_state['build_info']['callback'] . '_submit';
+  }
+
+  return $form;
+}
+
+/**
+ * Form validation handler for fieldable entity forms.
+ *
+ * @see entity_form_wrapper()
+ * @see entity_form_submit()
+ */
+function entity_form_validate($form, &$form_state) {
+  // Extract information.
+  $obj_type = $form_state['storage']['entity'];
+  $object = $form_state['storage']['object'];
+  
+  // Extract values.
+  foreach ($form_state['values'] as $key => $value) {
+    if (isset($object->$key) && is_array($object->$key) && is_array($value)) {
+      _entity_set_value_rec($object->$key, $value);
+    }
+    else {
+      $object->$key = $value;
+    }
+  }
+
+  // Extraxt field values and validate attached fields.
+  field_attach_form_validate($obj_type, $object, $form, $form_state);
+
+  // Invoke entity validation function, if existent; i.e. ENTITY_validate().
+  $function = $obj_type . '_validate';
+  if (function_exists($function)) {
+    $function($object, $form, $form_state);
+  }
+  // Invoke hook_ENTITY_validate() implementations.
+  $hook = $obj_type . '_validate';
+  foreach (module_implements($hook) as $module) {
+    $function = $module . '_' . $hook;
+    $function($object, $form, $form_state);
+  }
+}
+
+/**
+ * Form submit handler for fieldable entity forms.
+ *
+ * @see entity_form_wrapper()
+ * @see entity_form_validate()
+ */
+function entity_form_submit($form, &$form_state) {
+  // Extract information.
+  $obj_type = $form_state['storage']['entity'];
+  $object = &$form_state['storage']['object'];
+
+  // Invoke entity submit function, if existent; i.e. ENTITY_submit().
+  $function = $obj_type . '_submit';
+  if (function_exists($function)) {
+    $function($object, $form, $form_state);
+  }
+  // Invoke hook_ENTITY_submit() implementations.
+  $hook = $obj_type . '_submit';
+  foreach (module_implements($hook) as $module) {
+    $function = $module . '_' . $hook;
+    $function($object, $form, $form_state);
+  }
+
+  // Prepare submitted form values for attached fields for saving.
+  field_attach_submit($obj_type, $object, $form, $form_state);
+
+  // @todo Form submit handlers should set this on their own. Not even sure why
+  //   one can save a node with this being here (yet another FAPI bug?).
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Recursively updates the given $data array with the given array of values.
+ */
+function _entity_set_value_rec(array &$data, array $values) {
+  foreach ($values as $key => $child) {
+    if (isset($data[$key]) && is_array($data[$key]) && is_array($child)) {
+      _entity_set_value_rec($data[$key], $child);
+    }
+    else {
+      $data[$key] = $child;
+    }
+  }
+}
diff --git includes/form.inc includes/form.inc
index 15fd33a..620c00b 100644
--- includes/form.inc
+++ includes/form.inc
@@ -485,6 +485,10 @@ function drupal_form_submit($form_id, &$form_state) {
 function drupal_retrieve_form($form_id, &$form_state) {
   $forms = &drupal_static(__FUNCTION__);
 
+  // Initialize build information.
+  $form_state['build_info']['form_id'] = $form_id;
+  $form_state['build_info']['callback'] = $form_id;
+
   // We save two copies of the incoming arguments: one for modules to use
   // when mapping form ids to constructor functions, and another to pass to
   // the constructor function itself.
@@ -512,10 +516,11 @@ function drupal_retrieve_form($form_id, &$form_state) {
       $args = array_merge($form_definition['callback arguments'], $args);
     }
     if (isset($form_definition['callback'])) {
-      $callback = $form_definition['callback'];
+      $form_state['build_info']['callback'] = $form_definition['callback'];
     }
     // In case $form_state['wrapper_callback'] is not defined already, we also
     // allow hook_forms() to define one.
+    // @todo Technically, this belongs into build_info as well.
     if (!isset($form_state['wrapper_callback']) && isset($form_definition['wrapper_callback'])) {
       $form_state['wrapper_callback'] = $form_definition['wrapper_callback'];
     }
@@ -542,7 +547,8 @@ function drupal_retrieve_form($form_id, &$form_state) {
 
   // If $callback was returned by a hook_forms() implementation, call it.
   // Otherwise, call the function named after the form id.
-  $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
+  $form = call_user_func_array($form_state['build_info']['callback'], $args);
+  // @todo Remove; already in build_info now.
   $form['#form_id'] = $form_id;
 
   return $form;
diff --git modules/field/field.attach.inc modules/field/field.attach.inc
index 41cbb8b..27bfb90 100644
--- modules/field/field.attach.inc
+++ modules/field/field.attach.inc
@@ -766,7 +766,8 @@ function field_attach_form_validate($obj_type, $object, $form, &$form_state) {
  */
 function field_attach_submit($obj_type, $object, $form, &$form_state) {
   // Extract field values from submitted values.
-  _field_invoke_default('extract_form_values', $obj_type, $object, $form, $form_state);
+  //TODO: this is already done on validation so the object is already updated
+  //_field_invoke_default('extract_form_values', $obj_type, $object, $form, $form_state);
 
   _field_invoke_default('submit', $obj_type, $object, $form, $form_state);
 
diff --git modules/node/node.module modules/node/node.module
index 42d48fb..114eec4 100644
--- modules/node/node.module
+++ modules/node/node.module
@@ -926,8 +926,6 @@ function node_submit($node) {
   }
   $node->created = !empty($node->date) ? strtotime($node->date) : REQUEST_TIME;
   $node->validated = TRUE;
-
-  return $node;
 }
 
 /**
diff --git modules/node/node.pages.inc modules/node/node.pages.inc
index e3b5432..c1ab244 100644
--- modules/node/node.pages.inc
+++ modules/node/node.pages.inc
@@ -13,7 +13,7 @@
 function node_page_edit($node) {
   $type_name = node_type_get_name($node);
   drupal_set_title(t('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $node->title[FIELD_LANGUAGE_NONE][0]['value'])), PASS_THROUGH);
-  return drupal_get_form($node->type . '_node_form', $node);
+  return entity_get_form('node', $node->type . '_node_form', $node);
 }
 
 function node_add_page() {
@@ -65,21 +65,15 @@ function node_add($type) {
     $node = (object)array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $type, 'language' => '');
 
     drupal_set_title(t('Create @name', array('@name' => $types[$type]->name)), PASS_THROUGH);
-    $output = drupal_get_form($type . '_node_form', $node);
+    $output = entity_get_form('node', $node->type . '_node_form', $node);
   }
 
   return $output;
 }
 
-function node_form_validate($form, &$form_state) {
-  $node = (object)$form_state['values'];
-  node_validate($node, $form);
-
-  // Field validation. Requires access to $form_state, so this cannot be
-  // done in node_validate() as it currently exists.
-  field_attach_form_validate('node', $node, $form, $form_state);
-}
-
+/**
+ * @todo Why is this a separate function? Only invoked by node_form().
+ */
 function node_object_prepare($node) {
   // Set up default values, if required.
   $node_options = variable_get('node_options_' . $node->type, array('status', 'promote'));
@@ -110,9 +104,7 @@ function node_object_prepare($node) {
 function node_form($form, &$form_state, $node) {
   global $user;
 
-  if (isset($form_state['node'])) {
-    $node = (object)($form_state['node'] + (array)$node);
-  }
+  // @todo This is 100% #process.
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -151,6 +143,7 @@ function node_form($form, &$form_state, $node) {
     $form = array_merge_recursive($form, $extra);
   }
 
+  // @todo Remove.
   $form['#node'] = $node;
 
   $form['additional_settings'] = array(
@@ -272,14 +265,13 @@ function node_form($form, &$form_state, $node) {
     '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])),
     '#value' => t('Save'),
     '#weight' => 5,
-    '#submit' => array('node_form_submit'),
   );
   $form['buttons']['preview'] = array(
     '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED,
     '#type' => 'submit',
     '#value' => t('Preview'),
     '#weight' => 10,
-    '#submit' => array('node_form_build_preview'),
+    '#submit' => array('entity_form_submit', 'node_form_build_preview'),
   );
   if (!empty($node->nid) && node_access('delete', $node)) {
     $form['buttons']['delete'] = array(
@@ -289,11 +281,9 @@ function node_form($form, &$form_state, $node) {
       '#submit' => array('node_form_delete_submit'),
     );
   }
-  $form['#validate'][] = 'node_form_validate';
   $form['#theme'] = array($node->type . '_node_form', 'node_form');
 
   $form['#builder_function'] = 'node_form_submit_build_node';
-  field_attach_form('node', $node, $form, $form_state, $node->language);
 
   return $form;
 }
@@ -311,10 +301,8 @@ function node_form_delete_submit($form, &$form_state) {
   $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination));
 }
 
-
 function node_form_build_preview($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
-  $form_state['node_preview'] = node_preview($node);
+  $form_state['node_preview'] = node_preview($form_state['storage']['object']);
 }
 
 /**
@@ -413,7 +401,7 @@ function theme_node_preview($variables) {
 }
 
 function node_form_submit($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = $form_state['storage']['object'];
   $insert = empty($node->nid);
   node_save($node);
   $node_link = l(t('view'), 'node/' . $node->nid);
