diff --git a/apps/.htaccess b/apps/.htaccess
new file mode 100644
index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60
--- /dev/null
+++ b/apps/.htaccess
@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} (.*/apps)
+
+RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L]
+
+</IfModule>
diff --git a/apps/dispatcher.php b/apps/dispatcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a3ed9d342ffea060923a11213ecd6317a62939e
--- /dev/null
+++ b/apps/dispatcher.php
@@ -0,0 +1,32 @@
+<?php
+/*********************************************************************
+    dispatcher.php
+
+    Dispatcher for client applications
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+function clientLoginPage($msg='Unauthorized') {
+    Http::response(403,'Must login: '.Format::htmlchars($msg));
+    exit;
+}
+
+require('client.inc.php');
+
+if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
+require_once INCLUDE_DIR.'/class.dispatcher.php';
+
+$dispatcher = patterns('',
+);
+
+Signal::send('ajax.client', $dispatcher);
+print $dispatcher->resolve($ost->get_path_info());
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index 1ec760b077e1c934ee13cffa568147eb22897344..b6e5d2903b3ea7a7f41afbb3c666a3656a550e05 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -50,5 +50,22 @@ class DynamicFormsAjaxAPI extends AjaxController {
 
         $ent->delete();
     }
+
+    function getListItemProperties($item_id) {
+        if (!($item = DynamicListItem::lookup($item_id)))
+            Http::response(404, 'No such list item');
+
+        include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
+    }
+
+    function saveListItemProperties($item_id) {
+        if (!($item = DynamicListItem::lookup($item_id)))
+            Http::response(404, 'No such list item');
+
+        if (!$item->setConfiguration())
+            include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
+        else
+            $item->save();
+    }
 }
 ?>
diff --git a/include/class.app.php b/include/class.app.php
new file mode 100644
index 0000000000000000000000000000000000000000..8153c32dfcc25a7ec14fe17e3dd6ea2f9c8ad9a8
--- /dev/null
+++ b/include/class.app.php
@@ -0,0 +1,52 @@
+<?php
+/*********************************************************************
+    class.app.php
+
+    Application registration system
+    Apps, usually to be distributed as plugins, can register themselves
+    using this utility class, and navigation links will be added to the
+    staff and client interfaces.
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+class Application {
+    private static $client_apps;
+    private static $staff_apps;
+    private static $admin_apps;
+
+    function registerStaffApp($desc, $href, $info=array()) {
+        self::$staff_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getStaffApps() {
+        return self::$staff_apps;
+    }
+
+    function registerClientApp($desc, $href, $info=array()) {
+        self::$client_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getClientApps() {
+        return self::$client_apps;
+    }
+
+    function registerAdminApp($desc, $href, $info=array()) {
+        self::$admin_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getAdminApps() {
+        return self::$admin_apps;
+    }
+}
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index c1b790ccb46410923fcd11364539bd0ed057ad4a..af36795fafecf615ba9ad47f98cfc4284fb15278 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -765,7 +765,13 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
     }
 
     function asVar() {
-        return $this->toString();
+        return (is_object($this->getValue()))
+            ? $this->getValue() : $this->toString();
+    }
+
+    function getVar($tag) {
+        if (is_object($this->getValue()) && method_exists($this->getValue(), 'getVar'))
+            return $this->getValue()->getVar($tag);
     }
 
     function __toString() {
@@ -789,6 +795,7 @@ class DynamicList extends VerySimpleModel {
     );
 
     var $_items;
+    var $_form;
 
     function getSortModes() {
         return array(
@@ -813,10 +820,17 @@ class DynamicList extends VerySimpleModel {
             return $this->get('name') . 's';
     }
 
+    function getAllItems() {
+         return DynamicListItem::objects()->filter(
+                array('list_id'=>$this->get('id')))
+                ->order_by($this->getListOrderBy());
+    }
+
     function getItems($limit=false, $offset=false) {
         if (!$this->_items) {
             $this->_items = DynamicListItem::objects()->filter(
-                    array('list_id'=>$this->get('id')))
+                array('list_id'=>$this->get('id'),
+                      'status__hasbit'=>DynamicListItem::ENABLED))
                 ->order_by($this->getListOrderBy());
             if ($limit)
                 $this->_items->limit($limit);
@@ -831,6 +845,14 @@ class DynamicList extends VerySimpleModel {
             ->count();
     }
 
+    function getConfigurationForm() {
+        if (!$this->_form) {
+            $this->_form = DynamicForm::lookup(
+                array('type'=>'L'.$this->get('id')));
+        }
+        return $this->_form;
+    }
+
     function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
@@ -892,10 +914,83 @@ class DynamicListItem extends VerySimpleModel {
         ),
     );
 
+    var $_config;
+    var $_form;
+
+    const ENABLED               = 0x0001;
+
+    protected function hasStatus($flag) {
+        return 0 !== ($this->get('status') & $flag);
+    }
+
+    protected function clearStatus($flag) {
+        return $this->set('status', $this->get('status') & ~$flag);
+    }
+
+    protected function setStatus($flag) {
+        return $this->set('status', $this->get('status') | $flag);
+    }
+
+    function isEnabled() {
+        return $this->hasStatus(self::ENABLED);
+    }
+
+    function enable() {
+        $this->setStatus(self::ENABLED);
+    }
+    function disable() {
+        $this->clearStatus(self::ENABLED);
+    }
+
+    function getConfiguration() {
+        if (!$this->_config) {
+            $this->_config = $this->get('properties');
+            if (is_string($this->_config))
+                $this->_config = JsonDataParser::parse($this->_config);
+            elseif (!$this->_config)
+                $this->_config = array();
+        }
+        return $this->_config;
+    }
+
+    function setConfiguration(&$errors=array()) {
+        $config = array();
+        foreach ($this->getConfigurationForm()->getFields() as $field) {
+            $val = $field->to_database($field->getClean());
+            $config[$field->get('id')] = is_array($val) ? $val[1] : $val;
+            $errors = array_merge($errors, $field->errors());
+        }
+        if (count($errors) === 0)
+            $this->set('properties', JsonDataEncoder::encode($config));
+
+        return count($errors) === 0;
+    }
+
+    function getConfigurationForm() {
+        if (!$this->_form) {
+            $this->_form = DynamicForm::lookup(
+                array('type'=>'L'.$this->get('list_id')));
+        }
+        return $this->_form;
+    }
+
+    function getVar($name) {
+        $config = $this->getConfiguration();
+        $name = mb_strtolower($name);
+        foreach ($this->getConfigurationForm()->getFields() as $field) {
+            if (mb_strtolower($field->get('name')) == $name)
+                return $config[$field->get('id')];
+        }
+    }
+
     function toString() {
         return $this->get('value');
     }
 
+    function __toString() {
+        return $this->toString();
+    }
+
     function delete() {
         # Don't really delete, just unset the list_id to un-associate it with
         # the list
@@ -986,6 +1081,10 @@ class SelectionField extends FormField {
             $this->_choices = array();
             foreach ($this->getList()->getItems() as $i)
                 $this->_choices[$i->get('id')] = $i->get('value');
+            if ($this->value && !isset($this->_choices[$this->value])) {
+                $v = DynamicListItem::lookup($this->value);
+                $this->_choices[$v->get('id')] = $v->get('value').' (Disabled)';
+            }
         }
         return $this->_choices;
     }
diff --git a/include/class.nav.php b/include/class.nav.php
index 992daf107f8f8c212d4fc5528a9912e0f5e21e1b..20246ad4ba0df9e0b7895c6d41cef932c04ecc65 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once(INCLUDE_DIR.'class.app.php');
 
 class StaffNav {
     var $tabs=array();
@@ -43,6 +44,10 @@ class StaffNav {
         return (!$this->isAdminPanel());
     }
 
+    function getRegisteredApps() {
+        return Application::getStaffApps();
+    }
+
     function setTabActive($tab, $menu=''){
 
         if($this->tabs[$tab]){
@@ -100,6 +105,8 @@ class StaffNav {
             $this->tabs['users'] = array('desc' => 'Users', 'href' => 'users.php', 'title' => 'User Directory');
             $this->tabs['tickets'] = array('desc'=>'Tickets','href'=>'tickets.php','title'=>'Ticket Queue');
             $this->tabs['kbase'] = array('desc'=>'Knowledgebase','href'=>'kb.php','title'=>'Knowledgebase');
+            if (count($this->getRegisteredApps()))
+                $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
         }
 
         return $this->tabs;
@@ -148,6 +155,10 @@ class StaffNav {
                             $subnav[]=array('desc'=>'Canned&nbsp;Responses','href'=>'canned.php','iconclass'=>'canned');
                     }
                    break;
+                case 'apps':
+                    foreach ($this->getRegisteredApps() as $app)
+                        $subnav[] = $app;
+                    break;
             }
             if($subnav)
                 $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav;
@@ -173,6 +184,10 @@ class AdminNav extends StaffNav{
         parent::StaffNav($staff, 'admin');
     }
 
+    function getRegisteredApps() {
+        return Application::getAdminApps();
+    }
+
     function getTabs(){
 
 
@@ -184,6 +199,8 @@ class AdminNav extends StaffNav{
             $tabs['manage']=array('desc'=>'Manage','href'=>'helptopics.php','title'=>'Manage Options');
             $tabs['emails']=array('desc'=>'Emails','href'=>'emails.php','title'=>'Email Settings');
             $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Manage Staff');
+            if (count($this->getRegisteredApps()))
+                $tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
             $this->tabs=$tabs;
         }
 
@@ -234,6 +251,10 @@ class AdminNav extends StaffNav{
                     $subnav[]=array('desc'=>'Groups','href'=>'groups.php','iconclass'=>'groups');
                     $subnav[]=array('desc'=>'Departments','href'=>'departments.php','iconclass'=>'departments');
                     break;
+                case 'apps':
+                    foreach ($this->getRegisteredApps() as $app)
+                        $subnav[] = $app;
+                    break;
             }
             if($subnav)
                 $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav;
@@ -258,6 +279,10 @@ class UserNav {
             $this->setActiveNav($active);
     }
 
+    function getRegisteredApps() {
+        return Application::getClientApps();
+    }
+
     function setActiveNav($nav){
 
         if($nav && $this->navs[$nav]){
diff --git a/include/class.orm.php b/include/class.orm.php
index 16bdad71130867cf7a31a4c61e2921f38a59e2bd..b76c9c292a66811657e689e46b3b430710738fe4 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -322,6 +322,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
         return $this->getIterator()->asArray();
     }
 
+    function one() {
+        $this->limit(1);
+        return $this[0];
+    }
+
     function count() {
         $class = $this->compiler;
         $compiler = new $class();
@@ -774,6 +779,7 @@ class MySqlCompiler extends SqlCompiler {
         'lte' => '%1$s <= %2$s',
         'isnull' => '%1$s IS NULL',
         'like' => '%1$s LIKE %2$s',
+        'hasbit' => '%1$s & %2$s != 0',
         'in' => array('self', '__in'),
     );
 
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 6cde950f52d494aa64ca9ec1324df15fd3fb8be5..4b7bf4c412c0b14bd8b951008d128e3aab125ea0 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1278,7 +1278,7 @@ class Ticket {
                     // The answer object is retrieved here which will
                     // automatically invoke the toString() method when the
                     // answer is coerced into text
-                    return (string)$this->_answers[$tag];
+                    return $this->_answers[$tag];
         }
 
         return false;
diff --git a/include/class.variable.php b/include/class.variable.php
index 233c9fc18696886460991b35bf3dba18701762cc..5d92709d979666f7435de2733ead2e8e1184d559 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -82,16 +82,14 @@ class VariableReplacer {
         if (!$var || !method_exists($obj, 'getVar'))
             return "";
 
-        $parts = explode('.', $var);
-        if(($rv = call_user_func(array($obj, 'getVar'), $parts[0]))===false)
+        list($tag, $remainder) = explode('.', $var, 2);
+        if(($rv = call_user_func(array($obj, 'getVar'), $tag))===false)
             return "";
 
         if(!is_object($rv))
             return $rv;
 
-        list(, $part) = explode('.', $var, 2);
-
-        return $this->getVar($rv, $part);
+        return $this->getVar($rv, $remainder);
     }
 
     function replaceVars($input) {
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index 4beb705768cf298e1d62255a4bdc16334066642b..ffeb871e62f0321b619458e12b91b39593cd590e 100644
--- a/include/staff/dynamic-list.inc.php
+++ b/include/staff/dynamic-list.inc.php
@@ -22,6 +22,17 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <input type="hidden" name="a" value="<?php echo $_REQUEST['a']; ?>">
     <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
     <h2>Custom List</h2>
+
+<ul class="tabs">
+    <li><a href="#definition" class="active">
+        <i class="icon-plus"></i> Definition</a></li>
+    <li><a href="#items">
+        <i class="icon-list"></i> Items</a></li>
+    <li><a href="#properties">
+        <i class="icon-asterisk"></i> Properties</a></li>
+</ul>
+
+<div id="definition" class="tab_content">
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
@@ -52,7 +63,128 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 </select></td>
         </tr>
     </tbody>
+    <tbody>
+        <tr>
+            <th colspan="7">
+                <em><strong>Internal Notes:</strong> be liberal, they're internal</em>
+            </th>
+        </tr>
+        <tr>
+            <td colspan="7"><textarea name="notes" class="richtext no-bar"
+                rows="6" cols="80"><?php
+                echo $info['notes']; ?></textarea>
+            </td>
+        </tr>
+    </tbody>
     </table>
+</div>
+<div id="properties" class="tab_content" style="display:none">
+    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <thead>
+        <tr>
+            <th colspan="7">
+                <em><strong>Item Properties</strong> properties definable for each item</em>
+            </th>
+        </tr>
+        <tr>
+            <th nowrap>Sort
+                <i class="help-tip icon-question-sign" href="#field_sort"></i></th>
+            <th nowrap>Label
+                <i class="help-tip icon-question-sign" href="#field_label"></i></th>
+            <th nowrap>Type
+                <i class="help-tip icon-question-sign" href="#field_type"></i></th>
+            <th nowrap>Variable
+                <i class="help-tip icon-question-sign" href="#field_variable"></i></th>
+            <th nowrap>Delete
+                <i class="help-tip icon-question-sign" href="#field_delete"></i></th>
+        </tr>
+    </thead>
+    <tbody class="sortable-rows" data-sort="prop-sort-">
+    <?php if ($form) foreach ($form->getDynamicFields() as $f) {
+        $id = $f->get('id');
+        $deletable = !$f->isDeletable() ? 'disabled="disabled"' : '';
+        $force_name = $f->isNameForced() ? 'disabled="disabled"' : '';
+        $fi = $f->getImpl();
+        $ferrors = $f->errors(); ?>
+        <tr>
+            <td><i class="icon-sort"></i></td>
+            <td><input type="text" size="32" name="prop-label-<?php echo $id; ?>"
+                value="<?php echo Format::htmlchars($f->get('label')); ?>"/>
+                <font class="error"><?php
+                    if ($ferrors['label']) echo '<br/>'; echo $ferrors['label']; ?>
+            </td>
+            <td nowrap><select name="type-<?php echo $id; ?>" <?php
+                if (!$fi->isChangeable()) echo 'disabled="disabled"'; ?>>
+                <?php foreach (FormField::allTypes() as $group=>$types) {
+                        ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php
+                        foreach ($types as $type=>$nfo) {
+                            if ($f->get('type') != $type
+                                    && isset($nfo[2]) && !$nfo[2]) continue; ?>
+                <option value="<?php echo $type; ?>" <?php
+                    if ($f->get('type') == $type) echo 'selected="selected"'; ?>>
+                    <?php echo $nfo[0]; ?></option>
+                    <?php } ?>
+                </optgroup>
+                <?php } ?>
+            </select>
+            <?php if ($f->isConfigurable()) { ?>
+                <a class="action-button" style="float:none;overflow:inherit"
+                    href="ajax.php/form/field-config/<?php
+                        echo $f->get('id'); ?>"
+                    onclick="javascript:
+                        $('#overlay').show();
+                        $('#field-config .body').load(this.href);
+                        $('#field-config').show();
+                        return false;
+                    "><i class="icon-edit"></i> Config</a>
+            <?php } ?></td>
+            <td>
+                <input type="text" size="20" name="name-<?php echo $id; ?>"
+                    value="<?php echo Format::htmlchars($f->get('name'));
+                    ?>" <?php echo $force_name ?>/>
+                <font class="error"><?php
+                    if ($ferrors['name']) echo '<br/>'; echo $ferrors['name'];
+                ?></font>
+                </td>
+            <td><input type="checkbox" name="delete-<?php echo $id; ?>"
+                    <?php echo $deletable; ?>/>
+                <input type="hidden" name="prop-sort-<?php echo $id; ?>"
+                    value="<?php echo $f->get('sort'); ?>"/>
+                </td>
+        </tr>
+    <?php
+    }
+    for ($i=0; $i<$newcount; $i++) { ?>
+            <td><em>+</em>
+                <input type="hidden" name="prop-sort-new-<?php echo $i; ?>"
+                    value="<?php echo $info["prop-sort-new-$i"]; ?>"/></td>
+            <td><input type="text" size="32" name="prop-label-new-<?php echo $i; ?>"
+                value="<?php echo $info["prop-label-new-$i"]; ?>"/></td>
+            <td><select name="type-new-<?php echo $i; ?>">
+                <?php foreach (FormField::allTypes() as $group=>$types) {
+                    ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php
+                    foreach ($types as $type=>$nfo) {
+                        if (isset($nfo[2]) && !$nfo[2]) continue; ?>
+                <option value="<?php echo $type; ?>"
+                    <?php if ($info["type-new-$i"] == $type) echo 'selected="selected"'; ?>>
+                    <?php echo $nfo[0]; ?>
+                </option>
+                    <?php } ?>
+                </optgroup>
+                <?php } ?>
+            </select></td>
+            <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"
+                value="<?php echo $info["name-new-$i"]; ?>"/>
+                <font class="error"><?php
+                    if ($errors["new-$i"]['name']) echo '<br/>'; echo $errors["new-$i"]['name'];
+                ?></font>
+            <td></td>
+        </tr>
+    <?php } ?>
+    </tbody>
+</table>
+</div>
+<div id="items" class="tab_content" style="display:none">
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
     <?php if ($list) {
@@ -66,7 +198,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         else $showing = 'Add a few initial items to the list';
     ?>
         <tr>
-            <th colspan="4">
+            <th colspan="5">
                 <em><?php echo $showing; ?></em>
             </th>
         </tr>
@@ -74,25 +206,41 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             <th></th>
             <th>Value</th>
             <th>Extra <em style="display:inline">&mdash; abbreviations and such</em></th>
+            <th>Disabled</th>
             <th>Delete</th>
         </tr>
     </thead>
+
     <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?>
             class="sortable-rows" data-sort="sort-"<?php } ?>>
         <?php if ($list)
         $icon = ($info['sort_mode'] == 'SortCol')
             ? '<i class="icon-sort"></i>&nbsp;' : '';
         if ($list) {
-        foreach ($list->getItems() as $i) {
+        foreach ($list->getAllItems() as $i) {
             $id = $i->get('id'); ?>
-        <tr>
+        <tr class="<?php if (!$i->isEnabled()) echo 'disabled'; ?>">
             <td><?php echo $icon; ?>
                 <input type="hidden" name="sort-<?php echo $id; ?>"
                 value="<?php echo $i->get('sort'); ?>"/></td>
             <td><input type="text" size="40" name="value-<?php echo $id; ?>"
-                value="<?php echo $i->get('value'); ?>"/></td>
+                value="<?php echo $i->get('value'); ?>"/>
+                <?php if ($form->getFields()) { ?>
+                <a class="action-button" style="float:none;overflow:inherit"
+                    href="ajax.php/list/item/<?php
+                        echo $i->get('id'); ?>/properties"
+                    onclick="javascript:
+                        $('#overlay').show();
+                        $('#field-config .body').load(this.href);
+                        $('#field-config').show();
+                        return false;
+                    "><i class="icon-edit"></i> Properties</a>
+                <?php } ?></td>
             <td><input type="text" size="30" name="extra-<?php echo $id; ?>"
                 value="<?php echo $i->get('extra'); ?>"/></td>
+            <td>
+                <input type="checkbox" name="disable-<?php echo $id; ?>" <?php
+                if (!$i->isEnabled()) echo 'checked="checked"'; ?>/></td>
             <td>
                 <input type="checkbox" name="delete-<?php echo $id; ?>"/></td>
         </tr>
@@ -105,27 +253,19 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td>
             <td><input type="text" size="30" name="extra-new-<?php echo $i; ?>"/></td>
             <td></td>
+            <td></td>
         </tr>
     <?php } ?>
     </tbody>
-    <tbody>
-        <tr>
-            <th colspan="7">
-                <em><strong>Internal Notes:</strong> be liberal, they're internal</em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan="7"><textarea name="notes" class="richtext no-bar"
-                rows="6" cols="80"><?php
-                echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
-    </tbody>
-    </table>
     </table>
+</div>
 <p class="centered">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="Reset">
     <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'>
 </p>
 </form>
+
+<div style="display:none;" class="dialog draggable" id="field-config">
+    <div class="body"></div>
+</div>
diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a2ba9635df68b0b1a2b61795b1a4bb89e3e6e8e
--- /dev/null
+++ b/include/staff/templates/list-item-properties.tmpl.php
@@ -0,0 +1,68 @@
+    <h3>Item Properties &mdash; <?php echo $item->get('value') ?></h3>
+    <a class="close" href=""><i class="icon-remove-circle"></i></a>
+    <hr/>
+    <form method="post" action="ajax.php/list/item/<?php
+            echo $item->get('id'); ?>/properties" onsubmit="javascript:
+            var form = $(this);
+            $.post(this.action, form.serialize(), function(data, status, xhr) {
+                if (!data.length) {
+                    form.closest('.dialog').hide();
+                    $('#overlay').hide();
+                } else {
+                    form.closest('.dialog').empty().append(data);
+                }
+            });
+            return false;">
+        <table width="100%">
+        <?php
+        echo csrf_token();
+        $config = $item->getConfiguration();
+        foreach ($item->getConfigurationForm()->getFields() as $f) {
+            $name = $f->get('id');
+            if (isset($config[$name]))
+                $f->value = $config[$name];
+            else if ($f->get('default'))
+                $f->value = $f->get('default');
+            ?>
+            <tr><td class="multi-line">
+            <label for="<?php echo $f->getWidget()->name; ?>"
+                style="vertical-align:top;padding-top:0.2em">
+                <?php echo Format::htmlchars($f->get('label')); ?>:</label>
+            </td><td>
+            <span style="display:inline-block">
+            <?php
+            $f->render();
+            if ($f->get('required')) { ?>
+                <font class="error">*</font>
+            <?php
+            }
+            if ($f->get('hint')) { ?>
+                <br /><em style="color:gray;display:inline-block"><?php
+                    echo Format::htmlchars($f->get('hint')); ?></em>
+            <?php
+            }
+            ?>
+            </span>
+            <?php
+            foreach ($f->errors() as $e) { ?>
+                <br />
+                <font class="error"><?php echo $e; ?></font>
+            <?php } ?>
+            </td></tr>
+            <?php
+        }
+        ?>
+        </table>
+        <hr>
+        <p class="full-width">
+            <span class="buttons" style="float:left">
+                <input type="reset" value="Reset">
+                <input type="button" value="Cancel" class="close">
+            </span>
+            <span class="buttons" style="float:right">
+                <input type="submit" value="Save">
+            </span>
+         </p>
+    </form>
+    <div class="clear"></div>
+
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 19fc2a074f6659a91cc9a83bc9ce83334660c472..267cb8e334b83f11d55b2e6341254bc297b0f766 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-4323a6a81c35efbf7722b7fc4e475440
+9ef33a062ca3a20190dfad594d594a69
diff --git a/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql b/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9867ef23cb3e7a72b141aa6d56276b3a8d2d8c2d
--- /dev/null
+++ b/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql
@@ -0,0 +1,41 @@
+/**
+ * @version v1.8.2
+ * @signature 9ef33a062ca3a20190dfad594d594a69
+ * @title Add organization features
+ *
+ */
+
+ALTER TABLE `%TABLE_PREFIX%form`
+    CHANGE `type` `type` varchar(8) NOT NULL DEFAULT 'G';
+
+ALTER TABLE `%TABLE_PREFIX%list_items`
+    ADD `status` int(11) unsigned NOT NULL DEFAULT 1 AFTER `list_id`,
+    ADD `properties` text AFTER `sort`;
+
+ALTER TABLE `%TABLE_PREFIX%organization`
+    ADD `status` int(11) NOT NULL DEFAULT 0 AFTER `staff_id`,
+    ADD `domain` varchar(128) NOT NULL DEFAULT '' AFTER `status`,
+    ADD `extra` text AFTER `domain`;
+
+ALTER TABLE `%TABLE_PREFIX%filter`
+    ADD `status` int(11) unsigned NOT NULL DEFAULT '0' AFTER `isactive`,
+    ADD `ext_id` varchar(11) AFTER `topic_id`;
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%note`;
+CREATE TABLE `%TABLE_PREFIX%note` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(11) unsigned,
+  `staff_id` int(11) unsigned NOT NULL DEFAULT 0,
+  `ext_id` varchar(10),
+  `body` text,
+  `status` int(11) unsigned NOT NULL DEFAULT 0,
+  `sort` int(11) unsigned NOT NULL DEFAULT 0,
+  `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '9ef33a062ca3a20190dfad594d594a69'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/scp/ajax.php b/scp/ajax.php
index 5d12a8b43ebc215fce392f197b203bf842cfd1ec..65ce1c01143df2976f82c3ad2b21f97285af15f4 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -56,6 +56,10 @@ $dispatcher = patterns('',
         url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'),
         url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer')
     )),
+    url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
+        url_get('^item/(?P<id>\d+)/properties$', 'getListItemProperties'),
+        url_post('^item/(?P<id>\d+)/properties$', 'saveListItemProperties')
+    )),
     url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI',
         # Send
         url_get('^graph$', 'getPlotData'),
diff --git a/scp/apps/.htaccess b/scp/apps/.htaccess
new file mode 100644
index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60
--- /dev/null
+++ b/scp/apps/.htaccess
@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} (.*/apps)
+
+RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L]
+
+</IfModule>
diff --git a/scp/apps/dispatcher.php b/scp/apps/dispatcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1cf05fd81fc6a1b8fcf11e71346338e5eb8b161
--- /dev/null
+++ b/scp/apps/dispatcher.php
@@ -0,0 +1,41 @@
+<?php
+/*********************************************************************
+    dispatcher.php
+
+    Dispatcher for staff applications
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+# Override staffLoginPage() defined in staff.inc.php to return an
+# HTTP/Forbidden status rather than the actual login page.
+# XXX: This should be moved to the AjaxController class
+function staffLoginPage($msg='Unauthorized') {
+    Http::response(403,'Must login: '.Format::htmlchars($msg));
+    exit;
+}
+
+require('staff.inc.php');
+
+//Clean house...don't let the world see your crap.
+ini_set('display_errors','0'); //Disable error display
+ini_set('display_startup_errors','0');
+
+//TODO: disable direct access via the browser? i,e All request must have REFER?
+if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
+
+require_once INCLUDE_DIR.'/class.dispatcher.php';
+$dispatcher = patterns('',
+);
+
+Signal::send('apps.scp', $dispatcher);
+
+# Call the respective function
+print $dispatcher->resolve($ost->get_path_info());
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 55b706178234fb2d8a20134cbb55f35553176d2c..c73e107cf7fa7b7d8c54417541f7490909acc36a 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1629,3 +1629,9 @@ div.selected-signature .inner {
     background: #fc9f41; /* Old browsers */
     color: rgba(255,255,255,0.8) !important;
 }
+
+tr.disabled td,
+tr.disabled th {
+    opacity: 0.6;
+    background: #f5f5f5;
+}
diff --git a/scp/lists.php b/scp/lists.php
index a68267703661f332df7f8b5f4ad4d973b59f60cd..17f36d18bb2fbe7ca6df5615e50071967e1e03f4 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -6,6 +6,10 @@ $list=null;
 if($_REQUEST['id'] && !($list=DynamicList::lookup($_REQUEST['id'])))
     $errors['err']='Unknown or invalid dynamic list ID.';
 
+if ($list) {
+    $form = DynamicForm::lookup(array('type'=>'L'.$_REQUEST['id']));
+}
+
 if($_POST) {
     $fields = array('name', 'name_plural', 'sort_mode', 'notes');
     $required = array('name');
@@ -24,7 +28,7 @@ if($_POST) {
             else
                 $errors['err'] = 'Unable to update custom list. Unknown internal error';
 
-            foreach ($list->getItems() as $item) {
+            foreach ($list->getAllItems() as $item) {
                 $id = $item->get('id');
                 if ($_POST["delete-$id"] == 'on') {
                     $item->delete();
@@ -33,8 +37,54 @@ if($_POST) {
                 foreach (array('sort','value','extra') as $i)
                     if (isset($_POST["$i-$id"]))
                         $item->set($i, $_POST["$i-$id"]);
+
+                if ($_POST["disable-$id"] == 'on')
+                    $item->disable();
+                else
+                    $item->enable();
+
                 $item->save();
             }
+
+            $names = array();
+            if (!$form) {
+                $form = DynamicForm::create(array(
+                    'type'=>'L'.$_REQUEST['id'],
+                    'title'=>$_POST['name'] . ' Properties'
+                ));
+                $form->save(true);
+            }
+            foreach ($form->getDynamicFields() as $field) {
+                $id = $field->get('id');
+                if ($_POST["delete-$id"] == 'on' && $field->isDeletable()) {
+                    $field->delete();
+                    // Don't bother updating the field
+                    continue;
+                }
+                if (isset($_POST["type-$id"]) && $field->isChangeable())
+                    $field->set('type', $_POST["type-$id"]);
+                if (isset($_POST["name-$id"]) && !$field->isNameForced())
+                    $field->set('name', $_POST["name-$id"]);
+                # TODO: make sure all help topics still have all required fields
+                foreach (array('sort','label') as $f) {
+                    if (isset($_POST["prop-$f-$id"])) {
+                        $field->set($f, $_POST["prop-$f-$id"]);
+                    }
+                }
+                if (in_array($field->get('name'), $names))
+                    $field->addError('Field variable name is not unique', 'name');
+                if (preg_match('/[.{}\'"`; ]/u', $field->get('name')))
+                    $field->addError('Invalid character in variable name. Please use letters and numbers only.', 'name');
+                if ($field->get('name'))
+                    $names[] = $field->get('name');
+                if ($field->isValid())
+                    $field->save();
+                else
+                    # notrans (not shown)
+                    $errors["field-$id"] = 'Field has validation errors';
+                // Keep track of the last sort number
+                $max_sort = max($max_sort, $field->get('sort'));
+            }
             break;
         case 'add':
             foreach ($fields as $f)
@@ -47,13 +97,20 @@ if($_POST) {
                 'sort_mode'=>$_POST['sort_mode'],
                 'notes'=>$_POST['notes']));
 
+            $form = DynamicForm::create(array(
+                'title'=>$_POST['name'] . ' Properties'
+            ));
+
             if ($errors)
                 $errors['err'] = 'Unable to create custom list. Correct any error(s) below and try again.';
-            elseif ($list->save(true))
-                $msg = 'Custom list added successfully';
-            else
-                $errors['err'] = 'Unable to create custom list. Unknown internal error';
+            elseif (!$list->save(true))
+                $errors['err'] = 'Unable to create custom list: Unknown internal error';
 
+            $form->set('type', 'L'.$list->get('id'));
+            if (!$errors && !$form->save(true))
+                $errors['err'] = 'Unable to create properties for custom list: Unknown internal error';
+            else
+                $msg = 'Custom list added successfully';
             break;
 
         case 'mass_process':
@@ -82,7 +139,7 @@ if($_POST) {
     }
 
     if ($list) {
-        for ($i=0; isset($_POST["sort-new-$i"]); $i++) {
+        for ($i=0; isset($_POST["prop-sort-new-$i"]); $i++) {
             if (!$_POST["value-new-$i"])
                 continue;
             $item = DynamicListItem::create(array(
@@ -96,6 +153,29 @@ if($_POST) {
         # Invalidate items cache
         $list->_items = false;
     }
+
+    if ($form) {
+        for ($i=0; isset($_POST["prop-sort-new-$i"]); $i++) {
+            if (!$_POST["prop-label-new-$i"])
+                continue;
+            $field = DynamicFormField::create(array(
+                'form_id'=>$form->get('id'),
+                'sort'=>$_POST["prop-sort-new-$i"]
+                    ? $_POST["prop-sort-new-$i"] : ++$max_sort,
+                'label'=>$_POST["prop-label-new-$i"],
+                'type'=>$_POST["type-new-$i"],
+                'name'=>$_POST["name-new-$i"],
+            ));
+            $field->setForm($form);
+            if ($field->isValid())
+                $field->save();
+            else
+                $errors["new-$i"] = $field->errors();
+        }
+        // XXX: Move to an instrumented list that can handle this better
+        if (!$errors)
+            $form->_dfields = $form->_fields = null;
+    }
 }
 
 $page='dynamic-lists.inc.php';
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index c15a946a6aa97c5e965a1ebebe6667f36b46b681..acd48f9bb4cc395860fb93293a6b6e470d2ed2e8 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -96,7 +96,7 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES
 DROP TABLE IF EXISTS `%TABLE_PREFIX%form`;
 CREATE TABLE `%TABLE_PREFIX%form` (
     `id` int(11) unsigned NOT NULL auto_increment,
-    `type` char(1) NOT NULL DEFAULT 'G',
+    `type` varchar(8) NOT NULL DEFAULT 'G',
     `deletable` tinyint(1) NOT NULL DEFAULT 1,
     `title` varchar(255) NOT NULL,
     `instructions` varchar(512),
@@ -163,10 +163,12 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%list_items`;
 CREATE TABLE `%TABLE_PREFIX%list_items` (
     `id` int(11) unsigned NOT NULL auto_increment,
     `list_id` int(11),
+    `status` int(11) unsigned NOT NULL DEFAULT 1,
     `value` varchar(255) NOT NULL,
     -- extra value such as abbreviation
     `extra` varchar(255),
     `sort` int(11) NOT NULL DEFAULT 1,
+    `properties` text,
     PRIMARY KEY (`id`),
     KEY `list_item_lookup` (`list_id`)
 ) DEFAULT CHARSET=utf8;
@@ -268,6 +270,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   `id` int(11) unsigned NOT NULL auto_increment,
   `execorder` int(10) unsigned NOT NULL default '99',
   `isactive` tinyint(1) unsigned NOT NULL default '1',
+  `status` int(11) unsigned NOT NULL DEFAULT '0',
   `match_all_rules` tinyint(1) unsigned NOT NULL default '0',
   `stop_onmatch` tinyint(1) unsigned NOT NULL default '0',
   `reject_ticket` tinyint(1) unsigned NOT NULL default '0',
@@ -282,6 +285,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   `sla_id` int(10) unsigned NOT NULL default '0',
   `form_id` int(11) unsigned NOT NULL default '0',
   `topic_id` int(11) unsigned NOT NULL default '0',
+  `ext_id` varchar(11),
   `target` ENUM(  'Any',  'Web',  'Email',  'API' ) NOT NULL DEFAULT  'Any',
   `name` varchar(32) NOT NULL default '',
   `notes` text,
@@ -426,6 +430,9 @@ CREATE TABLE `%TABLE_PREFIX%organization` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `name` varchar(128) NOT NULL DEFAULT '',
   `staff_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `status` int(11) unsigned NOT NULL DEFAULT '0',
+  `domain` varchar(128) NOT NULL DEFAULT '',
+  `extra` text,
   `created` timestamp NULL DEFAULT NULL,
   `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
@@ -448,6 +455,20 @@ CREATE TABLE `%TABLE_PREFIX%canned_response` (
   KEY `active` (`isenabled`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%note`;
+CREATE TABLE `%TABLE_PREFIX%note` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(11) unsigned,
+  `staff_id` int(11) unsigned NOT NULL DEFAULT 0,
+  `ext_id` varchar(10),
+  `body` text,
+  `status` int(11) unsigned NOT NULL DEFAULT 0,
+  `sort` int(11) unsigned NOT NULL DEFAULT 0,
+  `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%session`;
 CREATE TABLE `%TABLE_PREFIX%session` (
   `session_id` varchar(255) collate ascii_general_ci NOT NULL default '',
diff --git a/web.config b/web.config
index fd61b6a95f6776289e5b1fd85660f05ceec5174f..ee754443fd21e93c111e2cd962fd019ded871c30 100644
--- a/web.config
+++ b/web.config
@@ -34,6 +34,16 @@
                     </conditions>
                     <action type="Rewrite" url="{R:1}pages/index.php/{R:2}"/>
                 </rule>
+                <rule name="Staff applications" stopProcessing="true">
+                    <match url="^(.*/)?scp/apps/(.*)$" ignoreCase="true"/>
+                    <conditions>
+                        <add input="{REQUEST_FILENAME}" matchType="IsFile"
+                            ignoreCase="false" negate="true" />
+                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory"
+                            ignoreCase="false" negate="true" />
+                    </conditions>
+                    <action type="Rewrite" url="{R:1}scp/apps/dispatcher.php/{R:2}"/>
+                </rule>
             </rules>
         </rewrite>
         <defaultDocument>