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.dynamic_forms.php b/include/class.dynamic_forms.php
index c1b790ccb46410923fcd11364539bd0ed057ad4a..d9914e51d230e76fe8dbd80b3512871964065c0c 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -789,6 +789,7 @@ class DynamicList extends VerySimpleModel {
     );
 
     var $_items;
+    var $_form;
 
     function getSortModes() {
         return array(
@@ -831,6 +832,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,6 +901,49 @@ class DynamicListItem extends VerySimpleModel {
         ),
     );
 
+    var $_config;
+    var $_form;
+
+    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();
+        }
+        return $this->_config;
+    }
+
+    function setConfiguration(&$errors=array()) {
+        $config = array();
+        foreach ($this->getConfigurationForm()->getFields() as $field) {
+            $config[$field->get('id')] = $field->to_database($field->getClean());
+            $errors = array_merge($errors, $field->errors());
+        }
+        if (count($errors) === 0)
+            $this->set('configuration', 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();
+        foreach ($this->getConfigurationForm()->getFields() as $field) {
+            if (strcasecmp($field->get('name'), $name) === 0)
+                return $config[$field->get('id')];
+        }
+    }
+
     function toString() {
         return $this->get('value');
     }
diff --git a/include/class.nav.php b/include/class.nav.php
index 992daf107f8f8c212d4fc5528a9912e0f5e21e1b..c744e24d141e4ec319ed1eef1bd45345bc33869d 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -100,6 +100,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');
+            // TODO: If at least one app is installed
+            $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
         }
 
         return $this->tabs;
@@ -148,6 +150,9 @@ class StaffNav {
                             $subnav[]=array('desc'=>'Canned Responses','href'=>'canned.php','iconclass'=>'canned');
                     }
                    break;
+                case 'apps':
+                    $subnav[]=array('desc'=>'Equipment', 'href'=>'apps?a=equipment','iconclass'=>'icon-bug');
+                    break;
             }
             if($subnav)
                 $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav;
diff --git a/include/class.orm.php b/include/class.orm.php
index ef04e3d3e9bb7bb2412c074e9c73e2c57d3d9157..a5035bc3edd1575a5076c918f70b235a8fb21048 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -309,6 +309,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();
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index 4beb705768cf298e1d62255a4bdc16334066642b..d05396f99732f9ed03064f867eca1778cbfdafa7 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="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="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="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="sort-new-<?php echo $i; ?>"
+                    value="<?php echo $info["sort-new-$i"]; ?>"/></td>
+            <td><input type="text" size="32" name="label-new-<?php echo $i; ?>"
+                value="<?php echo $info["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) {
@@ -77,6 +209,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             <th>Delete</th>
         </tr>
     </thead>
+
     <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?>
             class="sortable-rows" data-sort="sort-"<?php } ?>>
         <?php if ($list)
@@ -90,7 +223,18 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <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>
@@ -108,24 +252,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </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/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/lists.php b/scp/lists.php
index a68267703661f332df7f8b5f4ad4d973b59f60cd..06e52d8d5611d920f06f5ffa827a3be5396d357a 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');
@@ -35,6 +39,39 @@ if($_POST) {
                         $item->set($i, $_POST["$i-$id"]);
                 $item->save();
             }
+
+            $names = array();
+            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["$f-$id"])) {
+                        $field->set($f, $_POST["$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)
@@ -53,7 +90,6 @@ if($_POST) {
                 $msg = 'Custom list added successfully';
             else
                 $errors['err'] = 'Unable to create custom list. Unknown internal error';
-
             break;
 
         case 'mass_process':
@@ -96,6 +132,28 @@ if($_POST) {
         # Invalidate items cache
         $list->_items = false;
     }
+
+    if ($form) {
+        for ($i=0; isset($_POST["sort-new-$i"]); $i++) {
+            if (!$_POST["label-new-$i"])
+                continue;
+            $field = DynamicFormField::create(array(
+                'form_id'=>$form->get('id'),
+                'sort'=>$_POST["sort-new-$i"] ? $_POST["sort-new-$i"] : ++$max_sort,
+                'label'=>$_POST["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';