I found a solution here - Item expiration reminders in SharePoint using workflow - but for some reason, the workflow didn't work as expected, possibly due to the structure of my data. So I've made some changes, trimmed it a little, and present it below.
Only one additional column had to be created - named "Previous due date", it's a date column in which the workflow stores the current expiry date. It may even be possible to remove this column entirely, but I've left it in to make it easier to check the progress of the workflow.
What the workflow does is: it stores a workflow variable called Alert Date, which is simply three months before the Expiration Date. If the Expiration Date is after today, it will set the new column, "Previous due date", to the current Expiration Date, and also sets a workflow variable, "Original Due Date", to the Expiration Date. It will then carry out two actions in parellel -
- Wait for the Alert Date to be reached, and then send an email
- Wait for the Expiration Date in the item to change, and then stop the workflow.
Since the workflow runs on change or create, we don't want the same workflow running multiple times on the same item because the item has been changed, so the second action takes care of that.
Here's the actual workflow: