diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,63 +1,52 @@
 # cmt
 
-Write consistent git commit messages
+Write consistent git commit messages based on a custom template.
 
-## Install
+Similar idea to [commitizen](https://github.com/commitizen/cz-cli), but with an emphasis on making it easy to define a custom commit style.
 
-[Binaries for Mac and Linux are available](https://github.com/smallhadroncollider/cmt/releases). Add the binary to a directory in your path (such as `/usr/local/bin`).
+- [Concept](#Concept)
+- [Format](#Format)
+- [Usage](#Usage)
+- [Install](#Install)
 
-### Cabal
+![Demo](https://files.smallhadroncollider.com/cmt-0.4.gif)
 
-**Requirements**: [Cabal](https://www.haskell.org/cabal/)
 
-```bash
-cabal install cmt
-```
-
-Make sure you run `cabal update` if you haven't run it recently.
-
-### Building
+## Concept
 
-**Requirements**: [Stack](https://docs.haskellstack.org/en/stable/README/)
+It's important to write consistent commit messages, but depending on the project you may well want to use different commit styles.
 
-The following command will build cmt and then install it in `~/.local/bin`:
+With `cmt` you create a `.cmt` file in your project directory. The `.cmt` file enforces a particular style of commit message for that project. You can also add predefined commit messages for things like version bumps and updating the readme.
 
-```bash
-stack build && stack install
-```
+For example, for my programming projects I try to use a commit style similar to the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153). However, this isn't appropriate for my teaching notes repos or for my capistrano build repos.
 
 
-## Usage
-
-Add a `.cmt` file to your project directory.
-
-```bash
-cmt # will show the options and then commit
-```
+## Format
 
-If you're using the `${*}` format option then:
+A `.cmt` file consist of two parts: the input parts and the output format.
 
-```bash
-cmt "blah blah blah" # this will go in ${*} place
-```
+A basic `.cmt` file to include a subject and body would look like:
 
-If there are commit message you use frequently (such as "version bump"), you can setup predefined messages with aliases:
+```txt
+# The input parts
+{
+    "Subject" = @ # Single line input
+    "Body" = !@ # Multi-line input
+}
 
-```bash
-cmt -p vb # use the version bump message
-```
+# predefined commit messages
+# this section is optional
+{
+    vb = "version bump"
+}
 
-If the commit returns with a non-zero status code, your previous commit message is stored in a `.cmt.bkp` file. You can re-run the commit when you're ready with:
+# The output format
+${Subject}
 
-```bash
-cmt --prev
+${Body}
 ```
 
-### Format
-
-A `.cmt` file consist of two parts: the input parts and the output format.
-
-For example, the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153):
+A more complex example, the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153):
 
 ```txt
 # The input parts
@@ -72,16 +61,17 @@
         "test",
         "chore"
     ]
-    "Scope" = % # Select from a list of staged files
-    "Subject" = @ # Single line input
+    "Scope" = @ # Single line input
+    "Subject" = @
     "Body" = !@ # Multi-line input
     "Footer" = !@
 }
 
 # predefined messages
+# this section is optional
 {
-    vb = "chore (package.yaml): version bump"
-    readme = "docs (README.md): updated readme"
+    vb = "chore: version bump"
+    readme = "docs: updated readme"
 }
 
 # The output format
@@ -94,9 +84,17 @@
 ${Footer}
 ```
 
+For my capistrano build repos the `.cmt` file is simply:
 
-#### Input Parts
+```txt
+{}
 
+"latest build"
+```
+
+
+### Input Parts
+
 These are at the top of the `.cmt` file and surrounded by opening and closing curly braces. A consist of a name and a type:
 
 - `@`: single line input
@@ -104,12 +102,40 @@
 - `%`: select from a list of staged files
 - `["option 1", "option 2"]`: list of options
 
-#### Output Format
+### Predefined Messages
 
-The output format consists of named input parts plus anything else you want.
+The predefined messages section is optional. You can provide a list of names and messages and then use the `-p <name>` command-line argument to use one of them.
 
-You can accept a output called `${*}`, which will add in whatever is passed to `cmt` as command line arguments.
+For example, with the following config, `cmt -p vb` would use the message "version bump".
 
+```txt
+vb = "version bump"
+```
+
+Predefined messages can also use any input parts defined in the prior section. An example of this would be:
+
+```txt
+{
+    "Project" = [
+        "ghc",
+        "cabal"
+    ]
+}
+{
+    vb = "${Project}: version bump"
+}
+```
+
+Running `cmt -p vb` will now prompt you to select which project is getting version bumped.
+
+### Output Format
+
+The output format consists of named input parts (`${<name>}`) plus anything else you want.
+
+#### Wildcard Output
+
+You can accept an output called `${*}`, which will add in whatever is passed to `cmt` as command-line arguments.
+
 For example:
 
 ```txt
@@ -127,4 +153,64 @@
 
 ```bash
 cmt "Blah blah blah"
+```
+
+
+## Usage
+
+Add a `.cmt` file to your project directory.
+
+```bash
+cmt # will show the options and then commit
+```
+
+`cmt` will also look in your home directory if a `.cmt` file isn't found in the project directory hierarchy. This can be used to define a global commit style, which you can then override on a per-project basis.
+
+### Predefined Messages
+
+If there are commit message you use frequently (such as "version bump"), you can setup predefined messages with aliases:
+
+```bash
+cmt -p vb # use the version bump message
+```
+
+### Wildcard Output
+
+If you're using the `${*}` format option then:
+
+```bash
+cmt "blah blah blah" # this will go in ${*} place
+```
+
+### Re-run Failed Commits
+
+If the commit returns with a non-zero status code, your previous commit message is stored in a `.cmt.bkp` file. You can re-run the commit when you're ready with:
+
+```bash
+cmt --prev
+```
+
+
+## Install
+
+[Binaries for Mac and Linux are available](https://github.com/smallhadroncollider/cmt/releases). Add the binary to a directory in your path (such as `/usr/local/bin`).
+
+### Cabal
+
+**Requirements**: [Cabal](https://www.haskell.org/cabal/)
+
+```bash
+cabal install cmt
+```
+
+Make sure you run `cabal update` if you haven't run it recently.
+
+### Building
+
+**Requirements**: [Stack](https://docs.haskellstack.org/en/stable/README/)
+
+The following command will build cmt and then install it in `~/.local/bin`:
+
+```bash
+stack build && stack install
 ```
diff --git a/cmt.cabal b/cmt.cabal
--- a/cmt.cabal
+++ b/cmt.cabal
@@ -1,6 +1,6 @@
 cabal-version: 1.12
 name: cmt
-version: 0.4.0.0
+version: 0.5.0.0
 license: BSD3
 license-file: LICENSE
 copyright: Small Hadron Collider / Mark Wales
@@ -43,9 +43,10 @@
         attoparsec >=0.13.2.2 && <0.14,
         base >=4.7 && <5,
         classy-prelude >=1.5.0 && <1.6,
+        containers >=0.6.0.1 && <0.7,
         directory >=1.3.3.0 && <1.4,
         filepath >=1.4.2.1 && <1.5,
-        process >=1.6.3.0 && <1.7,
+        process >=1.6.5.0 && <1.7,
         terminal-size >=0.3.2.1 && <0.4,
         text >=1.2.3.1 && <1.3
 
diff --git a/src/Cmt.hs b/src/Cmt.hs
--- a/src/Cmt.hs
+++ b/src/Cmt.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt
     ( go
@@ -11,17 +12,18 @@
 import System.Directory (removeFile)
 import System.Exit      (exitFailure, exitSuccess)
 
-import Cmt.IO.Config     (load, readCfg)
+import Cmt.IO.Config     (checkFormat, load, readCfg)
 import Cmt.IO.Git        (commit)
 import Cmt.IO.Input      (loop)
 import Cmt.Output.Format (format)
 import Cmt.Parser.Config (predefined)
-import Cmt.Types.Config  (Config, Output)
+import Cmt.Types.Config  (Config, Outputs)
 
 data Next
     = Previous
     | PreDefined Text
-    | Continue [Output]
+                 Outputs
+    | Continue Outputs
 
 backup :: FilePath
 backup = ".cmt.bkp"
@@ -38,7 +40,7 @@
             writeFile backup (encodeUtf8 txt)
             failure msg
 
-display :: Either Text (Config, [Output]) -> IO ()
+display :: Either Text (Config, Outputs) -> IO ()
 display (Left err) = putStrLn err
 display (Right (cfg, output)) = do
     parts <- loop cfg
@@ -50,27 +52,29 @@
     removeFile backup
     send txt
 
-predef :: Text -> IO ()
-predef name = do
+predef :: Text -> Outputs -> IO ()
+predef name output = do
     cfg <- load
     case predefined =<< cfg of
         Left msg -> failure msg
         Right pre ->
-            case find ((==) name . fst) pre of
-                Nothing       -> failure "No matching predefined message"
-                Just (_, txt) -> send txt
+            case lookup name pre of
+                Nothing -> failure "No matching predefined message"
+                Just cf -> display $ checkFormat output cf
 
 parseArgs :: [Text] -> Next
-parseArgs ["--prev"]   = Previous
-parseArgs ["-p", name] = PreDefined name
-parseArgs []           = Continue []
-parseArgs [msg]        = Continue [("*", msg)]
-parseArgs parts        = Continue [("*", unwords parts)]
+parseArgs ["--prev"]        = Previous
+parseArgs ["-p", name]      = PreDefined name []
+parseArgs ["-p", name, msg] = PreDefined name [("*", msg)]
+parseArgs ("-p":name:parts) = PreDefined name [("*", unwords parts)]
+parseArgs []                = Continue []
+parseArgs [msg]             = Continue [("*", msg)]
+parseArgs parts             = Continue [("*", unwords parts)]
 
 go :: IO ()
 go = do
     next <- parseArgs <$> getArgs
     case next of
-        Continue output -> readCfg output >>= display
-        Previous        -> previous
-        PreDefined name -> predef name
+        Continue output        -> readCfg output >>= display
+        Previous               -> previous
+        PreDefined name output -> predef name output
diff --git a/src/Cmt/IO/Config.hs b/src/Cmt/IO/Config.hs
--- a/src/Cmt/IO/Config.hs
+++ b/src/Cmt/IO/Config.hs
@@ -1,9 +1,11 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt.IO.Config
     ( load
     , readCfg
+    , checkFormat
     ) where
 
 import ClassyPrelude
@@ -18,7 +20,7 @@
 configFile :: FilePath
 configFile = ".cmt"
 
-checkFormat :: [Output] -> Config -> Either Text (Config, [Output])
+checkFormat :: Outputs -> Config -> Either Text (Config, Outputs)
 checkFormat output (Config parts format) = do
     let partNames = nub $ partName <$> parts
     let formatNames = nub . catMaybes $ formatName <$> format
@@ -29,7 +31,7 @@
             | otherwise = Right (Config parts format, output)
     result
 
-parse :: [Output] -> Text -> Either Text (Config, [Output])
+parse :: Outputs -> Text -> Either Text (Config, Outputs)
 parse output cfg = config cfg >>= checkFormat output
 
 read :: FilePath -> IO Text
@@ -74,7 +76,7 @@
         Just path -> Right <$> read path
         Nothing   -> pure $ Left ".cmt file not found"
 
-readCfg :: [Output] -> IO (Either Text (Config, [Output]))
+readCfg :: Outputs -> IO (Either Text (Config, Outputs))
 readCfg output = do
     cfg <- load
     pure (parse output =<< cfg)
diff --git a/src/Cmt/IO/Input.hs b/src/Cmt/IO/Input.hs
--- a/src/Cmt/IO/Input.hs
+++ b/src/Cmt/IO/Input.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt.IO.Input
     ( loop
@@ -7,12 +8,15 @@
 
 import ClassyPrelude
 
+import Data.Map.Strict              (Map)
 import System.Console.Terminal.Size (size, width)
 
 import Cmt.IO.Git         (changed)
 import Cmt.Parser.Options (parse)
 import Cmt.Types.Config
 
+type Choice = Map Int Text
+
 prompt :: Text -> IO Text
 prompt s = do
     putStr $ s <> " "
@@ -25,23 +29,27 @@
 putName :: Text -> IO ()
 putName name = putStrLn $ "\n" <> name <> ":"
 
-listItem :: (Int, Text) -> Text
-listItem (n, o) = tshow n <> ") " <> o
+listItem :: Int -> Text -> Text
+listItem n o = tshow n <> ") " <> o
 
-displayOptions :: [(Int, Text)] -> IO ()
+displayOptions :: Choice -> IO ()
 displayOptions opts = do
-    let long = intercalate "   " $ listItem <$> opts
+    let parts = mapWithKey listItem opts
+    let long = intercalate "   " parts
     maxLength <- getWidth
     if length long < maxLength
         then putStrLn long
-        else sequence_ $ putStrLn . listItem <$> opts
+        else sequence_ $ putStrLn <$> parts
 
-choice :: [(Int, Text)] -> Int -> Maybe Text
-choice opts chosen = snd <$> find ((== chosen) . fst) opts
+choice :: Choice -> Int -> Maybe Text
+choice opts chosen = lookup chosen opts
 
+choiceGen :: [Text] -> Choice
+choiceGen ts = mapFromList $ zip [1 ..] ts
+
 options :: [Text] -> IO Text
 options opts = do
-    let opts' = zip [1 ..] opts
+    let opts' = choiceGen opts
     displayOptions opts'
     chosen <- parse <$> prompt ">"
     case chosen of
@@ -66,11 +74,11 @@
         then line
         else pure value
 
-output :: Part -> IO (Name, Text)
+output :: Part -> IO Output
 output (Part name Line)           = putName name >> (,) name <$> line
 output (Part name (Options opts)) = putName name >> (,) name <$> options opts
 output (Part name Lines)          = putName name >> (,) name <$> (unlines <$> multiLine [])
 output (Part name Changed)        = putName name >> (,) name <$> (options =<< changed)
 
-loop :: Config -> IO [(Name, Text)]
-loop (Config parts _) = sequence $ output <$> parts
+loop :: Config -> IO Outputs
+loop (Config parts _) = mapFromList <$> traverse output parts
diff --git a/src/Cmt/Output/Format.hs b/src/Cmt/Output/Format.hs
--- a/src/Cmt/Output/Format.hs
+++ b/src/Cmt/Output/Format.hs
@@ -1,5 +1,5 @@
 {-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt.Output.Format
     ( format
@@ -9,12 +9,9 @@
 
 import Cmt.Types.Config
 
-tidy :: [Output] -> FormatPart -> Text
-tidy _ (Literal c) = c
-tidy parts (Named name) =
-    case find ((== name) . fst) parts of
-        Just (_, t) -> t
-        Nothing     -> ""
+tidy :: Outputs -> FormatPart -> Maybe Text
+tidy _ (Literal c)      = pure c
+tidy parts (Named name) = lookup name parts
 
-format :: Config -> [Output] -> Text
-format (Config _ fmt) parts = concat $ tidy parts <$> fmt
+format :: Config -> Outputs -> Text
+format (Config _ fmt) parts = concat . catMaybes $ tidy parts <$> fmt
diff --git a/src/Cmt/Parser/Config.hs b/src/Cmt/Parser/Config.hs
--- a/src/Cmt/Parser/Config.hs
+++ b/src/Cmt/Parser/Config.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt.Parser.Config
     ( config
@@ -18,15 +19,15 @@
 configP :: Parser Config
 configP = do
     parts <- partsP
-    _ <- predefinedPartsP
+    _ <- predefinedPartsP parts
     format <- formatP $ partName <$> parts
     _ <- endOfInput
     pure $ Config parts format
 
-predefinedP :: Parser [PreDefinedPart]
+predefinedP :: Parser PreDefinedParts
 predefinedP = do
     parts <- partsP
-    pre <- predefinedPartsP
+    pre <- predefinedPartsP parts
     _ <- formatP $ partName <$> parts
     _ <- endOfInput
     pure pre
@@ -39,7 +40,7 @@
         Left _ ->
             Left "Could not parse config. Check that your format doesn't contain any invalid parts."
 
-predefined :: Text -> Either Text [PreDefinedPart]
+predefined :: Text -> Either Text PreDefinedParts
 predefined cfg =
     case parseOnly predefinedP cfg of
         Right c -> Right c
diff --git a/src/Cmt/Parser/Config/PreDefined.hs b/src/Cmt/Parser/Config/PreDefined.hs
--- a/src/Cmt/Parser/Config/PreDefined.hs
+++ b/src/Cmt/Parser/Config/PreDefined.hs
@@ -1,5 +1,5 @@
 {-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 
 module Cmt.Parser.Config.PreDefined
     ( predefinedPartsP
@@ -10,15 +10,29 @@
 import Data.Attoparsec.Text
 
 import Cmt.Parser.Attoparsec
+import Cmt.Parser.Config.Format (formatP)
 import Cmt.Types.Config
 
 -- predefined
-value :: Parser Text
-value = lexeme $ char '"' *> tnotChar '"' <* char '"'
+value :: Name -> [Part] -> Parser Config
+value name parts = lexeme (char '"' *> takeTill (== '"') <* char '"') >>= getConfig
+  where
+    getConfig :: Text -> Parser Config
+    getConfig template =
+        case parseOnly (formatP $ partName <$> parts) template of
+            Left _    -> fail $ "Invalid predefined template: " ++ show name
+            Right fmt -> pure $ Config (filterParts fmt parts) fmt
+    filterParts :: [FormatPart] -> [Part] -> [Part]
+    filterParts fps = filter ((`elem` catMaybes (formatName <$> fps)) . partName)
 
-partP :: Parser PreDefinedPart
-partP = stripComments $ (,) <$> (pack <$> many' letter <* lexeme (char '=')) <*> value
+partP :: [Part] -> Parser PreDefinedPart
+partP ps =
+    stripComments $ do
+        name <- pack <$> many' letter <* lexeme (char '=')
+        conf <- stripComments (value name ps)
+        pure (name, conf)
 
-predefinedPartsP :: Parser [PreDefinedPart]
-predefinedPartsP =
-    stripComments $ option [] (stripComments (char '{') *> many' partP <* stripComments (char '}'))
+predefinedPartsP :: [Part] -> Parser PreDefinedParts
+predefinedPartsP ps =
+    mapFromList <$>
+    option [] (stripComments (char '{') *> many' (partP ps) <* stripComments (char '}'))
diff --git a/src/Cmt/Types/Config.hs b/src/Cmt/Types/Config.hs
--- a/src/Cmt/Types/Config.hs
+++ b/src/Cmt/Types/Config.hs
@@ -2,11 +2,16 @@
 
 module Cmt.Types.Config where
 
-import ClassyPrelude (Eq, Maybe (..), Show, Text)
+import ClassyPrelude   (Eq, Maybe (..), Show, Text)
+import Data.Map.Strict (Map)
 
 type Output = (Name, Text)
 
-type PreDefinedPart = (Text, Text)
+type Outputs = Map Name Text
+
+type PreDefinedPart = (Text, Config)
+
+type PreDefinedParts = Map Text Config
 
 type Name = Text
 
diff --git a/test/Cmt/Parser/ConfigTest.hs b/test/Cmt/Parser/ConfigTest.hs
--- a/test/Cmt/Parser/ConfigTest.hs
+++ b/test/Cmt/Parser/ConfigTest.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 {-# LANGUAGE TemplateHaskell #-}
 
 module Cmt.Parser.ConfigTest where
@@ -47,6 +48,24 @@
 pre :: Text
 pre = decodeUtf8 $(embedFile "test/data/.cmt-predefined")
 
+preWithVars :: Text
+preWithVars = decodeUtf8 $(embedFile "test/data/.cmt-predefined-with-vars")
+
+preConfig :: PreDefinedParts
+preConfig =
+    [ ("vb", Config [] [Literal "chore (package.yaml): version bump"])
+    , ("readme", Config [] [Literal "docs (README.md): updated readme"])
+    ]
+
+preWithVarsConfig :: PreDefinedParts
+preWithVarsConfig =
+    [ ( "vb"
+      , Config [Part "Scope" Changed] [Literal "chore (", Named "Scope", Literal "): version bump"])
+    , ( "readme"
+      , Config [Part "Short Message" Line] [Literal "docs (README.md): ", Named "Short Message"])
+    , ("multiline", Config [] [Literal "This\nNow works # but comments are included\nI believe\n"])
+    ]
+
 -- import Test.Tasty.HUnit
 test_config :: TestTree
 test_config =
@@ -71,12 +90,12 @@
                     (assertEqual "Gives back correct format" (Right []) (predefined comments))
               , testCase
                     "predefined"
+                    (assertEqual "Gives back correct format" (Right preConfig) (predefined pre))
+              , testCase
+                    "predefined with variables"
                     (assertEqual
                          "Gives back correct format"
-                         (Right
-                              [ ("vb", "chore (package.yaml): version bump")
-                              , ("readme", "docs (README.md): updated readme")
-                              ])
-                         (predefined pre))
+                         (Right preWithVarsConfig)
+                         (predefined preWithVars))
               ]
         ]
