diff --git a/parser/src/command.rs b/parser/src/command.rs
index fa0e23940..2072276e0 100644
--- a/parser/src/command.rs
+++ b/parser/src/command.rs
@@ -4,6 +4,7 @@ use crate::token::{Token, Tokenizer};
 
 pub mod assign;
 pub mod relabel;
+pub mod triage;
 
 pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
     input.find(&format!("@{}", bot))
@@ -13,6 +14,7 @@ pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
 pub enum Command<'a> {
     Relabel(Result<relabel::RelabelCommand, Error<'a>>),
     Assign(Result<assign::AssignCommand, Error<'a>>),
+    Triage(Result<triage::TriageCommand, Error<'a>>),
     None,
 }
 
@@ -50,33 +52,45 @@ impl<'a> Input<'a> {
 
         let original_tokenizer = tok.clone();
 
+        fn attempt_parse<'a, F, E, T>(
+            original: &Tokenizer<'a>,
+            success: &mut Vec<(Tokenizer<'a>, Command<'a>)>,
+            cmd: F,
+            transform: E,
+        ) where
+            F: Fn(&mut Tokenizer<'a>) -> Result<Option<T>, Error<'a>>,
+            E: Fn(Result<T, Error<'a>>) -> Command<'a>,
         {
-            let mut tok = original_tokenizer.clone();
-            let res = relabel::RelabelCommand::parse(&mut tok);
+            let mut tok = original.clone();
+            let res = cmd(&mut tok);
             match res {
                 Ok(None) => {}
                 Ok(Some(cmd)) => {
-                    success.push((tok, Command::Relabel(Ok(cmd))));
+                    success.push((tok, transform(Ok(cmd))));
                 }
                 Err(err) => {
-                    success.push((tok, Command::Relabel(Err(err))));
-                }
-            }
-        }
-
-        {
-            let mut tok = original_tokenizer.clone();
-            let res = assign::AssignCommand::parse(&mut tok);
-            match res {
-                Ok(None) => {}
-                Ok(Some(cmd)) => {
-                    success.push((tok, Command::Assign(Ok(cmd))));
-                }
-                Err(err) => {
-                    success.push((tok, Command::Assign(Err(err))));
+                    success.push((tok, transform(Err(err))));
                 }
             }
         }
+        attempt_parse(
+            &original_tokenizer,
+            &mut success,
+            relabel::RelabelCommand::parse,
+            Command::Relabel,
+        );
+        attempt_parse(
+            &original_tokenizer,
+            &mut success,
+            assign::AssignCommand::parse,
+            Command::Assign,
+        );
+        attempt_parse(
+            &original_tokenizer,
+            &mut success,
+            triage::TriageCommand::parse,
+            Command::Triage,
+        );
 
         if success.len() > 1 {
             panic!(
@@ -112,6 +126,7 @@ impl<'a> Command<'a> {
         match self {
             Command::Relabel(r) => r.is_ok(),
             Command::Assign(r) => r.is_ok(),
+            Command::Triage(r) => r.is_ok(),
             Command::None => true,
         }
     }
diff --git a/parser/src/command/triage.rs b/parser/src/command/triage.rs
new file mode 100644
index 000000000..d0175e9e9
--- /dev/null
+++ b/parser/src/command/triage.rs
@@ -0,0 +1,77 @@
+//! The triage command parser.
+//!
+//! Gives the priority to be changed to.
+//!
+//! The grammar is as follows:
+//!
+//! ```text
+//! Command: `@bot triage {high,medium,low,P-high,P-medium,P-low}`
+//! ```
+
+use crate::error::Error;
+use crate::token::{Token, Tokenizer};
+use std::fmt;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Priority {
+    High,
+    Low,
+    Medium,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub struct TriageCommand {
+    pub priority: Priority,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum ParseError {
+    ExpectedPriority,
+    ExpectedEnd,
+}
+
+impl std::error::Error for ParseError {}
+
+impl fmt::Display for ParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ParseError::ExpectedPriority => write!(f, "expected priority (high, medium, low)"),
+            ParseError::ExpectedEnd => write!(f, "expected end of command"),
+        }
+    }
+}
+
+impl TriageCommand {
+    pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
+        let mut toks = input.clone();
+        if let Some(Token::Word("triage")) = toks.peek_token()? {
+            toks.next_token()?;
+            let priority = match toks.peek_token()? {
+                Some(Token::Word("high")) | Some(Token::Word("P-high")) => {
+                    toks.next_token()?;
+                    Priority::High
+                }
+                Some(Token::Word("medium")) | Some(Token::Word("P-medium")) => {
+                    toks.next_token()?;
+                    Priority::Medium
+                }
+                Some(Token::Word("low")) | Some(Token::Word("P-low")) => {
+                    toks.next_token()?;
+                    Priority::Low
+                }
+                _ => {
+                    return Err(toks.error(ParseError::ExpectedPriority));
+                }
+            };
+            if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? {
+                toks.next_token()?;
+                *input = toks;
+                return Ok(Some(TriageCommand { priority: priority }));
+            } else {
+                return Err(toks.error(ParseError::ExpectedEnd));
+            }
+        } else {
+            return Ok(None);
+        }
+    }
+}
diff --git a/src/config.rs b/src/config.rs
index a60830419..c0322b2a4 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -16,6 +16,7 @@ lazy_static::lazy_static! {
 pub(crate) struct Config {
     pub(crate) relabel: Option<RelabelConfig>,
     pub(crate) assign: Option<AssignConfig>,
+    pub(crate) triage: Option<TriageConfig>,
 }
 
 #[derive(serde::Deserialize)]
@@ -31,6 +32,14 @@ pub(crate) struct RelabelConfig {
     pub(crate) allow_unauthenticated: Vec<String>,
 }
 
+#[derive(serde::Deserialize)]
+pub(crate) struct TriageConfig {
+    pub(crate) remove: Vec<String>,
+    pub(crate) high: String,
+    pub(crate) medium: String,
+    pub(crate) low: String,
+}
+
 pub(crate) fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> {
     if let Some(config) = get_cached_config(repo) {
         Ok(config)
diff --git a/src/github.rs b/src/github.rs
index 38b57c62c..650d8ad83 100644
--- a/src/github.rs
+++ b/src/github.rs
@@ -31,7 +31,7 @@ impl User {
     }
 }
 
-#[derive(Debug, Clone, serde::Deserialize)]
+#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
 pub struct Label {
     pub name: String,
 }
diff --git a/src/handlers.rs b/src/handlers.rs
index f0aa30e48..114eb22e0 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -27,6 +27,7 @@ macro_rules! handlers {
 handlers! {
     assign = assign::AssignmentHandler,
     relabel = relabel::RelabelHandler,
+    triage = triage::TriageHandler,
     //tracking_issue = tracking_issue::TrackingIssueHandler,
 }
 
diff --git a/src/handlers/triage.rs b/src/handlers/triage.rs
new file mode 100644
index 000000000..3a3f2d7e9
--- /dev/null
+++ b/src/handlers/triage.rs
@@ -0,0 +1,82 @@
+//! Triage command
+//!
+//! Removes I-nominated tag and adds priority label.
+
+use crate::{
+    config::TriageConfig,
+    github::{self, Event},
+    handlers::{Context, Handler},
+};
+use failure::Error;
+use parser::command::triage::{Priority, TriageCommand};
+use parser::command::{Command, Input};
+
+pub(super) struct TriageHandler;
+
+impl Handler for TriageHandler {
+    type Input = TriageCommand;
+    type Config = TriageConfig;
+
+    fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error> {
+        #[allow(irrefutable_let_patterns)]
+        let event = if let Event::IssueComment(e) = event {
+            e
+        } else {
+            // not interested in other events
+            return Ok(None);
+        };
+
+        let mut input = Input::new(&event.comment.body, &ctx.username);
+        match input.parse_command() {
+            Command::Triage(Ok(command)) => Ok(Some(command)),
+            Command::Triage(Err(err)) => {
+                failure::bail!(
+                    "Parsing triage command in [comment]({}) failed: {}",
+                    event.comment.html_url,
+                    err
+                );
+            }
+            _ => Ok(None),
+        }
+    }
+
+    fn handle_input(
+        &self,
+        ctx: &Context,
+        config: &TriageConfig,
+        event: &Event,
+        cmd: TriageCommand,
+    ) -> Result<(), Error> {
+        #[allow(irrefutable_let_patterns)]
+        let event = if let Event::IssueComment(e) = event {
+            e
+        } else {
+            // not interested in other events
+            return Ok(());
+        };
+
+        let is_team_member =
+            if let Err(_) | Ok(false) = event.comment.user.is_team_member(&ctx.github) {
+                false
+            } else {
+                true
+            };
+        if !is_team_member {
+            failure::bail!("Cannot use triage command as non-team-member");
+        }
+
+        let mut labels = event.issue.labels().to_owned();
+        let add = match cmd.priority {
+            Priority::High => &config.high,
+            Priority::Medium => &config.medium,
+            Priority::Low => &config.low,
+        };
+        labels.push(github::Label { name: add.clone() });
+        labels.retain(|label| !config.remove.contains(&label.name));
+        if &labels[..] != event.issue.labels() {
+            event.issue.set_labels(&ctx.github, labels)?;
+        }
+
+        Ok(())
+    }
+}