diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # A Haskell library for the GitLab web API
 
+## gitlab-haskell library
+
 This library interacts with a GitLab server's API. It supports queries
 about and updates to:
 
@@ -18,3 +20,61 @@
 
 The library parses JSON results into Haskell data types in the
 `GitLab.Types` module.
+
+## gitlab-tools executable
+
+This package also includes a `gitlab-tools` executable.
+
+```
+> gitlab-tools --help
+Program to execute bulk GitLab actions
+
+Usage: gitlab-tools --host host [--token token] [--filename filename]
+                    [--group group] [--project project]
+                    [--add-reporters-to-group] [--share-projects-with-group]
+                    [--registered]
+  Executes actions against a GitLab server
+
+Available options:
+  --host host              URL of the GitLab server
+  --token token            GitLab access token
+  --filename filename      name of a CSV file
+  --group group            name of a group
+  --project project        name of a project
+  --add-reporters-to-group Add users as reporters to a group
+  --share-projects-with-group
+                           Share all projects with a given name to the specific
+                           group
+  --registered             prints 'yes' or 'no' depending on whether a user
+                           exists on the GitLab server
+  -h,--help                Show this help text
+```
+
+### Examples
+
+Sharing projects with a group:
+
+```
+> gitlab-tools \
+    --host <GitLab url> --token <token> \
+    --share-projects-with-group --project my_project \
+    --group my_group
+```
+
+This commands finds all projects called `my_project` then adds the
+`my_group` group to all of those projects as a member with the
+Reporter role.
+
+Adding users to a group:
+
+```
+> gitlab-tools \
+    --host <GitLab url> --token <token> \
+    --filename <filename>.csv \
+    --add-reporters-to-group \
+    --group my_group
+```
+
+This command reads usernames from a comma separated file, and if they
+are registered on the GitLab server it adds them to the `my_group`
+group with the Reporter role.
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,156 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import GitLab
+import Network.HTTP.Types.Status
+import Data.Semigroup ((<>))
+import Options.Applicative
+
+main :: IO ()
+main = processOptions =<< execParser opts
+  where
+    opts = info (parser <**> helper)
+           ( fullDesc
+             <> progDesc "Executes actions against a GitLab server"
+             <> header "Program to execute bulk GitLab actions" )
+
+data Options =
+  Options
+  { gitlabUrl :: String
+  , gitlabToken :: String
+  , addUsersFilename :: String
+  , group :: String
+  , project :: String
+  , addReportersToGroup :: Bool
+  , addGroupToProject :: Bool
+  , isRegistered :: Bool
+  }
+  deriving (Show)
+
+parser :: Parser Options
+parser = Options
+  <$> strOption
+  ( long "host"
+    <> metavar "host"
+    <> help "URL of the GitLab server" )
+  <*> strOption
+  ( long "token"
+    <> metavar "token"
+    <> value ""
+    <> help "GitLab access token" )
+  <*> strOption
+  ( long "filename"
+    <> metavar "filename"
+    <> value ""
+    <> help "name of a CSV file" )
+  <*> strOption
+  ( long "group"
+    <> metavar "group"
+    <> value ""
+    <> help "name of a group" )
+  <*> strOption
+  ( long "project"
+    <> metavar "project"
+    <> value ""
+    <> help "name of a project" )
+  <*> switch
+  ( long "add-reporters-to-group"
+    <> help "Add users as reporters to a group" )
+  <*> switch
+  ( long "share-projects-with-group"
+    <> help "Share all projects with a given name to the specific group" )
+  <*> switch
+  ( long "registered"
+    <> help "prints 'yes' or 'no' depending on whether a user exists on the GitLab server" )
+
+processOptions :: Options -> IO ()
+processOptions (Options _url _tok "" _grp _proj True _addGroup _isRegistered) =
+  return ()
+processOptions (Options _url _tok _filename "" _proj True _addGroup _isRegistered) =
+  putStrLn "you must specify a group name too"
+processOptions (Options _gUrl _gToken _fname _groupName _ True True _isRegistered) =
+  putStrLn "you can only choose one of adding users to group or adding group to projects"
+
+-- add reporters to a group
+processOptions (Options gUrl gToken fname groupName _ True False _isRegistered) = do
+  text <- T.readFile fname
+  let usernames = T.splitOn "," text
+  runGitLab
+    (defaultGitLabServer
+      { url = T.pack gUrl
+      , token = T.pack gToken
+      })
+    (addUsersToGroupDbg (T.pack groupName) Reporter usernames)
+    
+-- add group as a member to all projects with a given name, as Reporter
+processOptions (Options gUrl gToken _ groupName projectName False True _isRegistered) = do
+  when (null groupName || null projectName)
+    (error "for --share-project you must specify a group name and a project name")
+  runGitLab
+    (defaultGitLabServer
+      { url = T.pack gUrl
+      , token = T.pack gToken
+      })
+    (do groups <- groupsWithName (T.pack groupName)
+        void $ liftIO $ when (null groups)
+          (error ("group not found: " ++ groupName))
+        void $ liftIO $ when (length groups > 1)
+          (error ("multiple groups found for: " ++ groupName))
+        -- should only be 1 group with this head
+        let grp = head groups
+        projects <- projectsWithName (T.pack projectName)
+        mapM_
+          (\prj -> do
+             result <- shareProjectWithGroup (group_id grp) (project_id prj) Reporter
+             case result of
+               Left st -> liftIO (putStrLn ("unable to share project " ++ show (project_id prj) ++ " with group " ++ show (group_id grp) ++ ". Reason: " ++ show st))
+               Right _details -> liftIO (putStrLn ("Added group " ++ show (group_id grp) ++ " to project " ++ show (project_id prj) ++ " as a Reporter"))
+          )
+          projects
+    )
+
+-- ask if users are registered for a given GitLab server
+processOptions (Options _gUrl _gToken "" __groupName _ False False True) =
+  putStrLn "--registered needs a filename with --filename"
+processOptions (Options gUrl gToken fname _groupName _ False False True) = do
+  text <- T.readFile fname
+  let usernames = T.splitOn "," text
+  runGitLab
+    (defaultGitLabServer
+      { url = T.pack gUrl
+      , token = T.pack gToken
+      })
+    (mapM_
+      (\usrName ->
+          do res <- searchUser usrName
+             -- empty list of returned users
+             if null res
+             then liftIO $ putStrLn (T.unpack usrName ++ ": no")
+             else liftIO $ putStrLn (T.unpack usrName ++ ": yes")
+      )
+      usernames
+    )
+
+processOptions Options{} =
+  error "combination of flags not recognised"
+  
+addUsersToGroupDbg ::
+  (MonadIO m) => Text -> AccessLevel -> [Text] -> GitLab m ()
+addUsersToGroupDbg groupName access usernames = do
+  (results :: [Either Status Member]) <-
+    addUsersToGroup groupName access usernames
+  liftIO $ mapM_
+    (\(result :: Either Status Member) ->
+       case result of
+         Left errorStatus -> print errorStatus
+         Right member ->
+           putStrLn
+             ("Added to " ++
+              show groupName ++ ": " ++ show (member_username member)))
+    results
diff --git a/gitlab-haskell.cabal b/gitlab-haskell.cabal
--- a/gitlab-haskell.cabal
+++ b/gitlab-haskell.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 name:           gitlab-haskell
 category:       Git
-version:        0.1.1
+version:        0.1.2
 synopsis:       A Haskell library for the GitLab web API
 description:
   This Haskell library queries and updates the database of a GitLab instance using the GitLab web API: <https://docs.gitlab.com/ee/api/>
@@ -23,6 +23,19 @@
 source-repository head
   type: git
   location: https://gitlab.com/robstewart57/gitlab-haskell
+
+executable gitlab-tools
+  main-is: Main.hs
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+                base >=4.7 && <5
+              , gitlab-haskell
+              , text
+              , http-types
+              , optparse-applicative
+  default-language: Haskell2010
 
 library
   exposed-modules:
