diff --git a/modules/data.rules.inc b/modules/data.rules.inc
index e78420b..412586f 100644
--- a/modules/data.rules.inc
+++ b/modules/data.rules.inc
@@ -8,6 +8,19 @@
  */
 
 /**
+ * Implements hook_rules_category_info() on behalf of the pseudo data module.
+ */
+function rules_data_category_info() {
+  return array(
+    'rules_data' => array(
+      'label' => t('Data'),
+      'equals group' => t('Data'),
+      'weight' => -50,
+    ),
+  );
+}
+
+/**
  * Implements hook_rules_file_info() on behalf of the pseudo data module.
  * @see rules_core_modules()
  */
diff --git a/modules/entity.rules.inc b/modules/entity.rules.inc
index 76ec4ee..64776ae 100644
--- a/modules/entity.rules.inc
+++ b/modules/entity.rules.inc
@@ -16,6 +16,19 @@ function rules_entity_file_info() {
 }
 
 /**
+ * Implements hook_rules_category_info() on behalf of the pseudo entity module.
+ */
+function rules_entity_category_info() {
+  return array(
+    'rules_entity' => array(
+      'label' => t('Entities'),
+      'equals group' => t('Entities'),
+      'weight' => -50,
+    ),
+  );
+}
+
+/**
  * Implements hook_rules_action_info() on behalf of the entity module.
  * @see rules_core_modules()
  */
diff --git a/modules/node.eval.inc b/modules/node.eval.inc
index 39922b0..50940a7 100644
--- a/modules/node.eval.inc
+++ b/modules/node.eval.inc
@@ -18,7 +18,7 @@ abstract class RulesNodeConditionBase extends RulesConditionHandlerBase {
       'parameter' => array(
         'node' => array('type' => 'node', 'label' => t('Content')),
       ),
-      'group' => t('Node'),
+      'category' => 'node',
       'access callback' => 'rules_node_integration_access',
     );
   }
diff --git a/modules/node.rules.inc b/modules/node.rules.inc
index a126931..2629840 100644
--- a/modules/node.rules.inc
+++ b/modules/node.rules.inc
@@ -8,6 +8,18 @@
  */
 
 /**
+ * Implements hook_rules_category_info() on behalf of the node module.
+ */
+function rules_node_category_info() {
+  return array(
+    'node' => array(
+      'label' => t('Node'),
+      'equals group' => t('Node'),
+    ),
+  );
+}
+
+/**
  * Implements hook_rules_file_info() on behalf of the node module.
  */
 function rules_node_file_info() {
@@ -21,28 +33,28 @@ function rules_node_event_info() {
   $items = array(
     'node_insert' => array(
       'label' => t('After saving new content'),
-      'group' => t('Node'),
+      'category' => 'node',
       'variables' => rules_events_node_variables(t('created content')),
       'access callback' => 'rules_node_integration_access',
       'class' => 'RulesNodeEventHandler',
     ),
     'node_update' => array(
       'label' => t('After updating existing content'),
-      'group' => t('Node'),
+      'category' => 'node',
       'variables' => rules_events_node_variables(t('updated content'), TRUE),
       'access callback' => 'rules_node_integration_access',
       'class' => 'RulesNodeEventHandler',
     ),
     'node_presave' => array(
       'label' => t('Before saving content'),
-      'group' => t('Node'),
+      'category' => 'node',
       'variables' => rules_events_node_variables(t('saved content'), TRUE),
       'access callback' => 'rules_node_integration_access',
       'class' => 'RulesNodeEventHandler',
     ),
     'node_view' => array(
       'label' => t('Content is viewed'),
-      'group' => t('Node'),
+      'category' => 'node',
       'help' => t("Note that if drupal's page cache is enabled, this event won't be generated for pages served from cache."),
       'variables' => rules_events_node_variables(t('viewed content')) + array(
         'view_mode' => array(
@@ -58,7 +70,7 @@ function rules_node_event_info() {
     ),
     'node_delete' => array(
       'label' => t('After deleting content'),
-      'group' => t('Node'),
+      'category' => 'node',
       'variables' => rules_events_node_variables(t('deleted content')),
       'access callback' => 'rules_node_integration_access',
       'class' => 'RulesNodeEventHandler',
@@ -96,7 +108,7 @@ function rules_node_action_info() {
     'parameter' => array(
       'node' => array('type' => 'node', 'label' => t('Content'), 'save' => TRUE),
     ),
-    'group' => t('Node'),
+    'category' => 'node',
     'access callback' => 'rules_node_admin_access',
   );
   // Add support for hand-picked core actions.
diff --git a/modules/rules_core.rules.inc b/modules/rules_core.rules.inc
index 4b8889b..39fff93 100644
--- a/modules/rules_core.rules.inc
+++ b/modules/rules_core.rules.inc
@@ -9,6 +9,19 @@
  */
 
 /**
+ * Implements hook_rules_category_info() on behalf of the rules_core.
+ */
+function rules_rules_core_category_info() {
+  return array(
+    'rules_components' => array(
+      'label' => t('Components'),
+      'equals group' => t('Components'),
+      'weight' => 50,
+    ),
+  );
+}
+
+/**
  * Implements hook_rules_file_info() on behalf of the pseudo rules_core module.
  *
  * @see rules_core_modules()
diff --git a/rules.api.php b/rules.api.php
index 7b01b14..3ecfb2e 100644
--- a/rules.api.php
+++ b/rules.api.php
@@ -156,6 +156,46 @@ function hook_rules_action_info() {
 }
 
 /**
+ * Define categories for Rules items, e.g. actions, conditions or events.
+ *
+ * Categories are similar to the previously used 'group' key in e.g.
+ * hook_rules_action_info(), but have a machine name and some more optional
+ * keys like a weight, or an icon.
+ *
+ * For best compatibility, modules may keep using the 'group' key for referring
+ * to categories. However, if a 'group' key and a 'category' is given the group
+ * will be treated as grouping in the given category (e.g. group "paypal" in
+ * category "commerce payment").
+ *
+ * @return
+ *   An array of information about the module's provided categories.
+ *   The array contains a sub-array for each category, with the category name as
+ *   the key. Names may only contain lowercase alpha-numeric characters
+ *   and underscores and should be prefixed with the providing module name.
+ *   Possible attributes for each sub-array are:
+ *   - label: The label of the category. Start capitalized. Required.
+ *   - weight: (optional) A weight for sorting the category. Defaults to 0.
+ *   - equals group: (optional) For BC, categories may be defined that equal
+ *     a previsouly used 'group'.
+ *   - icon: (optional) The relative file path of a category item to use.
+ *     The icon should be a transparent png-24 containing no colors (only #fff).
+ *     See XXX for instructions on how to create a suiting icon.
+ *     Note that the icon is currently not used by Rules, however other UIs
+ *     building upon Rules (like fluxkraft) may do, and future releases of Rules
+ *     might do as well. If no icon is given, an icon needs to be
+ *     auto-generated.
+ */
+function hook_rules_category_info() {
+  return array(
+    'rules_data' => array(
+      'label' => t('Data'),
+      'equals group' => t('Data'),
+      'weight' => -50,
+    ),
+  );
+}
+
+/**
  * Specify files containing rules integration code.
  *
  * All files specified in that hook will be included when rules looks for
diff --git a/rules.rules.inc b/rules.rules.inc
index 120773d..fcd3c94 100644
--- a/rules.rules.inc
+++ b/rules.rules.inc
@@ -59,6 +59,13 @@ function rules_rules_file_info() {
 }
 
 /**
+ * Implements hook_rules_category_info().
+ */
+function rules_rules_category_info() {
+  return _rules_rules_collect_items('category_info');
+}
+
+/**
  * Implements hook_rules_action_info().
  */
 function rules_rules_action_info() {
diff --git a/ui/ui.core.inc b/ui/ui.core.inc
index 8b47b0b..a51c6af 100644
--- a/ui/ui.core.inc
+++ b/ui/ui.core.inc
@@ -842,47 +842,10 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface {
   }
 
   /**
-   * Returns an array of options to use with a select for the items specified
-   * in the given hook.
-   *
-   * @param $item_type
-   *   The item type to get options for. One of 'data', 'event', 'condition' and
-   *   'action'.
-   * @param $items
-   *   (optional) An array of items to restrict the options to.
-   *
-   * @return
-   *   An array of options.
+   * @see RulesUICategory::getOptions()
    */
   public static function getOptions($item_type, $items = NULL) {
-    $sorted_data = array();
-    $ungrouped = array();
-    $data = $items ? $items : rules_fetch_data($item_type . '_info');
-    foreach ($data as $name => $info) {
-      // Verfiy the current user has access to use it.
-      if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) {
-        continue;
-      }
-      if (!empty($info['group'])) {
-        $sorted_data[drupal_ucfirst($info['group'])][$name] = drupal_ucfirst($info['label']);
-      }
-      else {
-        $ungrouped[$name] = drupal_ucfirst($info['label']);
-      }
-    }
-    asort($ungrouped);
-    foreach ($sorted_data as $key => $choices) {
-      asort($choices);
-      $sorted_data[$key] = $choices;
-    }
-    ksort($sorted_data);
-    // Always move the 'Components' group down it it exists.
-    if (isset($sorted_data[t('Components')])) {
-      $copy = $sorted_data[t('Components')];
-      unset($sorted_data[t('Components')]);
-      $sorted_data[t('Components')] = $copy;
-    }
-    return $ungrouped + $sorted_data;
+    return RulesUICategory::getOptions($item_type, $items = NULL);
   }
 
   public static function formDefaults(&$form, &$form_state) {
@@ -1176,3 +1139,137 @@ class RulesActionContainerUI extends RulesContainerPluginUI {
     $form['elements']['#caption'] = t('Actions');
   }
 }
+
+/**
+ * Class holding category related methods.
+ */
+class RulesUICategory {
+
+  /**
+   * Gets info about all available categories, or about a specific category.
+   *
+   * @return array
+   */
+  public static function getInfo($category = NULL) {
+    $data = rules_fetch_data('category_info');
+    if (isset($category)) {
+      return $data[$category];
+    }
+    return $data;
+  }
+
+  /**
+   * Returns a group label, e.g. as usable for opt-groups in a select list.
+   *
+   * @param array $item_info
+   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
+   * @param bool $in_category
+   *   (optional) Whether group labels for grouping inside a category should be
+   *   return. Defaults to FALSE.
+   *
+   * @return string|boolean
+   *   The group label to use, or FALSE if none can be found.
+   */
+  public static function getItemGroup($item_info, $in_category = FALSE) {
+    if (isset($item_info['category']) && !$in_category) {
+      return self::getCategory($item_info, 'label');
+    }
+    else if (!empty($item_info['group'])) {
+      return $item_info['group'];
+    }
+    return FALSE;
+  }
+
+  /**
+   * Gets the category for the given item info array.
+   *
+   * @param array $item_info
+   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
+   * @param string|null $key
+   *   (optional) The key of the category info to return, e.g. 'label'. If none
+   *   is given the whole info array is returned.
+   *
+   * @return array|mixed|false
+   *   Either the whole category info array or the value of the given key. If
+   *   no category can be found, FALSE is returned.
+   */
+  public static function getCategory($item_info, $key = NULL) {
+    if (isset($item_info['category'])) {
+      $info = self::getInfo($item_info['category']);
+      return isset($key) ? $info[$key] : $info;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns an array of options to use with a select for the items specified
+   * in the given hook.
+   *
+   * @param $item_type
+   *   The item type to get options for. One of 'data', 'event', 'condition' and
+   *   'action'.
+   * @param $items
+   *   (optional) An array of items to restrict the options to.
+   *
+   * @return
+   *   An array of options.
+   */
+  public static function getOptions($item_type, $items = NULL) {
+    $sorted_data = array();
+    $ungrouped = array();
+    $data = $items ? $items : rules_fetch_data($item_type . '_info');
+    foreach ($data as $name => $info) {
+      // Verfiy the current user has access to use it.
+      if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) {
+        continue;
+      }
+      if ($group = RulesUICategory::getItemGroup($info)) {
+        $sorted_data[drupal_ucfirst($group)][$name] = drupal_ucfirst($info['label']);
+      }
+      else {
+        $ungrouped[$name] = drupal_ucfirst($info['label']);
+      }
+    }
+    asort($ungrouped);
+    foreach ($sorted_data as $key => $choices) {
+      asort($choices);
+      $sorted_data[$key] = $choices;
+    }
+
+    // Sort the grouped data by category weights, defaulting to weight 0 for
+    // groups without a respective category.
+    $sorted_groups = array();
+    foreach (array_keys($sorted_data) as $label) {
+      $sorted_groups[$label] = array('weight' => 0, 'label' => $label);
+    }
+    // Add in category weights.
+    foreach (RulesUICategory::getInfo() as $info) {
+      if (isset($sorted_groups[$info['label']])) {
+        $sorted_groups[$info['label']] = $info;
+      }
+    }
+    uasort($sorted_groups, '_rules_ui_sort_categories');
+
+    // Now replace weights with group content.
+    foreach ($sorted_groups as $group => $weight) {
+      $sorted_groups[$group] = $sorted_data[$group];
+    }
+    return $ungrouped + $sorted_groups;
+  }
+}
+
+/**
+ * Helper for sorting categories.
+ */
+function _rules_ui_sort_categories($a, $b) {
+  // @see element_sort()
+  $a_weight = isset($a['weight']) ? $a['weight'] : 0;
+  $b_weight = isset($b['weight']) ? $b['weight'] : 0;
+  if ($a_weight == $b_weight) {
+    // @see element_sort_by_title()
+    $a_title = isset($a['label']) ? $a['label'] : '';
+    $b_title = isset($b['label']) ? $b['label'] : '';
+    return strnatcasecmp($a_title, $b_title);
+  }
+  return ($a_weight < $b_weight) ? -1 : 1;
+}
