Caffeinate is a drip engine for managing, creating, and performing scheduled messages sequences from your Ruby on Rails application. This was originally meant for email, but now supports anything!
Caffeinate provides a simple DSL to create scheduled sequences which can be sent by ActionMailer, or invoked by a Ruby object, without any additional configuration.
There's a cool demo app you can spin up here.
Originally, this was meant for just email, but as of V2.3 supports plain old Ruby objects just as well. Having said, the documentation primarily revolves around using ActionMailer, but it's just as easy to plug in any Ruby class. See Using Without ActionMailer below.
No! Not at all!
There's not a lot of activity here because it's stable and working! I am more than happy to entertain new features.
See https://github.com/joshmn/caffeinate-webui for an accompanying lightweight UI for simple administrative tasks and overview.
If you have anything like this is your codebase, you need Caffeinate:
classUser < ApplicationRecordafter_commiton: :createdoOnboardingMailer.welcome_to_my_cool_app(self).deliver_laterOnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)endendclassOnboardingMailer < ActionMailer::Basedefwelcome_to_my_cool_app(user)mail(to: user.email,subject: "Welcome to CoolApp!")enddefsome_cool_tips(user)returnifuser.unsubscribed_from_onboarding_campaign?mail(to: user.email,subject: "Here are some cool tips for MyCoolApp")enddefhelp_getting_started(user)returnifuser.unsubscribed_from_onboarding_campaign?returnifuser.onboarding_completed?mail(to: user.email,subject: "Do you need help getting started?")endend- You're checking state in a mailer
- The unsubscribe feature is, most likely, tied to a
User, which means... - It's going to be so fun to scale when you finally want to add more unsubscribe links for different types of sequences
- "one of your projects has expired", but which one? Then you have to add a column to
projectsand manage all that state... ew
- "one of your projects has expired", but which one? Then you have to add a column to
If you have anything like this is your codebase, you need Caffeinate:
classUser < ApplicationRecordafter_commiton: :createdoOnboardingWorker.perform_later(:welcome,self.id)OnboardingWorker.perform_in(2.days,:some_cool_tips,self.id)OnboardingWorker.perform_later(3.days,:help_getting_started,self.id)endendclassOnboardingWorkerincludeSidekiq::Workerdefperform(action,user_id)user=User.find(user_id)user.public_send(action)endendclassUserdefwelcomesend_twilio_message("Welcome to our app!")enddefsome_cool_tipsreturnifself.unsubscribed_from_onboarding_campaign?send_twilio_message("Here are some cool tips for MyCoolApp")enddefhelp_getting_startedreturnifunsubscribed_from_onboarding_campaign?returnifonboarding_completed?send_twilio_message("Do you need help getting started?")endprivatedefsend_twilio_message(message)twilio_client.messages.create(body: message,to: "+12345678901",from: "+15005550006",)enddeftwilio_client@twilio_client ||= Twilio::REST::Client.newRails.application.credentials.twilio[:account_sid],Rails.application.credentials.twilio[:auth_token]endendI don't even need to tell you why this is smelly!
In five minutes you can implement this onboarding campaign:
Add to Gemfile, run the installer, migrate:
$ bundle add caffeinate
$ rails g caffeinate:install
$ rake db:migrateAssuming you intend to use Caffeinate to handle emails using ActionMailer, mailers should be responsible for receiving context and creating a mail object. Nothing more. (If you are looking for examples that don't use ActionMailer, see Without ActionMailer.)
The only other change you need to make is the argument that the mailer action receives. It will now receive a Caffeinate::Mailing. Learn more about the data models:
classOnboardingMailer < ActionMailer::Basedefwelcome_to_my_cool_app(mailing)@user=mailing.subscribermail(to: @user.email,subject: "Welcome to CoolApp!")enddefsome_cool_tips(mailing)@user=mailing.subscribermail(to: @user.email,subject: "Here are some cool tips for MyCoolApp")enddefhelp_getting_started(mailing)@user=mailing.subscribermail(to: @user.email,subject: "Do you need help getting started?")endendA Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.
In app/drippers/onboarding_dripper.rb:
classOnboardingDripper < ApplicationDripper# each sequence is a campaign. This will dynamically create one by the given slugself.campaign=:onboarding# gets called before every time we process a dripbefore_dripdo |_drip,mailing| ifmailing.subscription.subscriber.onboarding_completed?mailing.subscription.unsubscribe!("Completed onboarding")throw(:abort)endend# map drips to the mailerdrip:welcome_to_my_cool_app,mailer: 'OnboardingMailer',delay: 0.hoursdrip:some_cool_tips,mailer: 'OnboardingMailer',delay: 2.daysdrip:help_getting_started,mailer: 'OnboardingMailer',delay: 3.daysendWe want to skip sending the mailing if the subscriber (User) completed onboarding. Let's unsubscribe
with #unsubscribe! and give it an optional reason of Completed onboarding so we can reference it later
when we look at analytics. throw(:abort) halts the callback chain just like regular Rails callbacks, stopping the
mailing from being sent.
Call OnboardingDripper.subscribe to subscribe a polymorphic subscriber to the Campaign, which creates
a Caffeinate::CampaignSubscription.
classUser < ApplicationRecordafter_commiton: :createdoOnboardingDripper.subscribe!(self)endendYou'll usually do this in a scheduled background job or cron.
OnboardingDripper.perform!Alternatively, you can run all of the registered drippers with Caffeinate.perform!.
You're done.
Check out the docs for a more in-depth guide that includes all the options you can use for more complex setups, tips, tricks, and shortcuts.
Now supports POROs that inherit from a magical class! Using the example above, implementing an SMS client. The same rules apply, just change mailer_class or mailer to action_class, and create a Caffeinate::ActionProxy (acts just like an ActionMailer). See Without ActionMailer.) for more.
Caffeinate also...
- ✅ Works with regular Ruby methods as of V2.3
- ✅ Allows hyper-precise scheduled times. 9:19AM in the user's timezone? Sure! Only on business days? YES!
- ✅ Periodicals
- ✅ Manages unsubscribes
- ✅ Works with singular and multiple associations
- ✅ Compatible with every background processor
- ✅ Tested against large databases at AngelList and is performant as hell
- ✅ Effortlessly handles complex workflows
- Need to skip a certain mailing? You can!
Not a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:
- https://github.com/honeybadger-io/heya
- https://github.com/tarr11/dripper
- https://github.com/Sology/maily_herald
There's so much more that can be done with this. I'd love to see what you're thinking.
If you have general feedback, I'd love to know what you're using Caffeinate for! Please email me (any-thing [at] josh.mn) or tweet me @joshmn or create an issue! I'd love to chat.
- Thanks to sourdoughdev for releasing the gem name to me. :)
- Thanks to markokajzer for listening to me talk about this most mornings.
The gem is available as open source under the terms of the MIT License.
