Simple command line todo-list
It seems like you may not quite grasp the interplay between do-notation and monad transformer stacks. Take a look at how I've rewritten program here to leverage the actual machinery of StateT. The version you wrote is needlessly verbose due to your manually plumbing the state around!

program :: StateT TodoList IO ()
program = do
    choice &lt;- requestChoice
    parseChoice choice
    printList
    program


parseChoice can be cleaned up by favoring pattern matching over guards. Whenever you see a wall of guards that depend only on Eq, consider pattern matching instead.

parseChoice :: Text -&gt; StateT TodoList IO ()
parseChoice choice =
    case split (== '|') (toLower choice) of
        ["add", date, message] -&gt; addItem $ TodoItem date message
        ["remove", index]      -&gt; removeItem $ parseInt index
        ["save", file]         -&gt; saveList file
        ["load", file]         -&gt; loadList file
        ["quit"]               -&gt; lift exitSuccess
        (invalid:_)            -&gt; invalidChoice invalid


I think you can probably guess what can change about removeItem after reading my other changes now, so before reading this next code block try rewriting it on your own.

removeItem :: Maybe Int -&gt; StateT TodoList IO ()
removeItem Nothing  = lift $ putStrLn "Invalid index entered"
removeItem (Just n) = modify (removeAt n)