diff --git a/actions/modify.action.inc b/actions/modify.action.inc
new file mode 100644
index 0000000..edd6c95
--- /dev/null
+++ b/actions/modify.action.inc
@@ -0,0 +1,537 @@
+<?php
+
+/**
+ * @file VBO action to modify entity values (properties and fields).
+ */
+
+// Specifies that all available values should be shown to the user for editing.
+define('VBO_MODIFY_ACTION_ALL', '_all_');
+
+function views_bulk_operations_modify_action_info() {
+  return array('views_bulk_operations_modify_action' => array(
+    'type' => 'entity',
+    'label' => t('Modify entity values'),
+    'configurable' => TRUE,
+    'behavior' => array('changes_property'),
+  ));
+}
+
+/**
+ * Action function.
+ *
+ * Goes through new values and uses them to modify the passed entity by either
+ * replacing the existing values, or appending to them (based on user input).
+ */
+function views_bulk_operations_modify_action($entity, $context) {
+  list(,,$bundle_name) = entity_extract_ids($context['entity_type'], $entity);
+
+  // Handle Field API fields.
+  if (!empty($context['selected']['bundle_' . $bundle_name])) {
+    $pseudo_entity = $context['entities'][$bundle_name];
+    foreach ($context['selected']['bundle_' . $bundle_name] as $key) {
+      // Replace any tokens that might exist in the field columns.
+      foreach ($pseudo_entity->{$key}[LANGUAGE_NONE] as $delta => &$item) {
+        foreach ($item as $column => &$value) {
+          if (is_string($value)) {
+            $value = token_replace($value, array($context['entity_type'] => $entity), array('sanitize' => FALSE));
+          }
+        }
+      }
+
+      if (in_array($key, $context['append']['bundle_' . $bundle_name])) {
+        $entity->{$key}[LANGUAGE_NONE] = array_merge($entity->{$key}[LANGUAGE_NONE], $pseudo_entity->{$key}[LANGUAGE_NONE]);
+
+        // Check if we breached cardinality, and notify the user.
+        $field_info = field_info_field($key);
+        $field_count = count($entity->{$key}[LANGUAGE_NONE]);
+        if ($field_info['cardinality'] != FIELD_CARDINALITY_UNLIMITED && $field_count > $field_info['cardinality']) {
+          $entity_label = entity_label($context['entity_type'], $entity);
+          $warning = t('Tried to set !field_count values for field !field_name that supports a maximum of !cardinality.',
+                       array('!field_count' => $field_count,
+                             '!field_name' => $field_info['field_name'],
+                             '!cardinality' => $field_info['cardinality']));
+          drupal_set_message($warning, 'warning', FALSE);
+        }
+      }
+      else {
+        $entity->$key = $pseudo_entity->$key;
+      }
+    }
+  }
+
+  // Handle properties.
+  if (!empty($context['selected']['properties'])) {
+    // Use the wrapper to set property values, since some properties need
+    // additional massaging by their setter callbacks.
+    // The wrapper will automatically modify $entity itself.
+    $wrapper = entity_metadata_wrapper($context['entity_type'], $entity);
+    foreach ($context['selected']['properties'] as $key) {
+      if (in_array($key, $context['append']['properties'])) {
+        $old_values = $wrapper->$key->value();
+        $wrapper->$key->set($context['properties'][$key]);
+        $new_values = $wrapper->{$key}->value();
+        $all_values = array_merge($old_values, $new_values);
+        $wrapper->$key->set($all_values);
+      }
+      else {
+        $value = $context['properties'][$key];
+        if (is_string($value)) {
+          $value = token_replace($value, array($context['entity_type'] => $entity), array('sanitize' => FALSE));
+        }
+        $wrapper->$key->set($value);
+      }
+    }
+  }
+}
+
+/**
+ * Action form function.
+ *
+ * Displays form elements for properties acquired through Entity Metadata
+ * (hook_entity_property_info()), as well as field widgets for each
+ * entity bundle, as provided by field_attach_form().
+ */
+function views_bulk_operations_modify_action_form($context, &$form_state) {
+  // This action form uses admin-provided settings. If they were not set, pull the defaults now.
+  if (!isset($context['settings'])) {
+    $context['settings'] = views_bulk_operations_modify_action_views_bulk_operations_form_options();
+  }
+
+  $form_state['entity_type'] = $entity_type = $context['entity_type'];
+  // For Field API integration to work, a pseudo-entity is constructed for each
+  // bundle that has fields available for editing.
+  // The entities then get passed to Field API functions
+  // (field_attach_form(), field_attach_form_validate(), field_attach_submit()),
+  // and filled with form data.
+  // After submit, the pseudo-entities get passed to the actual action
+  // (views_bulk_operations_modify_action()) which copies the data from the
+  // relevant pseudo-entity constructed here to the actual entity being modified.
+  $form_state['entities'] = array();
+
+  $info = entity_get_info($entity_type);
+  $properties = _views_bulk_operations_modify_action_get_properties($entity_type, $context['settings']['display_values']);
+  $bundles = _views_bulk_operations_modify_action_get_bundles($entity_type, $context['settings']['display_values']);
+
+  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/modify.action.css';
+  $form['#tree'] = TRUE;
+
+  if (!empty($properties)) {
+    $form['properties'] = array(
+      '#type' => 'fieldset',
+      '#title' => 'Properties',
+    );
+    $form['properties']['show_value'] = array(
+      '#suffix' => '<div class="clearfix"></div>',
+    );
+
+    foreach ($properties as $key => $property) {
+      $form['properties']['show_value'][$key] = array(
+        '#type' => 'checkbox',
+        '#title' => $property['label'],
+      );
+
+      $determined_type = ($property['type'] == 'boolean') ? 'checkbox' : 'textfield';
+      $form['properties'][$key] = array(
+        '#type' => $determined_type,
+        '#title' => $property['label'],
+        '#description' => $property['description'],
+        '#states' => array(
+          'visible' => array(
+            '#edit-properties-show-value-' . str_replace('_', '-', $key) => array('checked' => TRUE),
+          ),
+        ),
+      );
+
+      if (!empty($property['options list'])) {
+        $form['properties'][$key]['#type'] = 'select';
+        $form['properties'][$key]['#options'] = $property['options list']($key, array());
+
+        if ($property['type'] == 'list') {
+          $form['properties'][$key]['#type'] = 'checkboxes';
+
+          $form['properties']['_append::' . $key] = array(
+            '#type' => 'checkbox',
+            '#title' => t('Add new value(s) to %label, instead of overwriting the existing values.', array('%label' => $property['label'])),
+            '#states' => array(
+              'visible' => array(
+                '#edit-properties-show-value-' . $key => array('checked' => TRUE),
+              ),
+            ),
+          );
+        }
+      }
+    }
+  }
+
+  foreach ($bundles as $bundle_name => $bundle) {
+    $bundle_key = $info['entity keys']['bundle'];
+    $entity = entity_create($context['entity_type'], array($bundle_key => $bundle_name));
+    $form_state['entities'][$bundle_name] = $entity;
+
+    // Show the more detailed label only if the entity type has multiple bundles.
+    // Otherwise, it would just be confusing.
+    if (count($info['bundles']) > 1) {
+      $label = t('Fields for @bundle_key @label', array('@bundle_key' => $bundle_key, '@label' => $bundle['label']));
+    }
+    else {
+      $label = t('Fields');
+    }
+
+    $form_key = 'bundle_' . $bundle_name;
+    $form[$form_key] = array(
+      '#type' => 'fieldset',
+      '#title' => $label,
+      '#parents' => array($form_key),
+    );
+    $form[$form_key]['show_value'] = array(
+      '#suffix' => '<div class="clearfix"></div>',
+      '#weight' => -100
+    );
+    field_attach_form($context['entity_type'], $entity, $form[$form_key], $form_state, LANGUAGE_NONE);
+
+    $display_values = $context['settings']['display_values'];
+    $instances = field_info_instances($entity_type, $bundle_name);
+    foreach ($instances as $field_name => $field) {
+      // The admin has specified which fields to display, but this field didn't
+      // make the cut. Hide it with #access => FALSE and move on.
+      if (empty($display_values[VBO_MODIFY_ACTION_ALL]) && empty($display_values[$bundle_name . '::' . $field_name])) {
+        $form[$form_key][$field_name]['#access'] = FALSE;
+        continue;
+      }
+
+      // For our use case it makes no sense for any field widget to be required.
+      $language = $form[$form_key][$field_name]['#language'];
+      _views_bulk_operations_modify_action_unset_required($form[$form_key][$field_name][$language]);
+
+      $form[$form_key]['show_value'][$field_name] = array(
+        '#type' => 'checkbox',
+        '#title' => $field['label'],
+        '#weight' => $form[$form_key][$field_name]['#weight'],
+      );
+      $form[$form_key][$field_name]['#states'] = array(
+        'visible' => array(
+          '#edit-bundle-' . str_replace('_', '-', $bundle_name) . '-show-value-' . str_replace('_', '-', $field_name) => array('checked' => TRUE),
+        ),
+      );
+
+      $field_info = field_info_field($field_name);
+      if ($field_info['cardinality'] != 1) {
+        $form[$form_key]['_append::' . $field_name] = array(
+          '#type' => 'checkbox',
+          '#title' => t('Add new value(s) to %label, instead of overwriting the existing values.', array('%label' => $field['label'])),
+          '#states' => array(
+            'visible' => array(
+              '#edit-bundle-' . str_replace('_', '-', $bundle_name) . '-show-value-' . str_replace('_', '-', $field_name) => array('checked' => TRUE),
+            ),
+          ),
+          '#weight' => $form[$form_key][$field_name]['#weight'] + 0.1,
+        );
+      }
+    }
+
+    // Make sure the checkboxes for showing fields are displayed in the same
+    // order as the fields themselves.
+    uasort($form[$form_key]['show_value'], 'element_sort');
+  }
+
+  // If the form has only one group (for example, "Properties"), remove the
+  // title and the fieldset, since there's no need to visually group values.
+  $form_elements = element_get_visible_children($form);
+  if (count($form_elements) == 1) {
+    $element_key = reset($form_elements);
+    unset($form[$element_key]['#type']);
+    unset($form[$element_key]['#title']);
+
+    // Get a list of all elements in the group, and filter out the non-values.
+    $values = element_get_visible_children($form[$element_key]);
+    foreach ($values as $index => $key) {
+      if ($key == 'show_value' || substr($key, 0, 1) == '_') {
+        unset($values[$index]);
+      }
+    }
+    // If the group has only one value, no need to hide it through #states.
+    if (count($values) == 1) {
+      $value_key = reset($values);
+      $form[$element_key]['show_value'][$value_key]['#type'] = 'value';
+      $form[$element_key]['show_value'][$value_key]['#value'] = TRUE;
+    }
+  }
+
+  if (module_exists('token') && $context['settings']['show_all_tokens']) {
+    $form['tokens'] = array(
+      '#type' => 'fieldset',
+      '#title' => 'Available tokens',
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+    $form['tokens']['tree'] = array(
+      '#theme' => 'token_tree',
+      '#token_types' => array('node'),
+      '#global_types' => array(),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Action form validate function.
+ *
+ * Checks that the user selected at least one value to modify, validates
+ * properties and calls Field API to validate fields for each bundle.
+ */
+function views_bulk_operations_modify_action_validate($form, &$form_state) {
+  // The form structure for "Show" checkboxes is a bit bumpy.
+  $search = array('properties');
+  foreach ($form_state['entities'] as $bundle => $entity) {
+    $search[] = 'bundle_' . $bundle;
+  }
+
+  $has_selected = FALSE;
+  foreach ($search as $group) {
+    // Store names of selected and appended entity values in a nicer format.
+    $form_state['selected'][$group] = array();
+    $form_state['append'][$group] = array();
+
+    // This group has no values, move on.
+    if (!isset($form_state['values'][$group])) {
+      continue;
+    }
+
+    foreach ($form_state['values'][$group]['show_value'] as $key => $value) {
+      if ($value) {
+        $has_selected = TRUE;
+        $form_state['selected'][$group][] = $key;
+      }
+      if (!empty($form_state['values'][$group]['_append::' . $key])) {
+        $form_state['append'][$group][] = $key;
+        unset($form_state['values'][$group]['_append::' . $key]);
+      }
+    }
+    unset($form_state['values'][$group]['show_value']);
+  }
+
+  if (!$has_selected) {
+    form_set_error('', t('You must select at least one value to modify.'));
+    return;
+  }
+
+  // Use the wrapper to validate property values.
+  if (!empty($form_state['selected']['properties'])) {
+    // The entity used is irrelevant, and we can't rely on
+    // $form_state['entities'] being non-empty, so a new one is created.
+    $info = entity_get_info($form_state['entity_type']);
+    $bundle_key = $info['entity keys']['bundle'];
+    $bundle_names = array_keys($info['bundles']);
+    $bundle_name = reset($bundle_names);
+    $entity = entity_create($form_state['entity_type'], array($bundle_key => $bundle_name));
+    $wrapper = entity_metadata_wrapper($form_state['entity_type'], $entity);
+
+    $properties = _views_bulk_operations_modify_action_get_properties($form_state['entity_type']);
+    foreach ($form_state['selected']['properties'] as $key) {
+      $value = $form_state['values']['properties'][$key];
+      if (!$wrapper->$key->validate($value)) {
+        $label = $properties[$key]['label'];
+        form_set_error('properties][' . $key, t('%label contains an invalid value.', array('%label' => $label)));
+      }
+    }
+  }
+
+  foreach ($form_state['entities'] as $bundle_name => $entity) {
+    field_attach_form_validate($form_state['entity_type'], $entity, $form['bundle_' . $bundle_name], $form_state);
+  }
+}
+
+/**
+ * Action form submit function.
+ *
+ * Fills each constructed entity with property and field values, then
+ * passes them to views_bulk_operations_modify_action().
+ */
+function views_bulk_operations_modify_action_submit($form, $form_state) {
+  foreach ($form_state['entities'] as $bundle_name => $entity) {
+    field_attach_submit($form_state['entity_type'], $entity, $form['bundle_' . $bundle_name], $form_state);
+  }
+
+  return array(
+    'append' => $form_state['append'],
+    'selected' => $form_state['selected'],
+    'entities' => $form_state['entities'],
+    'properties' => isset($form_state['values']['properties']) ? $form_state['values']['properties'] : array(),
+  );
+}
+
+/**
+ * Returns all properties that can be modified.
+ *
+ * Properties that can't be changed are entity keys, timestamps, and the ones
+ * without a setter callback.
+ *
+ * @param $entity_type
+ *   The entity type whose properties will be fetched.
+ * @param $display_values
+ *   An optional, admin-provided list of properties and fields that should be
+ *   displayed for editing, used to filter the returned list of properties.
+ */
+function _views_bulk_operations_modify_action_get_properties($entity_type, $display_values = NULL) {
+  $properties = array();
+  $info = entity_get_info($entity_type);
+
+  // List of properties that can't be modified.
+  $disabled_properties = array('created', 'changed');
+  foreach (array('id', 'bundle', 'revision') as $key) {
+    if (isset($info['entity keys'][$key])) {
+      $disabled_properties[] = $info['entity keys'][$key];
+    }
+  }
+  // List of supported types.
+  $supported_types = array('text', 'token', 'integer', 'decimal', 'date', 'duration',
+                           'boolean', 'uri', 'list');
+
+  $property_info = entity_get_property_info($entity_type);
+  foreach ($property_info['properties'] as $key => $property) {
+    if (in_array($key, $disabled_properties)) {
+      continue;
+    }
+    // Filter out properties that can't be set (they are usually generated by a
+    // getter callback based on other properties, and not stored in the DB).
+    if (empty($property['setter callback'])) {
+      continue;
+    }
+    // Determine the property type. If it's empty (permitted), default to text.
+    // If it's a list type such as list<boolean>, extract the "boolean" part.
+    $property['type'] = empty($property['type']) ? 'text' : $property['type'];
+    $type = $property['type'];
+    if ($list_type = entity_property_list_extract_type($type)) {
+      $type = $list_type;
+      $property['type'] = 'list';
+    }
+    // Filter out non-supported types (such as the Field API fields that
+    // Commerce adds to its entities so that they show up in tokens).
+    if (!in_array($type, $supported_types)) {
+      continue;
+    }
+
+    $properties[$key] = $property;
+  }
+
+  if (isset($display_values) && empty($display_values[VBO_MODIFY_ACTION_ALL])) {
+    // Return only the properties that the admin specified.
+    return array_intersect_key($properties, $display_values);
+  }
+
+  return $properties;
+}
+
+/**
+ * Returns all bundles for which field widgets should be displayed.
+ *
+ * If the admin decided to limit the modify form to certain properties / fields
+ * (through the action settings) then only bundles that have at least one field
+ * selected are returned.
+ *
+ * @todo Make it consult the view passed through $context, and if it has a
+ * filter on the bundle key, use the selected values.
+ *
+ * @param $entity_type
+ *   The entity type whose bundles will be fetched.
+ * @param $display_values
+ *   An admin-provided list of properties and fields that should be displayed
+ *   for editing, used to filter the returned list of bundles.
+ */
+function _views_bulk_operations_modify_action_get_bundles($entity_type, $display_values) {
+  $info = entity_get_info($entity_type);
+  $filtered_bundles = array();
+
+  foreach ($info['bundles'] as $bundle_name => $bundle) {
+    $instances = field_info_instances($entity_type, $bundle_name);
+    // Ignore bundles that don't have any field instances attached.
+    if (empty($instances)) {
+      continue;
+    }
+
+    $has_enabled_fields = FALSE;
+    foreach ($display_values as $key) {
+      if (strpos($key, $bundle_name . '::') !== FALSE) {
+        $has_enabled_fields = TRUE;
+      }
+    }
+    // The admin has either specified that all values should be modifiable, or
+    // selected at least one field belonging to this bundle.
+    if (!empty($display_values[VBO_MODIFY_ACTION_ALL]) || $has_enabled_fields) {
+      $filtered_bundles[$bundle_name] = $bundle;
+    }
+  }
+
+  return $filtered_bundles;
+}
+
+/**
+ * Helper function that recursively strips #required from field widgets.
+ */
+function _views_bulk_operations_modify_action_unset_required(&$element) {
+  unset($element['#required']);
+  foreach (element_children($element) as $key) {
+    _views_bulk_operations_modify_action_unset_required($element[$key]);
+  }
+}
+
+/**
+ * VBO settings form function.
+ */
+function views_bulk_operations_modify_action_views_bulk_operations_form_options() {
+  $options['show_all_tokens'] = TRUE;
+  $options['display_values'] = array(VBO_MODIFY_ACTION_ALL);
+  return $options;
+}
+
+/**
+ * The settings form for this action.
+ */
+function views_bulk_operations_modify_action_views_bulk_operations_form($options, $entity_type, $dom_id) {
+  // Initialize default values.
+  if (empty($options)) {
+    $options = views_bulk_operations_modify_action_views_bulk_operations_form_options();
+  }
+
+  $form['show_all_tokens'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Show available tokens'),
+    '#description' => t('Check this to show a list of all available tokens in the bottom of the form. Requires the token module.'),
+    '#default_value' => $options['show_all_tokens'],
+  );
+
+  $info = entity_get_info($entity_type);
+  $properties = _views_bulk_operations_modify_action_get_properties($entity_type);
+  $values = array(VBO_MODIFY_ACTION_ALL => t('- All -'));
+  foreach ($properties as $key => $property) {
+    $label = t('Properties');
+    $values[$label][$key] = $property['label'];
+  }
+  foreach ($info['bundles'] as $bundle_name => $bundle) {
+    $bundle_key = $info['entity keys']['bundle'];
+    // Show the more detailed label only if the entity type has multiple bundles.
+    // Otherwise, it would just be confusing.
+    if (count($info['bundles']) > 1) {
+      $label = t('Fields for @bundle_key @label', array('@bundle_key' => $bundle_key, '@label' => $bundle['label']));
+    }
+    else {
+      $label = t('Fields');
+    }
+
+    $instances = field_info_instances($entity_type, $bundle_name);
+    foreach ($instances as $field_name => $field) {
+      $values[$label][$bundle_name . '::' . $field_name] = $field['label'];
+    }
+  }
+
+  $form['display_values'] = array(
+    '#type' => 'select',
+    '#title' => t('Display values'),
+    '#options' => $values,
+    '#multiple' => TRUE,
+    '#description' => t('Select which values the action form should present to the user.'),
+    '#default_value' => $options['display_values'],
+  );
+  return $form;
+}
diff --git a/css/modify.action.css b/css/modify.action.css
new file mode 100644
index 0000000..e4f24af
--- /dev/null
+++ b/css/modify.action.css
@@ -0,0 +1,4 @@
+div[class*="show-value"] {
+  float: left;
+  margin-right: 20px;
+}
diff --git a/views_bulk_operations.module b/views_bulk_operations.module
index 5282bfa..bfd80ca 100644
--- a/views_bulk_operations.module
+++ b/views_bulk_operations.module
@@ -46,6 +46,7 @@ function views_bulk_operations_load_action_includes() {
     'archive.action.inc',
     'argument_selector.action.inc',
     'delete.action.inc',
+    'modify.action.inc',
     'script.action.inc',
     'user_roles.action.inc',
   );
