diff --git a/bootstrap.php b/bootstrap.php
index 63d71fe8754d51c5d110ec1d9fa447dc0bfefdac..2085a4fbc1909d4c00ab79424e405f4c6ebd78b0 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -125,6 +125,7 @@ class Bootstrap {
 
         define('FILTER_TABLE', $prefix.'filter');
         define('FILTER_RULE_TABLE', $prefix.'filter_rule');
+        define('FILTER_ACTION_TABLE', $prefix.'filter_action');
 
         define('PLUGIN_TABLE', $prefix.'plugin');
         define('SEQUENCE_TABLE', $prefix.'sequence');
diff --git a/include/ajax.filter.php b/include/ajax.filter.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c34b54428e4f1078ef2f210be1bc5c2f82ce3c4
--- /dev/null
+++ b/include/ajax.filter.php
@@ -0,0 +1,15 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.filter.php');
+
+class FilterAjaxAPI extends AjaxController {
+
+    function getFilterActionForm($type) {
+        if (!($A = FilterAction::lookupByType($type)))
+            Http::response(404, 'No such filter action type');
+
+        $form = $A->getConfigurationForm();
+        include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+    }
+
+}
diff --git a/include/class.filter.php b/include/class.filter.php
index acc3a3590a7921d1bee8599417c20fd1714e011a..c29df8dc579a52aa071fe6c806521676bb2c5b92 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -14,6 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
+require_once INCLUDE_DIR . 'class.filter_action.php';
+
 class Filter {
 
     var $id;
@@ -293,47 +295,23 @@ class Filter {
 
         return $match;
     }
+
+    function getActions() {
+        return FilterAction::objects()->filter(array(
+            'filter_id'=>$this->getId()
+        ));
+    }
     /**
      * If the matches() method returns TRUE, send the initial ticket to this
      * method to apply the filter actions defined
      */
     function apply(&$ticket, $info=null) {
-        # TODO: Disable alerting
-        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
-        if ($this->disableAlerts()) $ticket['autorespond']=false;
-        #       Set owning department (?)
-        if ($this->getDeptId())     $ticket['deptId']=$this->getDeptId();
-        #       Set ticket priority (?)
-        if ($this->getPriorityId()) $ticket['priorityId']=$this->getPriorityId();
-        #       Set SLA plan (?)
-        if ($this->getSLAId())      $ticket['slaId']=$this->getSLAId();
-        #       Set status
-        if ($this->getStatusId())   $ticket['statusId']=$this->getStatusId();
-        #       Auto-assign to (?)
-        #       XXX: Unset the other (of staffId or teamId) (?)
-        if ($this->getStaffId())    $ticket['staffId']=$this->getStaffId();
-        elseif ($this->getTeamId()) $ticket['teamId']=$this->getTeamId();
-        #       Override name with reply-to information from the TicketFilter
-        #       match
-        if ($this->useReplyToEmail() && $info['reply-to']) {
-            $changed = $info['reply-to'] != $ticket['email']
-                || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']);
-            $ticket['email'] = $info['reply-to'];
-            if ($info['reply-to-name'])
-                $ticket['name'] = $info['reply-to-name'];
-            if ($changed)
-                throw new FilterDataChanged();
+        foreach ($this->getActions() as $a) {
+            $a->apply($ticket, $info);
         }
-
-        # Use canned response.
-        if ($this->getCannedResponse())
-            $ticket['cannedResponseId'] = $this->getCannedResponse();
-
-        # Apply help topic
-        if ($this->getHelpTopic())
-            $ticket['topicId'] = $this->getHelpTopic();
     }
-     static function getSupportedMatches() {
+
+    static function getSupportedMatches() {
         foreach (static::$match_types as $k=>&$v) {
             if (is_callable($v[0]))
                 $v[0] = $v[0]();
@@ -518,17 +496,9 @@ class Filter {
             .',name='.db_input($vars['name'])
             .',execorder='.db_input($vars['execorder'])
             .',email_id='.db_input($emailId)
-            .',dept_id='.db_input($vars['dept_id'])
-            .',status_id='.db_input($vars['status_id'])
-            .',priority_id='.db_input($vars['priority_id'])
-            .',sla_id='.db_input($vars['sla_id'])
-            .',topic_id='.db_input($vars['topic_id'])
             .',match_all_rules='.db_input($vars['match_all_rules'])
             .',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0)
             .',reject_ticket='.db_input(isset($vars['reject_ticket'])?1:0)
-            .',use_replyto_email='.db_input(isset($vars['use_replyto_email'])?1:0)
-            .',disable_autoresponder='.db_input(isset($vars['disable_autoresponder'])?1:0)
-            .',canned_response_id='.db_input($vars['canned_response_id'])
             .',notes='.db_input(Format::sanitize($vars['notes']));
 
 
@@ -558,9 +528,37 @@ class Filter {
         # Don't care about errors stashed in $xerrors
         $xerrors = array();
         self::save_rules($id,$vars,$xerrors);
+        self::save_actions($id, $vars, $errors);
 
         return true;
     }
+
+    function save_actions($id, $vars, &$errors) {
+        if (!is_array(@$vars['actions']))
+            return;
+
+        foreach ($vars['actions'] as $v) {
+            $info = substr($v, 1);
+            switch ($v[0]) {
+            case 'N': # new filter action
+                $I = FilterAction::create(array(
+                    'type'=>$info,
+                    'filter_id'=>$id,
+                ));
+                $I->setConfiguration($errors);
+                $I->save();
+                break;
+            case 'I': # exiting filter action
+                if ($I = FilterAction::lookup($info))
+                    $I->setConfiguration() && $I->save();
+                break;
+            case 'D': # deleted filter action
+                if ($I = FilterAction::lookup($info))
+                    $I->delete();
+                break;
+            }
+        }
+    }
 }
 
 class FilterRule {
diff --git a/include/class.filter_action.php b/include/class.filter_action.php
new file mode 100644
index 0000000000000000000000000000000000000000..b54eabb68db7c6fe819a6b7c848eb7f451cc6e32
--- /dev/null
+++ b/include/class.filter_action.php
@@ -0,0 +1,395 @@
+<?php
+
+require_once INCLUDE_DIR . 'class.orm.php';
+
+class FilterAction extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILTER_ACTION_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+    );
+
+    static $registry = array();
+
+    var $_impl;
+    var $_config;
+
+    function getId() {
+        return $this->id;
+    }
+
+    function getConfiguration() {
+        if (!$this->_config) {
+            $this->_config = $this->get('configuration');
+            if (is_string($this->_config))
+                $this->_config = JsonDataParser::parse($this->_config);
+            elseif (!$this->_config)
+                $this->_config = array();
+            foreach ($this->getImpl()->getConfigurationOptions() as $name=>$field)
+                if (!isset($this->_config[$name]))
+                    $this->_config[$name] = $field->get('default');
+        }
+        return $this->_config;
+    }
+
+    function setConfiguration($source=false, &$errors=array()) {
+        $config = array();
+        foreach ($this->getImpl()->getConfigurationForm($source ?: $_POST)
+                ->getFields() as $name=>$field) {
+            $config[$name] = $field->to_php($field->getClean());
+            $errors = array_merge($errors, $field->errors());
+        }
+        if (count($errors) === 0)
+            $this->set('configuration', JsonDataEncoder::encode($config));
+        return count($errors) === 0;
+    }
+
+    function getImpl() {
+        if (!isset($this->_impl)) {
+            if (!($I = self::lookupByType($this->type, $this)))
+                throw new Exception(sprintf(
+                    '%s: No such filter action registered', $this->type));
+            $this->_impl = $I;
+        }
+        return $this->_impl;
+    }
+
+    function apply(&$ticket, array $info) {
+        return $this->getImpl()->apply($ticket, $info);
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    static function register($class, $type=false) {
+        // TODO: Check if $class implements TriggerAction
+        self::$registry[$type ?: $class::$type] = $class;
+    }
+
+    static function lookupByType($type, $thisObj=false) {
+        if (!isset(self::$registry[$type]))
+            return null;
+
+        $class = self::$registry[$type];
+        return new $class($thisObj);
+    }
+
+    static function allRegistered() {
+        $types = array();
+        foreach (self::$registry as $type=>$class) {
+            $types[$type] = $class::getName();
+        }
+        return $types;
+    }
+}
+
+abstract class TriggerAction {
+    function __construct($action=false) {
+        $this->action = $action;
+    }
+
+    function getConfiguration() {
+        if ($this->action)
+            return $this->action->getConfiguration();
+        return array();
+    }
+
+    function getConfigurationForm($source=false) {
+        if (!$this->_cform) {
+            $config = $this->getConfiguration();
+            $options = $this->getConfigurationOptions();
+            // Find a uid offset for this guy
+            $uid = 1000;
+            foreach (FilterAction::$registry as $type=>$class) {
+                $uid += 100;
+                if ($type == $this->getType())
+                    break;
+            }
+            // Ensure IDs are unique
+            foreach ($options as $f) {
+                $f->set('id', $uid++);
+            }
+            $this->_cform = new Form($options, $source);
+            if (!$source) {
+                foreach ($this->_cform->getFields() as $name=>$f) {
+                    if ($config && isset($config[$name]))
+                        $f->value = $config[$name];
+                    elseif ($f->get('default'))
+                        $f->value = $f->get('default');
+                }
+            }
+        }
+        return $this->_cform;
+    }
+
+    static function getType() { return static::$type; }
+    static function getName() { return __(static::$name); }
+
+    abstract function apply(&$ticket, array $info);
+    abstract function getConfigurationOptions();
+}
+
+class FA_UseReplyTo extends TriggerAction {
+    static $type = 'replyto';
+    static $name = /* trans */ 'Reply-To Email';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['enable'] && $info['reply-to']) {
+            $ticket['email'] = $info['reply-to'];
+            if ($info['reply-to-name'])
+                $ticket['name'] = $info['reply-to-name'];
+        }
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'enable' => new BooleanField(array(
+                'configuration' => array(
+                    'desc' => __('Use the Reply-To email header')
+                )
+            )),
+        );
+    }
+}
+FilterAction::register('FA_UseReplyTo');
+
+class FA_DisableAutoResponse extends TriggerAction {
+    static $type = 'noresp';
+    static $name = /* trans */ "Ticket auto-response";
+
+    function apply(&$ticket, array $info) {
+        # TODO: Disable alerting
+        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
+        $config = $this->getConfiguration();
+        if ($config['enable']) {
+            $ticket['autorespond']=false;
+        }
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'enable' => new BooleanField(array(
+                'configuration' => array(
+                    'desc' => __('<strong>Disable</strong> auto-response')
+                ),
+            )),
+        );
+    }
+}
+FilterAction::register('FA_DisableAutoResponse');
+
+class FA_AutoCannedResponse extends TriggerAction {
+    static $type = 'canned';
+    static $name = /* trans */ "Canned Response";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['canned_id']) {
+            $ticket['cannedResponseId'] = $config['canned_id'];
+        }
+    }
+
+    function getConfigurationOptions() {
+        $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
+        $choices = array(false => '— '.__('None').' —');
+        if ($res=db_query($sql)) {
+            while (list($id, $title, $isenabled)=db_fetch_row($res)) {
+                if (!$isenabled)
+                    $title .= ' ' . __('(disabled)');
+                $choices[$id] = $title;
+            }
+        }
+        return array(
+            'canned_id' => new ChoiceField(array(
+                'default' => false,
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AutoCannedResponse');
+
+class FA_RouteDepartment extends TriggerAction {
+    static $type = 'dept';
+    static $name = /* trans */ 'Department';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['dept_id'])
+            $ticket['deptId'] = $config['dept_id'];
+    }
+
+    function getConfigurationOptions() {
+        $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name';
+        $choices = array();
+        if(($res=db_query($sql)) && db_num_rows($res)){
+            while(list($id,$name)=db_fetch_row($res)){
+                $choices[$id] = $name;
+            }
+        }
+        return array(
+            'dept_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_RouteDepartment');
+
+class FA_AssignPriority extends TriggerAction {
+    static $type = 'pri';
+    static $name = /* trans */ "Priority";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['priority'])
+            $ticket['priority_id'] = $config['priority']->getId();
+    }
+
+    function getConfigurationOptions() {
+        $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
+              .' ORDER BY priority_urgency DESC';
+        $choices = array();
+        if ($res = db_query($sql)) {
+            while ($row = db_fetch_row($res))
+                $choices[$row[0]] = $row[1];
+        }
+        return array(
+            'priority' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignPriority');
+
+class FA_AssignSLA extends TriggerAction {
+    static $type = 'sla';
+    static $name = /* trans */ 'SLA Plan';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['sla_id'])
+            $ticket['slaId'] = $config['sla_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = SLA::getSLAs();
+        return array(
+            'sla_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignSLA');
+
+class FA_AssignTeam extends TriggerAction {
+    static $type = 'team';
+    static $name = /* trans */ 'Assign Team';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['team_id'])
+            $ticket['teamId'] = $config['team_id'];
+    }
+
+    function getConfigurationOptions() {
+        $sql='SELECT team_id, isenabled, name FROM '.TEAM_TABLE .' ORDER BY name';
+        $choices = array();
+        if(($res=db_query($sql)) && db_num_rows($res)){
+            while (list($id, $isenabled, $name) = db_fetch_row($res)){
+                if (!$isenabled)
+                    $name .= ' '.__('(disabled)');
+                $choices[$id] = $name;
+            }
+        }
+        return array(
+            'team_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTeam');
+
+class FA_AssignAgent extends TriggerAction {
+    static $type = 'agent';
+    static $name = /* trans */ 'Assign Agent';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['staff_id'])
+            $ticket['staffId'] = $config['staff_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Staff::getStaffMembers();
+        return array(
+            'staff_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignAgent');
+
+class FA_AssignTopic extends TriggerAction {
+    static $type = 'topic';
+    static $name = /* trans */ 'Help Topic';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['topic_id'])
+            $ticket['topicId'] = $config['topic_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = HelpTopic::getAllHelpTopics();
+        return array(
+            'topic_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTopic');
+
+class FA_SetStatus extends TriggerAction {
+    static $type = 'status';
+    static $name = /* trans */ 'Ticket Status';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['status_id'])
+            $ticket['statusId'] = $config['status_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = array();
+        foreach (TicketStatusList::getStatuses() as $S) {
+            // TODO: Move this to TicketStatus::getName
+            $name = $S->getName();
+            if (!($isenabled = $S->isEnabled()))
+                $name.=' '.__('(disabled)');
+            $choices[$S->getId()] = $name;
+        }
+        return array(
+            'status_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_SetStatus');
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php
index f09731af64a0cc7f5455b3677b408bb208af29aa..5ef7d36a1482f441ddb470a6a4f45fb6465477d4 100644
--- a/include/staff/filter.inc.php
+++ b/include/staff/filter.inc.php
@@ -181,195 +181,65 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     &nbsp;<i class="help-tip icon-question-sign" href="#reject_ticket"></i>
             </td>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Reply-To Email');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="use_replyto_email" value="1" <?php echo $info['use_replyto_email']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Use</strong> Reply-To Email');?> <em>(<?php echo __('if available');?>)</em>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#reply_to_email"></i></em>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Ticket auto-response');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="disable_autoresponder" value="1" <?php echo $info['disable_autoresponder']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Disable</strong> auto-response.');?>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Canned Response');?>:
-            </td>
-                <td>
-                <select name="canned_response_id">
-                    <option value="">&mdash; <?php echo __('None');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
-                    if ($res=db_query($sql)) {
-                        while (list($id, $title, $isenabled)=db_fetch_row($res)) {
-                            $selected=($info['canned_response_id'] &&
-                                    $id==$info['canned_response_id'])
-                                ? 'selected="selected"' : '';
-
-                            if (!$isenabled)
-                                $title .= ' ' . __('(disabled)');
-
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                $id, $selected, $title);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<i class="help-tip icon-question-sign" href="#canned_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Department');?>:
-            </td>
-            <td>
-                <select name="dept_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    foreach (Dept::getDepartments() as $id=>$name) {
-                        $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
-                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['dept_id']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#department"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Status'); ?>:
-            </td>
-            <td>
-                <span>
-                <select name="status_id">
-                    <option value="">&mdash; <?php echo __('Default'); ?> &mdash;</option>
-                    <?php
-                    foreach (TicketStatusList::getStatuses() as $status) {
-                        $name = $status->getName();
-                        if (!($isenabled = $status->isEnabled()))
-                            $name.=' '.__('(disabled)');
-
-                        echo sprintf('<option value="%d" %s %s>%s</option>',
-                                $status->getId(),
-                                ($info['status_id'] == $status->getId())
-                                 ? 'selected="selected"' : '',
-                                 $isenabled ? '' : 'disabled="disabled"',
-                                 $name
-                                );
-                    }
-                    ?>
-                </select>
-                &nbsp;
-                <span class="error"><?php echo $errors['status_id']; ?></span>
-                <i class="help-tip icon-question-sign" href="#status"></i>
-                </span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Priority');?>:
-            </td>
-            <td>
-                <select name="priority_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['priority_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#priority"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('SLA Plan');?>:
-            </td>
-            <td>
-                <select name="sla_id">
-                    <option value="0">&mdash; <?php echo __('System Default');?> &mdash;</option>
-                    <?php
-                    if($slas=SLA::getSLAs()) {
-                        foreach($slas as $id =>$name) {
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                    $id, ($info['sla_id']==$id)?'selected="selected"':'',$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['sla_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#sla_plan"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Auto-assign To');?>:
-            </td>
-            <td>
-                <select name="assign">
-                    <option value="0">&mdash; <?php echo __('Unassigned');?> &mdash;</option>
-                    <?php
-                    if (($users=Staff::getStaffMembers())) {
-                        echo '<OPTGROUP label="'.__('Agents').'">';
-                        foreach($users as $id => $name) {
-                            $name = new PersonsName($name);
-                            $k="s$id";
-                            $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':'';
-                            ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    $sql='SELECT team_id, isenabled, name FROM '.TEAM_TABLE .' ORDER BY name';
-                    if ($teams = Team::getTeams()) {
-                        echo '<OPTGROUP label="'.__('Teams').'">';
-                        foreach ($teams as $id=>$name) {
-                            $k="t$id";
-                            $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':'';
-                            ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo
-                $errors['assign']; ?></span><i class="help-tip icon-question-sign" href="#auto_assign"></i>
+    </tbody>
+    <tbody id="dynamic-actions">
+<?php
+$existing = array();
+if ($filter) { foreach ($filter->getActions() as $A) {
+    $existing[] = $A->type;
+?>
+        <tr><td><?php echo $A->getImpl()->getName(); ?>:</td>
+            <td><div style="position:relative"><?php
+                $form = $A->getImpl()->getConfigurationForm();
+                include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+?>
+                <input type="hidden" name="actions[]" value="I<?php echo $A->getId(); ?>"/>
+                <div class="pull-right" style="position:absolute;top:2px;right:2px;">
+                    <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+        if (!confirm(__('You sure?')))
+            return false;
+        $(this).closest('td').find('input[name=\'actions[]\']')
+            .val(function(i,v) { return 'D' + v.substring(1); });
+        $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+        return false;"><i class="icon-trash"></i></a>
+                </div>
+</div>
             </td>
         </tr>
+<?php } } ?>
+    </tbody>
+    <tbody>
         <tr>
-            <td width="180">
-                <?php echo __('Help Topic'); ?>
-            </td>
+            <td><strong>
+                <?php echo __('Add'); ?>:
+            </strong></td>
             <td>
-                <select name="topic_id">
-                    <option value="0" selected="selected">&mdash; <?php
-                        echo __('Unchanged'); ?> &mdash;</option>
-                    <?php
-                    foreach (Topic::getAllHelpTopics(true) as $id=>$name) {
-                        $selected=($info['topic_id'] && $id==$info['topic_id'])?'selected="selected"':'';
-                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                    }
-                    ?>
+                <select name="new-action" id="new-action-select"
+                    onchange="javascript: $('#new-action-btn').trigger('click');">
+                    <option value=""><?php echo __('— Select an Action —'); ?></option>
+<?php foreach (FilterAction::allRegistered() as $type=>$name) {
+    if (in_array($type, $existing))
+        continue;
+?>
+                    <option data-title="<?php echo $name; ?>" value="<?php echo $type; ?>"><?php echo $name; ?></option>
+<?php } ?>
                 </select>
-                &nbsp;<span class="error"><?php echo $errors['topic_id']; ?></span><i class="help-tip icon-question-sign" href="#help_topic"></i>
+                <input id="new-action-btn" type="button" value="<?php echo __('Add'); ?>"
+                onclick="javascript:
+        var selected = $('#new-action-select').find(':selected');
+        $('#dynamic-actions')
+          .append($('<tr></tr>')
+            .append($('<td></td>')
+              .text(selected.data('title'))
+            ).append($('<td></td>')
+              .append($('<em></em>').text(__('Loading ...')))
+              .load('ajax.php/filter/action/' + selected.val() + '/config', function() {
+                selected.prop('disabled', true);
+              })
+            )
+          ).append(
+            $('<input>').attr({type:'hidden',name:'actions[]',value:'N'+selected.val()})
+          );"/>
             </td>
         </tr>
         <tr>
diff --git a/include/staff/templates/dynamic-form-simple.tmpl.php b/include/staff/templates/dynamic-form-simple.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..896805c45c48f6ace493b9786f0abd00e804cbc7
--- /dev/null
+++ b/include/staff/templates/dynamic-form-simple.tmpl.php
@@ -0,0 +1,33 @@
+        <?php
+        echo $form->getMedia();
+        foreach ($form->getFields() as $name=>$f) { ?>
+            <div class="flush-left custom-field" id="field<?php echo $f->getWidget()->id;
+                ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
+            <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>">
+            <label for="<?php echo $f->getWidget()->name; ?>">
+      <?php if ($f->get('label')) { ?>
+                <?php echo Format::htmlchars($f->get('label')); ?>:
+      <?php } ?>
+      <?php if ($f->get('required')) { ?>
+                <span class="error">*</span>
+      <?php } ?>
+            </label>
+            <?php
+            if ($f->get('hint')) { ?>
+                <br/><em style="color:gray;display:inline-block"><?php
+                    echo Format::htmlchars($f->get('hint')); ?></em>
+            <?php
+            } ?>
+            </div><div>
+            <?php
+            $f->render();
+            ?>
+            </div>
+            <?php
+            foreach ($f->errors() as $e) { ?>
+                <div class="error"><?php echo $e; ?></div>
+            <?php } ?>
+            </div>
+        <?php }
+        ?>
+    </form>
diff --git a/include/upgrader/streams/core/b26f29a6-00000000.cleanup.sql b/include/upgrader/streams/core/b26f29a6-00000000.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..d40218224f680e4852eb29ca6589d0eec15f02be
--- /dev/null
+++ b/include/upgrader/streams/core/b26f29a6-00000000.cleanup.sql
@@ -0,0 +1,11 @@
+ALTER TABLE `%TABLE_PREFIX%filter`
+  DROP `use_replyto_email`,
+  DROP `disable_autoresponder`,
+  DROP `canned_response_id`,
+  DROP `status_id`,
+  DROP `priority_id`,
+  DROP `dept_id`,
+  DROP `staff_id`,
+  DROP `team_id`,
+  DROP `sla_id`,
+  DROP `form_id`;
diff --git a/include/upgrader/streams/core/b26f29a6-00000000.patch.sql b/include/upgrader/streams/core/b26f29a6-00000000.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..821b2cbbe3b2a28762263ddd1c9bf9a8a8f91c2c
--- /dev/null
+++ b/include/upgrader/streams/core/b26f29a6-00000000.patch.sql
@@ -0,0 +1,86 @@
+/**
+ * @version v1.9.5
+ * @signature 00000000000000000000000000000000
+ * @title Add flexible filter actions
+ *
+ * This patch migrates the columnar layout of the %filter table into a new
+ * %filter_action table. The cleanup portion of the script will drop the old
+ * columns from the %filter table.
+ */
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'replyto', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `use_replyto_email` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'noresp', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `disable_autoresponder` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'canned', CONCAT('{"canned_id":',`canned_response_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `canned_response_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'dept', CONCAT('{"dept_id":',`dept_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `dept_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'pri', CONCAT('{"priority_id":',`priority_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `priority_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'sla', CONCAT('{"sla_id":',`sla_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `sla_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'team', CONCAT('{"team_id":',`team_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `team_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'agent', CONCAT('{"staff_id":',`staff_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `staff_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'topic', CONCAT('{"topic_id":',`topic_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `topic_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'status', CONCAT('{"status_id":',`status_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `status_id` != 0;
+
+-- Set new schema signature
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '00000000000000000000000000000000'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/scp/ajax.php b/scp/ajax.php
index 79dd8e1b384456df05a05373c4887f99cd7d0098..140085b3910165086b67e1e9f4d61428777569e2 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -60,6 +60,9 @@ $dispatcher = patterns('',
         url_post('^upload/(\w+)?$', 'attach'),
         url_get('^(?P<id>\d+)/fields/view$', 'getAllFields')
     )),
+    url('^/filter/', patterns('ajax.filter.php:FilterAjaxAPI',
+        url_get('^action/(?P<type>\w+)/config$', 'getFilterActionForm')
+    )),
     url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
         url_get('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'getListItemProperties'),
         url_post('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'saveListItemProperties')
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 738df56ce9521c2a087d5aa04f326b89cff14f0d..669f071e10ed72c9ab76baac3b8fd8e959b15524 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -326,6 +326,18 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   KEY `email_id` (`email_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`;
 CREATE TABLE `%TABLE_PREFIX%filter_rule` (
   `id` int(11) unsigned NOT NULL auto_increment,