diff --git a/include/ajax.content.php b/include/ajax.content.php
index 9ea0d7d1431bdf58aac6b2ffada47acb7e4585bd..8737473570d93aae7ba826f156deb66a031e3941 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -199,5 +199,43 @@ class ContentAjaxAPI extends AjaxController {
         $errors = Format::htmlchars($errors);
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
+
+    function context() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login Required');
+        if (!$_GET['root'])
+            Http::response(400, '`root` is required parameter');
+
+        // Get the template for this template
+        $tpl_info = EmailTemplateGroup::getTemplateDescription($_GET['root']);
+        if (!$tpl_info)
+            Http::response(422, 'No such context');
+
+        $global = osTicket::getVarScope();
+
+        $contextTypes = array(
+            'assignee' => array('class' => 'Staff', 'desc' => 'Newly assigned agent'),
+            'assigner' => array('class' => 'Staff', 'desc' => 'Agent performing the assignment'),
+            'comments' => 'Agent supplied comments',
+            'message' => array('class' => 'MessageThreadEntry', 'desc' => 'Message from the EndUser'),
+            'note' => array('class' => 'NoteThreadEntry', 'desc' => 'Internal note'),
+            'poster' => array('class' => 'User', 'desc' => 'EndUser or Agent originating the message'),
+            'recipient' => array('class' => 'TicketUser', 'desc' => 'Message recipient'),
+            'response' => array('class' => 'ResponseThreadEntry', 'desc' => 'Agent reply'),
+            'signature' => 'Selected staff or department signature',
+            'staff' => array('class' => 'Staff', 'desc' => 'Agent originating the activity'),
+            'ticket' => array('class' => 'Ticket', 'desc' => 'The ticket'),
+        );
+        $context = array();
+        foreach ($tpl_info['context'] as $C) {
+            $context[$C] = $contextTypes[$C];
+        }
+        $items = VariableReplacer::compileScope($context + $global);
+
+        header('Content-Type: application/json');
+        return $this->encode($items);
+    }
 }
 ?>
diff --git a/include/class.client.php b/include/class.client.php
index 25efc2ca60132537015107d13d273b913bc13ee7..5b819e2125857909a86ae27992b67b040cf23bbf 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -16,7 +16,7 @@
 require_once INCLUDE_DIR.'class.user.php';
 
 abstract class TicketUser
-implements EmailContact, ITicketUser {
+implements EmailContact, ITicketUser, TemplateVariable {
 
     static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
 
@@ -55,6 +55,13 @@ implements EmailContact, ITicketUser {
 
     }
 
+    static function getVarScope() {
+        return array(
+            'name' => array('class' => 'PersonsName', 'desc' => __('Full name')),
+            'ticket_link' => __('Link to the ticket'),
+        );
+    }
+
     function getId() { return ($this->user) ? $this->user->getId() : null; }
     function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
 
diff --git a/include/class.company.php b/include/class.company.php
index c9aa22f04af3889ea22b564f06ffdf2dff936a71..6773bd80e83ef59e724744fd9b33663c0f269b57 100644
--- a/include/class.company.php
+++ b/include/class.company.php
@@ -17,7 +17,8 @@
 require_once(INCLUDE_DIR.'class.forms.php');
 require_once(INCLUDE_DIR.'class.dynamic_forms.php');
 
-class Company {
+class Company
+implements TemplateVariable {
     var $form;
     var $entry;
 
@@ -59,6 +60,12 @@ class Company {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return VariableReplacer::compileFormScope(
+            DynamicForm::lookup(array('type'=>'C'))
+        );
+    }
+
     function __toString() {
         try {
             if ($name = $this->getForm()->getAnswer('name'))
diff --git a/include/class.dept.php b/include/class.dept.php
index 39f914f9f401233909742e991d528c7b6551c1c3..43a98692d1af69cf0c67e2dd3d90dac30d0741ae 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -14,7 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Dept extends VerySimpleModel {
+class Dept extends VerySimpleModel
+implements TemplateVariable {
 
     static $meta = array(
         'table' => DEPT_TABLE,
@@ -71,6 +72,26 @@ class Dept extends VerySimpleModel {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'name' => 'Department name',
+            'manager' => array(
+                'class' => 'Staff', 'desc' => 'Department manager',
+                'exclude' => 'dept',
+            ),
+            'members' => array(
+                'class' => 'UserList', 'desc' => 'Department members',
+            ),
+            'parent' => array(
+                'class' => 'Dept', 'desc' => 'Parent department',
+            ),
+            'sla' => array(
+                'class' => 'SLA', 'desc' => 'Service Level Agreement',
+            ),
+            'signature' => 'Department signature',
+        );
+    }
+
     function getId() {
         return $this->id;
     }
@@ -170,7 +191,7 @@ class Dept extends VerySimpleModel {
 
             $this->_members = $members->all();
         }
-        return $this->_members;
+        return new UserList($this->_members);
     }
 
     function getAvailableMembers() {
diff --git a/include/class.list.php b/include/class.list.php
index 841ff3ea5960b4c2394b2fcd595d61026c2d697f..5a1092ea1597b94880ce749335ce01e7a8934c8d 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -14,8 +14,8 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-
 require_once(INCLUDE_DIR .'class.dynamic_forms.php');
+require_once(INCLUDE_DIR .'class.variable.php');
 
 /**
  * Interface for Custom Lists
@@ -939,7 +939,9 @@ class TicketStatusList extends CustomListHandler {
     }
 }
 
-class TicketStatus  extends VerySimpleModel implements CustomListItem {
+class TicketStatus
+extends VerySimpleModel
+implements CustomListItem, TemplateVariable {
 
     static $meta = array(
         'table' => TICKET_STATUS_TABLE,
@@ -1195,6 +1197,15 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         return $T != $tag ? $T : $default;
     }
 
+    // TemplateVariable interface
+    static function getVarScope() {
+        $base = array(
+            'name' => __('Status label'),
+            'state' => __('State name (e.g. open or closed)'),
+        );
+        return $base;
+    }
+
     function getConfiguration() {
 
         if (!$this->_settings) {
diff --git a/include/class.organization.php b/include/class.organization.php
index ffd25216f52026e82a41d0f4a9b105bd82f17a5c..16ba01293ddc54c6699d06b0e1ff1d81735120c1 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -147,7 +147,8 @@ class OrganizationCdata extends VerySimpleModel {
 }
 
 
-class Organization extends OrganizationModel {
+class Organization extends OrganizationModel
+implements TemplateVariable {
     var $_entries;
     var $_forms;
 
@@ -299,6 +300,28 @@ class Organization extends OrganizationModel {
         foreach ($this->getDynamicData() as $e)
             if ($a = $e->getAnswer($tag))
                 return $a;
+
+        switch ($tag) {
+        case 'members':
+            return new UserList($this->users);
+        case 'manager':
+            return $this->getAccountManager();
+        case 'contacts':
+            return new UserList($this->users->filter(array(
+                'flags__hasbit' => User::PRIMARY_ORG_CONTACT
+            )));
+        }
+    }
+
+    static function getVarScope() {
+        $base = array(
+            'contacts' => array('class' => 'UserList', 'desc' => 'Primary contacts'),
+            'manager' => 'Account manager',
+            'members' => array('class' => 'UserList', 'desc' => 'Organization members'),
+            'name' => 'Organization name',
+        );
+        $extra = VariableReplacer::compileFormScope(OrganizationForm::getInstance());
+        return $base + $extra;
     }
 
     function update($vars, &$errors) {
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 15500c232ddbc3920fb0c5b73848d4c9e6ebebbc..75a247c1072a7cf3143c8929a4ef46e13b228d1b 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -145,6 +145,13 @@ class osTicket {
         return $replacer->replaceVars($input);
     }
 
+    static function getVarScope() {
+        return array(
+            'url' => __("osTicket's base url (FQDN)"),
+            'company' => array('class' => 'Company', 'desc' => __('Company Information')),
+        );
+    }
+
     function addExtraHeader($header, $pjax_script=false) {
         $this->headers[md5($header)] = $header;
         $this->pjax_extra[md5($header)] = $pjax_script;
diff --git a/include/class.priority.php b/include/class.priority.php
index ac6976d29bb53517b41ee82d5dc4bf536e124bf0..68f1c39d6cc2cbab3eddf6957fcd9aed1087849d 100644
--- a/include/class.priority.php
+++ b/include/class.priority.php
@@ -14,7 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Priority extends VerySimpleModel {
+class Priority extends VerySimpleModel
+implements TemplateVariable {
 
     static $meta = array(
         'table' => PRIORITY_TABLE,
@@ -46,6 +47,14 @@ class Priority extends VerySimpleModel {
         return $this->ispublic;
     }
 
+    // TemplateVariable interface
+    function asVar() { return $this->getDesc(); }
+    static function getVarScope() {
+        return array(
+            'desc' => 'Priority description',
+        );
+    }
+
     function __toString() {
         return $this->getDesc();
     }
diff --git a/include/class.sla.php b/include/class.sla.php
index 8edb7a4360099b32506af1e563850e44887294a8..f26a399f648ffb00aeef9f0520e3e7ab3f1b992d 100644
--- a/include/class.sla.php
+++ b/include/class.sla.php
@@ -13,7 +13,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class SLA extends VerySimpleModel {
+class SLA extends VerySimpleModel
+implements TemplateVariable {
 
     static $meta = array(
         'table' => SLA_TABLE,
@@ -95,6 +96,18 @@ class SLA extends VerySimpleModel {
         return $T != $tag ? $T : $default;
     }
 
+    // TemplateVariable interface
+    function asVar() {
+        return $this->getName();
+    }
+
+    static function getVarScope() {
+        return array(
+            'name' => 'SLA Name',
+            'graceperiod' => 'Grace period (in hours)',
+        );
+    }
+
     function update($vars, &$errors) {
 
         if (!$vars['grace_period'])
diff --git a/include/class.staff.php b/include/class.staff.php
index 4a13e1b382104d893ad1aae63376667ca53ce48c..c2765915de5a401e0f297d42da121b149a047ce1 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -24,7 +24,7 @@ include_once(INCLUDE_DIR.'class.user.php');
 include_once(INCLUDE_DIR.'class.auth.php');
 
 class Staff extends VerySimpleModel
-implements AuthenticatedUser, EmailContact {
+implements AuthenticatedUser, EmailContact, TemplateVariable {
 
     static $meta = array(
         'table' => STAFF_TABLE,
@@ -78,6 +78,15 @@ implements AuthenticatedUser, EmailContact {
         return $this->__toString();
     }
 
+    static function getVarScope() {
+      return array(
+        'name' => array(
+          'class' => 'PersonsName', 'desc' => 'Name of the agent',
+        ),
+        'signature' => "Agent's signature",
+      );
+    }
+
     function getHashtable() {
         $base = $this->ht;
         $base['group'] = $base['group_id'];
diff --git a/include/class.team.php b/include/class.team.php
index 5a81170782a7225316d335b225ff3e0c2caeb622..1d4358bfab656339fa16b6e09152c38737a67c96 100644
--- a/include/class.team.php
+++ b/include/class.team.php
@@ -14,7 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Team extends VerySimpleModel {
+class Team extends VerySimpleModel
+implements TemplateVariable {
 
     static $meta = array(
         'table' => TEAM_TABLE,
@@ -42,6 +43,18 @@ class Team extends VerySimpleModel {
         return (string) $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'name' => 'Team name',
+            'lead' => array(
+                'class' => 'Staff', 'desc' => 'Team leader',
+            ),
+            'members' => array(
+                'class' => 'UserList', 'desc' => 'Team members',
+            ),
+        );
+    }
+
     function getId() {
         return $this->team_id;
     }
@@ -62,7 +75,7 @@ class Team extends VerySimpleModel {
                 $this->_members[] = $m->staff;
         }
 
-        return $this->_members;
+        return new UserList($this->_members);
     }
 
     function hasMember($staff) {
diff --git a/include/class.template.php b/include/class.template.php
index 00a81c066c486a9fe9f95c1fea17b3bae57c683e..c656e236ab6bf7d6b0b943908bd7ca13d9bff5dc 100644
--- a/include/class.template.php
+++ b/include/class.template.php
@@ -30,56 +30,108 @@ class EmailTemplateGroup {
         'ticket.autoresp'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'New Ticket Auto-response',
-            'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.'),
+            'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.',
+            'context' => array(
+                'ticket', 'signature', 'message', 'recipient'
+            ),
+        ),
         'ticket.autoreply'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'New Ticket Auto-reply',
-            'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.'),
+            'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.',
+            'context' => array(
+                'ticket', 'signature', 'response', 'recipient',
+            ),
+        ),
         'message.autoresp'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'New Message Auto-response',
-            'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.'),
+            'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.',
+            'context' => array(
+                'ticket', 'signature', 'recipient',
+            ),
+        ),
         'ticket.notice'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'New Ticket Notice',
-            'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).'),
+            'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).',
+            'context' => array(
+                'ticket', 'signature', 'recipient', 'staff', 'message',
+            ),
+        ),
         'ticket.overlimit'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'Over Limit Notice',
-            'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.'),
+            'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.',
+            'context' => array(
+                'ticket', 'signature',
+            ),
+        ),
         'ticket.reply'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'Response/Reply Template',
-            'desc'=>/* @trans */ 'Template used on ticket response/reply'),
+            'desc'=>/* @trans */ 'Template used on ticket response/reply',
+            'context' => array(
+                'ticket', 'signature', 'response', 'staff', 'poster', 'recipient',
+            ),
+        ),
         'ticket.activity.notice'=>array(
             'group'=>'ticket.user',
             'name'=>/* @trans */ 'New Activity Notice',
-            'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)'),
+            'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)',
+            'context' => array(
+                'ticket', 'signature', 'message', 'poster', 'recipient',
+            ),
+        ),
         'ticket.alert'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'New Ticket Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.'),
+            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'message',
+            ),
+        ),
         'message.alert'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'New Message Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.'),
+            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'message', 'poster',
+            ),
+        ),
         'note.alert'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'Internal Activity Alert',
-            'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.'),
+            'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'note', 'comments', 'activity',
+            ),
+        ),
         'assigned.alert'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'Ticket Assignment Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.'),
+            'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.',
+            'context' => array(
+                'ticket', 'recipient', 'comments', 'assignee', 'assigner',
+            ),
+        ),
         'transfer.alert'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'Ticket Transfer Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.'),
+            'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.',
+            'context' => array(
+                'ticket', 'recipient', 'comments', 'staff',
+            ),
+        ),
         'ticket.overdue'=>array(
             'group'=>'ticket.staff',
             'name'=>/* @trans */ 'Overdue Ticket Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.'),
-        );
+            'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.',
+            'context' => array(
+                'ticket', 'recipient', 'comments',
+            ),
+        ),
+    );
 
     function EmailTemplateGroup($id){
         $this->id=0;
@@ -157,7 +209,7 @@ class EmailTemplateGroup {
         return (db_query($sql) && db_affected_rows());
     }
 
-    function getTemplateDescription($name) {
+    static function getTemplateDescription($name) {
         return static::$all_names[$name];
     }
 
diff --git a/include/class.thread.php b/include/class.thread.php
index 3cf9308025ffed40b4ca3744dd538c16d9e4715f..b5d8bdbd4ee23b81b7767274e42c9d46120e813f 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -394,7 +394,8 @@ class ThreadEntryEmailInfo extends VerySimpleModel {
     );
 }
 
-class ThreadEntry extends VerySimpleModel {
+class ThreadEntry extends VerySimpleModel
+implements TemplateVariable {
     static $meta = array(
         'table' => THREAD_ENTRY_TABLE,
         'pk' => array('id'),
@@ -844,6 +845,7 @@ class ThreadEntry extends VerySimpleModel {
         return (string) $this->getBody();
     }
 
+    // TemplateVariable interface
     function asVar() {
         return (string) $this->getBody()->display('email');
     }
@@ -865,6 +867,22 @@ class ThreadEntry extends VerySimpleModel {
         return false;
     }
 
+    static function getVarScope() {
+        return array(
+          'body' => 'Formatted message body',
+          'create_date' => 'Date created',
+          'ip_address' => 'IP address of remote user, for web submissions',
+          'poster' => 'Name of the thread item originator',
+          'staff' => array(
+            'class' => 'Staff', 'desc' => 'Agent posting the note or response',
+          ),
+          'subject' => 'Subject of the message, if any',
+          'user' => array(
+            'class' => 'User', 'desc' => 'User posting the message',
+          ),
+        );
+    }
+
     /**
      * Parameters:
      * mailinfo (hash<String>) email header information. Must include keys
@@ -1493,6 +1511,12 @@ class MessageThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['staff']);
+        return $base;
+    }
 }
 
 /* thread entry of type response */
@@ -1533,6 +1557,12 @@ class ResponseThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['user']);
+        return $base;
+    }
 }
 
 /* Thread entry of type note (Internal Note) */
@@ -1563,10 +1593,17 @@ class NoteThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['user']);
+        return $base;
+    }
 }
 
 // Object specific thread utils.
-class ObjectThread extends Thread {
+class ObjectThread extends Thread
+implements TemplateVariable {
     private $_entries = array();
 
     static $types = array(
@@ -1694,6 +1731,13 @@ class ObjectThread extends Thread {
         }
     }
 
+    static function getVarScope() {
+      return array(
+        'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')),
+        'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')),
+      );
+    }
+
     static function lookup($criteria, $type=false) {
         if (!$type)
             return parent::lookup($criteria);
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 30a5df7fd556d85d55074691e093289992d5aeab..c13e85fbb3d6fd2d0193f7bd0f6deaedd1cd3882 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -212,7 +212,7 @@ TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';
 
 
 class Ticket
-implements RestrictedAccess, Threadable {
+implements RestrictedAccess, Threadable, TemplateVariable {
 
     var $id;
     var $number;
@@ -1797,7 +1797,7 @@ implements RestrictedAccess, Threadable {
 
     }
 
-    //ticket obj as variable = ticket number.
+    // TemplateVariable interface
     function asVar() {
        return $this->getNumber();
     }
@@ -1854,6 +1854,52 @@ implements RestrictedAccess, Threadable {
         return false;
     }
 
+    static function getVarScope() {
+        $base = array(
+            'assigned' => 'Assigned agent and/or team',
+            'close_date' => 'Date of ticket closure',
+            'create_date' => 'Ticket create date',
+            'dept' => array(
+                'class' => 'Dept', 'desc' => 'Department',
+            ),
+            'due_date' => 'Ticket due date',
+            'email' => 'Default email address of ticket owner',
+            'name' => array(
+                'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
+            ),
+            'number' => 'Ticket number',
+            'phone' => 'Phone number of ticket owner',
+            'priority' => array(
+                'class' => 'Priority', 'desc' => __('Ticket priority'),
+            ),
+            'recipients' => array(
+                'class' => 'UserList', 'desc' => 'Ticket participant list',
+            ),
+            'status' => array(
+                'class' => 'TicketStatus', 'desc' => __('Ticket status'),
+            ),
+            'staff' => array(
+                'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
+            ),
+            'subject' => 'Subject',
+            'team' => array(
+                'class' => 'Team', 'desc' => __('Assigned/closing team'),
+            ),
+            'thread' => array(
+                'class' => 'TicketThread', 'desc' => 'Ticket thread',
+            ),
+            'topic' => array(
+                'class' => 'Topic', 'desc' => 'Help topic',
+            ),
+            'user' => array(
+                'class' => 'User', 'desc' => __('Ticket owner'),
+            ),
+        );
+
+        $extra = VariableReplacer::compileFormScope(TicketForm::getInstance());
+        return $base + $extra;
+    }
+
     //Replace base variables.
     function replaceVars($input, $vars = array()) {
         global $ost;
diff --git a/include/class.topic.php b/include/class.topic.php
index 9daeb7f07dd33b1e6493d4f85dc1b0b062ce834d..c60aa2c9e2695407deb58bc226ec261cec2e7bb7 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -17,7 +17,8 @@
 require_once INCLUDE_DIR . 'class.sequence.php';
 require_once INCLUDE_DIR . 'class.filter.php';
 
-class Topic extends VerySimpleModel {
+class Topic extends VerySimpleModel
+implements TemplateVariable {
 
     static $meta = array(
         'table' => TOPIC_TABLE,
@@ -72,6 +73,22 @@ class Topic extends VerySimpleModel {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'dept' => array(
+                'class' => 'Dept', 'desc' => 'Department',
+            ),
+            'fullname' => 'Help topic full path',
+            'name' => 'Help topic name',
+            'parent' => array(
+                'class' => 'Topic', 'desc' => 'Parent help topic',
+            ),
+            'sla' => array(
+                'class' => 'SLA', 'desc' => 'Service Level Agreement',
+            ),
+        );
+    }
+
     function getId() {
         return $this->topic_id;
     }
diff --git a/include/class.user.php b/include/class.user.php
index 7cb3cf2dfb9ef1855e56c6269a1f125831c3ae4b..5c9eee9f952dd3823dbd15de24d1c11ed9f50766 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -17,6 +17,7 @@
 require_once INCLUDE_DIR . 'class.orm.php';
 require_once INCLUDE_DIR . 'class.util.php';
 require_once INCLUDE_DIR . 'class.organization.php';
+require_once INCLUDE_DIR . 'class.variable.php';
 
 class UserEmailModel extends VerySimpleModel {
     static $meta = array(
@@ -188,7 +189,8 @@ class UserCdata extends VerySimpleModel {
     }
 }
 
-class User extends UserModel {
+class User extends UserModel
+implements TemplateVariable {
 
     var $_entries;
     var $_forms;
@@ -331,6 +333,18 @@ class User extends UserModel {
                 return $a;
     }
 
+    static function getVarScope() {
+        $base = array(
+            'email' => 'Default email address',
+            'name' => array(
+                'class' => 'PersonsName', 'desc' => 'User name, default format'
+            ),
+            'organization' => array('class' => 'Organization', 'desc' => 'Organization'),
+        );
+        $extra = VariableReplacer::compileFormScope(UserForm::getInstance());
+        return $base + $extra;
+    }
+
     function addDynamicData($data) {
         return $this->addForm(UserForm::objects()->one(), 1, $data);
     }
@@ -642,7 +656,8 @@ class User extends UserModel {
     }
 }
 
-class PersonsName {
+class PersonsName
+implements TemplateVariable {
     var $format;
     var $parts;
     var $name;
@@ -761,6 +776,16 @@ class PersonsName {
         return $this->__toString();
     }
 
+    static function getVarScope() {
+        $formats = array();
+        foreach (static::$formats as $name=>$info) {
+            if (in_array($name, array('original', 'complete')))
+                continue;
+            $formats[$name] = $info[0];
+        }
+        return $formats;
+    }
+
     function __toString() {
 
         @list(, $func) = static::$formats[$this->format];
@@ -1220,9 +1245,14 @@ class UserAccountStatus {
 /*
  *  Generic user list.
  */
-class UserList extends ListObject {
+class UserList extends ListObject
+implements TemplateVariable {
 
     function __toString() {
+        return $this->getNames();
+    }
+
+    function getNames() {
 
         $list = array();
         foreach($this->storage as $user) {
@@ -1232,5 +1262,33 @@ class UserList extends ListObject {
 
         return $list ? implode(', ', $list) : '';
     }
+
+    function getFull() {
+        $list = array();
+        foreach($this->storage as $user) {
+            if (is_object($user))
+                $list[] = sprintf("%s <%s>", $user->getName(), $user->getEmail());
+        }
+
+        return $list ? implode(', ', $list) : '';
+    }
+
+    function getEmails() {
+        $list = array();
+        foreach($this->storage as $user) {
+            if (is_object($user))
+                $list[] = $user->getEmail();
+        }
+
+        return $list ? implode(', ', $list) : '';
+    }
+
+    static function getVarScope() {
+        return array(
+            'names' => 'List of names',
+            'emails' => 'List of email addresses',
+            'full' => 'List of names and email addresses',
+        );
+    }
 }
 ?>
diff --git a/include/class.variable.php b/include/class.variable.php
index ffb850ec8f264dd2e15ada81e37e90b268c0ccec..3186739ca4bc42dfc13a9712407d87d70c5a52bc 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -146,5 +146,69 @@ class VariableReplacer {
 
         return $vars;
     }
+
+    static function compileScope($scope, $recurse=5, $exclude=false) {
+        $items = array();
+        foreach ($scope as $name => $info) {
+            if ($exclude === $name)
+                continue;
+            if (isset($info['class']) && $recurse) {
+                $items[$name] = $info['desc'];
+                foreach (static::compileScope($info['class']::getVarScope(), $recurse-1,
+                    @$info['exclude'] ?: $name)
+                as $name2=>$desc) {
+                    $items["{$name}.{$name2}"] = $desc;
+                }
+            }
+            if (!is_array($info)) {
+                $items[$name] = $info;
+            }
+        }
+        return $items;
+    }
+
+    static function compileFormScope($form) {
+        $items = array();
+        foreach ($form->getFields() as $f) {
+            if (!($name = $f->get('name')))
+                continue;
+            if (!$f->isStorable() || !$f->hasData())
+                continue;
+
+            $desc = $f->getLocal('label');
+            $items[$name] = $desc;
+            foreach (VariableReplacer::compileFieldScope($f) as $name2=>$desc) {
+                $items["$name.$name2"] = $desc;
+            }
+        }
+        return $items;
+    }
+
+    static function compileFieldScope($field, $recurse=2, $exclude=false) {
+        $items = array();
+        if (!$field->hasSubFields())
+            return $items;
+
+        foreach ($field->getSubFields() as $f) {
+            if (!($name = $f->get('name')))
+                continue;
+            if ($exclude === $name)
+                continue;
+            $items[$name] = $f->getLabel();
+            if ($recurse) {
+                foreach (static::compileFieldScope($f, $recurse-1, $name)
+                as $name2=>$desc) {
+                    $items["$name.$name2"] = $desc;
+                }
+            }
+        }
+        return $items;
+    }
+}
+
+interface TemplateVariable {
+    // function asVar(); — not absolutely required
+    // function getVar($name); — not absolutely required
+    static function getVarScope();
 }
 ?>
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php
index 37e29177e80d850efb54bbd6e8a0c79c891db421..cfbbe5790896adcd70b350dede508b42c96dde1d 100644
--- a/include/staff/tpl.inc.php
+++ b/include/staff/tpl.inc.php
@@ -108,6 +108,7 @@ $tpl=$msgtemplates[$selected];
     </div>
     <input type="hidden" name="draft_id" value=""/>
     <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft"
+        data-root-context="<?php echo $selected; ?>"
         data-toolbar-external="#toolbar" class="richtext draft" <?php
     list($draft, $attrs) = Draft::getDraftAndDataAttrs('tpl.'.$selected, $tpl_id, $info['body']);
     echo $attrs; ?>><?php echo $draft ?: $info['body'];
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index bd32edda61afbc875b2aa08d3fef296f6912db39..b908a2981d5bda79fddbd5f85f9dfcdd8b4bb4d7 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -294,6 +294,9 @@ $(function() {
             options['plugins'].push('imagepaste');
             options.draftDelete = el.hasClass('draft-delete');
         }
+        if (true || 'scp') { // XXX: Add this to SCP only
+            options['plugins'].push('contexttypeahead');
+        }
         if (el.hasClass('fullscreen'))
             options['plugins'].push('fullscreen');
         if ($('#ticket_thread[data-thread-id]').length)
diff --git a/js/redactor-plugins.js b/js/redactor-plugins.js
index f41fb48e9cd82e81e93fcbedb1d19a9fe20f2b26..16da1f7e3fa98e37d23d700004fb8e85048eb16d 100644
--- a/js/redactor-plugins.js
+++ b/js/redactor-plugins.js
@@ -1606,3 +1606,155 @@ RedactorPlugins.imageannotate = function() {
     }
   };
 };
+
+RedactorPlugins.contexttypeahead = function() {
+  return {
+    typeahead: false,
+    context: false,
+
+    init: function() {
+      if (!this.$element.data('rootContext'))
+        return;
+
+      this.opts.keyupCallback = this.contexttypeahead.watch.bind(this);
+      this.opts.keydownCallback = this.contexttypeahead.watch.bind(this);
+      this.$editor.on('click', this.contexttypeahead.watch.bind(this));
+    },
+
+    watch: function(e) {
+      var current = this.selection.getCurrent(),
+          search = new RegExp(/%\{([^}]*)$/),
+          match;
+
+      if (!current)
+        return;
+
+      content = current.textContent;
+      if (e.which == 27 || !(match = search.exec(content)))
+        // No longer in a element — close typeahead
+        return this.contexttypeahead.destroy();
+
+      // Locate the position of the cursor and the number of characters back
+      // to the `%{` symbols
+      var sel         = this.selection.get(),
+          range       = this.sel.getRangeAt(0),
+          clientRects = range.getClientRects(),
+          position    = clientRects[0],
+          cursorAt    = range.endOffset,
+          backTextLen = match[1].length - content.length + cursorAt,
+          backText    = match[1].substring(0, backTextLen);
+
+      if (backTextLen < 0)
+          return this.contexttypeahead.destroy();
+
+      if (e.type == 'click')
+          return;
+
+      // Insert a hidden text input to receive the typed text and add a
+      // typeahead widget
+      if (!this.contexttypeahead.typeahead) {
+        this.contexttypeahead.typeahead = $('<input type="text">')
+          .css({position: 'absolute', visibility: 'hidden'})
+          .width(0).height(position.height)
+          .appendTo(document.body)
+          .typeahead({
+            property: 'variable',
+            minLength: 0,
+            highlighter: function(variable, item) {
+              var base = $.fn.typeahead.Constructor.prototype.highlighter.call(this, variable);
+              return base + $('<span class="faded"/>')
+                .text(' — ' + item.desc)
+                .wrap('<div>').parent().html();
+            },
+            source: this.contexttypeahead.getContext.bind(this),
+            sorter: function(items) {
+              items.sort(
+                function(a,b) {return a.variable > b.variable ? 1 : -1;}
+              );
+              return items;
+            },
+            matcher: function(item) {
+              if (item.toLowerCase().indexOf(this.query.toLowerCase()) !== 0)
+                return false;
+
+              return (this.query.match(/\./g) || []).length == (item.match(/\./g) || []).length;
+            },
+            onselect: this.contexttypeahead.select.bind(this)
+          });
+      }
+
+      var left = position.left - this.contexttypeahead.textWidth(
+            backText,
+            this.selection.getParent() || $('<div class="redactor-editor">')
+          );
+
+      this.contexttypeahead.typeahead
+        .val(match[1])
+        .trigger(e)
+        .css({top: position.top + $(window).scrollTop(), left: left});
+
+      return !e.isDefaultPrevented();
+    },
+
+    getContext: function(typeahead, query) {
+      var dfd,
+          root = this.$element.data('rootContext');
+      if (!this.contexttypeahead.context) {
+        dfd = $.Deferred();
+        $.ajax('ajax.php/content/context', {
+          data: {root: root},
+          success: function(json) {
+            var items = $.map(json, function(v,k) {
+              return {variable: k, desc: v};
+            });
+            dfd.resolve(items);
+          }
+        });
+        this.contexttypeahead.context = dfd;
+      }
+      // Only fetch the context once for this redactor box
+      this.contexttypeahead.context.then(function(items) {
+        typeahead.process(items);
+      });
+    },
+
+    textWidth: function(text, clone) {
+      var c = $(clone),
+          o = c.clone().text(text)
+            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'})
+            .css({'font-family': c.css('font-family'), 'font-weight': c.css('font-weight'),
+              'font-size': c.css('font-size')})
+            .appendTo($('body')),
+          w = o.width();
+
+      o.remove();
+
+      return w;
+    },
+
+    destroy: function() {
+      if (this.contexttypeahead.typeahead) {
+        this.contexttypeahead.typeahead.typeahead('hide');
+        this.contexttypeahead.typeahead.remove();
+        this.contexttypeahead.typeahead = false;
+      }
+      // TODO: Hide typeahead widget
+    },
+
+    select: function(item) {
+      var current = this.selection.getCurrent(),
+          search = new RegExp(/%\{([^}]*)$/);
+
+      if (!current)
+        return;
+
+      // Set cursor at the end of the expanded text
+      var q = current.textContent
+            = current.textContent.replace(search, '%{' + item.variable);
+      this.range.setStart(current, current.length);
+      this.range.setEnd(current, current.length);
+      this.selection.addRange();
+      return this.contexttypeahead.destroy();
+    }
+  };
+};
diff --git a/scp/ajax.php b/scp/ajax.php
index 7a6b95fcb3662333969698cfc58ff0527f82b5e7..f34bcfb930f912ea70e6b4592324924ec62fd4a7 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -41,6 +41,7 @@ $dispatcher = patterns('',
     )),
     url('^/content/', patterns('ajax.content.php:ContentAjaxAPI',
         url_get('^log/(?P<id>\d+)', 'log'),
+        url_get('^context$', 'context'),
         url_get('^ticket_variables', 'ticket_variables'),
         url_get('^signature/(?P<type>\w+)(?:/(?P<id>\d+))?$', 'getSignature'),
         url_get('^(?P<id>\d+)/(?:(?P<lang>\w+)/)?manage$', 'manageContent'),
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 64735d3de27b7959e24fe40d1179aa2aff0764e1..ca4e66cf37b5bce4ebaf87f5e21d8e668d9625eb 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -56,7 +56,8 @@ div#header a {
 }
 
 .faded {
-    color:#666;
+    color: #666;
+    color: rgba(0,0,0,0.5);
 }
 .faded-more {
     color: #aaa;
diff --git a/scp/css/typeahead.css b/scp/css/typeahead.css
index 981923ab1f4c200b7121f5563171101dfea0a4d2..80f67886d7ba4f3ef32f8366198efe04cb901410 100644
--- a/scp/css/typeahead.css
+++ b/scp/css/typeahead.css
@@ -56,3 +56,12 @@
   text-decoration: none;
   background-color: #0088cc;
 }
+.dropdown-menu li > a:hover .faded,
+.dropdown-menu .active > a .faded,
+.dropdown-menu .active > a:hover .faded {
+  color: rgba(255,255,255,0.6);
+}
+
+.dropdown-menu li + li {
+    border-top: 1px solid rgba(0,0,0,0.15);
+}