The main complexity in this area is firstly having a robust calculation of when each cycle falls due, and secondly ensuring that the scheduling system is resilient to maintenance downtime and breakdown (for example, that it is possible to advance or otherwise rearrange the execution of scheduled tasks, or to catch up on tasks that were not executed on their usual cycle, and to deliver correct results in each case).
Judging by the other answers, these complexities are often under-appreciated.
The starting point is a table defining the scheduled tasks - the date range across which they are expected to run, and the cycle(s) of repetition.
You've identified daily, weekly, monthly, and yearly. The first thing to observe is that very frequently someone will want to specify the time of day at which a task runs - not just overnight, or first thing, but after dinner (once another department have finished their morning's work), or last thing (so that people can be held back for overtime, for example). Sometimes also, if the task involves heavy processing, you will want it done at a specific time out of hours, so as not to bog the system down during working hours, or so that two such heavy tasks do not conflict with one another.
A second thing to observe is that, despite casual appearances, almost no cycle ends up having a fixed period of time between each occurrence. Cycles consisting of months, or some factor of months (twice-monthly, two-monthly, three-monthly/quarterly, yearly) do not consist of a fixed number of days. I therefore make a conceptual distinction between day-denominated periods, and month-denominated periods.
So in practice, you will often end up defining cycles which fall on a specific day counted from the start of the month (or other month-denominated period), but also separately a cycle where the day is specified as a count from the end of the month (for example, the "second-to-last day of the month", which if counted from the start may be any of 27/28/29/30). These are not the same cycles, despite both being monthly.
But even where a cycle does appear to consist of a fixed period of time (obviously daily, also weekly, two-weekly/fortnightly, four-weekly/lunarly), in practice nobody really intends that an email should be generated on Christmas Day, and the list of exceptions will be far larger than that in most firms (including public/bank holidays, company shutdowns, and even ordinary weekends for cycles like monthly or quarterly).
(As an aside, which must be mentioned now in passing, I'm also going to completely ignore the notion of "leap seconds" in UTC.)
So this irregularity in the length of each cycle introduces two more concepts. Firstly, there must be a system of "date rolling", where a cycle occurrence is first calculated trivially, but then adjusted, backwards or forwards, to a relevant working day.
Secondly, there will always be unsystematic exceptions, and you must be able to adjust these by hand.
The consequence here is that the schedule of any given cycle should not be treated as something that is calculated on the fly. Instead, a calendar for every single available cycle should be generated and maintained up to a suitable time in the future, and it must then be capable of further manual adjustments.
Another issue you may encounter, for all month-denominated periods, is defining the starting point of the period. A "financial year" often does not start on 1 Jan, or even on the first day of any subsequent month. The tax year here in the UK starts 6 Apr. The ISO week-year, which allows a year to consist always of an integral number of weeks, does not start on any fixed date (as conventionally expressed). If you're generating scheduled emails for the accounts department, you now better be prepared to address these.
Now I've summarised all the complexity in determining when a scheduled task should run. But I mentioned at the start the problem when, for wholly unplanned reasons, things don't run when they should have, or they fail in the process.
You should therefore have a system that is capable of recording exactly when tasks have and haven't been run, the outcome, and of resolving missed or failed runs (either by retrying, catching up in order, catching up out of order, or skipping the missed tasks entirely), and ensuring that duplicates are not unintentionally performed. It should be possible to review these missed tasks manually first - the sysadmins who have been fixing a problem all morning, will not appreciate a system that comes back online and then immediately bogs down and runs away like a broken diesel engine, performing tasks that would normally have been done overnight over several hours.