{"id":13878747,"url":"https://github.com/joshmn/caffeinate","last_synced_at":"2025-10-10T04:03:47.723Z","repository":{"id":51088016,"uuid":"315818889","full_name":"joshmn/caffeinate","owner":"joshmn","description":"A Rails engine for drip campaigns/scheduled sequences and periodical support. Works with ActionMailer, and other things.","archived":false,"fork":false,"pushed_at":"2023-12-21T15:16:04.000Z","size":669,"stargazers_count":358,"open_issues_count":16,"forks_count":15,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-05-26T01:08:07.626Z","etag":null,"topics":["drip","drip-campaign","email","marketing-automation","rails-engine","scheduled-messages","scheduled-notifications"],"latest_commit_sha":null,"homepage":"https://caffeinate.email","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshmn.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/contributing.md","funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-11-25T03:30:48.000Z","updated_at":"2025-05-13T04:15:23.000Z","dependencies_parsed_at":"2023-12-21T18:30:53.103Z","dependency_job_id":"32fac28d-63e4-4da2-afe5-6ae316a329dd","html_url":"https://github.com/joshmn/caffeinate","commit_stats":null,"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/joshmn/caffeinate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshmn%2Fcaffeinate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshmn%2Fcaffeinate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshmn%2Fcaffeinate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshmn%2Fcaffeinate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshmn","download_url":"https://codeload.github.com/joshmn/caffeinate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshmn%2Fcaffeinate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002604,"owners_count":26083427,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["drip","drip-campaign","email","marketing-automation","rails-engine","scheduled-messages","scheduled-notifications"],"created_at":"2024-08-06T08:01:58.609Z","updated_at":"2025-10-10T04:03:47.694Z","avatar_url":"https://github.com/joshmn.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg width=\"450\" src=\"https://github.com/joshmn/caffeinate/raw/master/logo.png\" alt=\"Caffeinate logo\" /\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n    \u003ca href=\"https://codecov.io/gh/joshmn/caffeinate\"\u003e\n        \u003cimg src=\"https://codecov.io/gh/joshmn/caffeinate/branch/master/graph/badge.svg?token=5LCOB4ESHL\" alt=\"Coverage\"/\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://codeclimate.com/github/joshmn/caffeinate/maintainability\"\u003e\n        \u003cimg src=\"https://api.codeclimate.com/v1/badges/9c075416ce74985d5c6c/maintainability\" alt=\"Maintainability\"/\u003e\n    \u003c/a\u003e\n     \u003ca href=\"https://inch-ci.org/github/joshmn/caffeinate\"\u003e\n        \u003cimg src=\"https://inch-ci.org/github/joshmn/caffeinate.svg?branch=master\" alt=\"Docs\"/\u003e\n    \u003c/a\u003e\n\u003c/div\u003e\n\n# Caffeinate\n\nCaffeinate 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!\n\nCaffeinate provides a simple DSL to create scheduled sequences which can be sent by ActionMailer, or invoked by a Ruby object, without any additional configuration. \n\nThere's a cool demo app you can spin up [here](https://github.com/joshmn/caffeinate-marketing).\n\n## Now supports POROs!\n\nOriginally, 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.\n\n## Is this thing dead?\n\nNo! Not at all!\n\nThere's not a lot of activity here because it's stable and working! I am more than happy to entertain new features.\n\n## Oh my gosh, a web UI!\n\nSee https://github.com/joshmn/caffeinate-webui for an accompanying lightweight UI for simple administrative tasks and overview.\n\n## Do you suffer from ActionMailer tragedies?\n\nIf you have _anything_ like this is your codebase, **you need Caffeinate**:\n\n```ruby\nclass User \u003c ApplicationRecord\n  after_commit on: :create do\n    OnboardingMailer.welcome_to_my_cool_app(self).deliver_later\n    OnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)\n    OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)\n  end\nend\n```\n\n```ruby\nclass OnboardingMailer \u003c ActionMailer::Base\n  def welcome_to_my_cool_app(user)\n    mail(to: user.email, subject: \"Welcome to CoolApp!\")\n  end\n\n  def some_cool_tips(user)\n    return if user.unsubscribed_from_onboarding_campaign?\n\n    mail(to: user.email, subject: \"Here are some cool tips for MyCoolApp\")\n  end\n\n  def help_getting_started(user)\n    return if user.unsubscribed_from_onboarding_campaign?\n    return if user.onboarding_completed?\n\n    mail(to: user.email, subject: \"Do you need help getting started?\")\n  end\nend\n```\n\n### What's wrong with this?\n\n* You're checking state in a mailer\n* The unsubscribe feature is, most likely, tied to a `User`, which means...\n* It's going to be _so fun_ to scale when you finally want to add more unsubscribe links for different types of sequences\n    - \"one of your projects has expired\", but which one? Then you have to add a column to `projects` and manage all that state... ew\n\n## Perhaps you suffer from enqueued worker madness\n\nIf you have _anything_ like this is your codebase, **you need Caffeinate**:\n\n```ruby\nclass User \u003c ApplicationRecord\n  after_commit on: :create do\n    OnboardingWorker.perform_later(:welcome, self.id)\n    OnboardingWorker.perform_in(2.days, :some_cool_tips, self.id)\n    OnboardingWorker.perform_later(3.days, :help_getting_started, self.id)\n  end\nend\n```\n\n```ruby\nclass OnboardingWorker\n  include Sidekiq::Worker\n  \n  def perform(action, user_id)\n    user = User.find(user_id)\n    user.public_send(action)\n  end\nend\n\nclass User\n  def welcome\n    send_twilio_message(\"Welcome to our app!\")\n  end\n\n  def some_cool_tips\n    return if self.unsubscribed_from_onboarding_campaign?\n\n    send_twilio_message(\"Here are some cool tips for MyCoolApp\")\n  end\n\n  def help_getting_started\n    return if unsubscribed_from_onboarding_campaign?\n    return if onboarding_completed?\n\n    send_twilio_message(\"Do you need help getting started?\")\n  end\n  \n  private \n  \n  def send_twilio_message(message)\n    twilio_client.messages.create(\n            body: message,\n            to: \"+12345678901\",\n            from: \"+15005550006\",\n    )\n  end\n  \n  def twilio_client\n    @twilio_client ||= Twilio::REST::Client.new Rails.application.credentials.twilio[:account_sid], Rails.application.credentials.twilio[:auth_token]\n  end\nend\n```\n\nI don't even need to tell you why this is smelly!\n\n## Do this all better in five minutes\n\nIn five minutes you can implement this onboarding campaign:\n\n### Install it\n\nAdd to Gemfile, run the installer, migrate:\n\n```bash\n$ bundle add caffeinate\n$ rails g caffeinate:install\n$ rake db:migrate\n```\n\n### Clean up the business logic\n\nAssuming 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](docs/6-without-action-mailer.md).)\n\nThe 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](docs/2-data-models.md):\n\n```ruby\nclass OnboardingMailer \u003c ActionMailer::Base\n  def welcome_to_my_cool_app(mailing)\n    @user = mailing.subscriber\n    mail(to: @user.email, subject: \"Welcome to CoolApp!\")\n  end\n\n  def some_cool_tips(mailing)\n    @user = mailing.subscriber\n    mail(to: @user.email, subject: \"Here are some cool tips for MyCoolApp\")\n  end\n\n  def help_getting_started(mailing)\n    @user = mailing.subscriber\n    mail(to: @user.email, subject: \"Do you need help getting started?\")\n  end\nend\n```\n\n### Create a Dripper\n\nA Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.\n\nIn `app/drippers/onboarding_dripper.rb`:\n\n```ruby\nclass OnboardingDripper \u003c ApplicationDripper\n  # each sequence is a campaign. This will dynamically create one by the given slug\n  self.campaign = :onboarding \n  \n  # gets called before every time we process a drip\n  before_drip do |_drip, mailing| \n    if mailing.subscription.subscriber.onboarding_completed?\n      mailing.subscription.unsubscribe!(\"Completed onboarding\")\n      throw(:abort)\n    end \n  end\n  \n  # map drips to the mailer\n  drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours\n  drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days\n  drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days\nend\n```\n\nWe want to skip sending the `mailing` if the `subscriber` (`User`) completed onboarding. Let's unsubscribe \nwith `#unsubscribe!` and give it an optional reason of `Completed onboarding` so we can reference it later \nwhen we look at analytics. `throw(:abort)` halts the callback chain just like regular Rails callbacks, stopping the \nmailing from being sent.\n\n### Add a subscriber to the Campaign\n\nCall `OnboardingDripper.subscribe` to subscribe a polymorphic `subscriber` to the Campaign, which creates\na `Caffeinate::CampaignSubscription`.\n\n```ruby\nclass User \u003c ApplicationRecord\n  after_commit on: :create do\n    OnboardingDripper.subscribe!(self)\n  end\nend\n```\n\n### Run the Dripper\n\nYou'll usually do this in a scheduled background job or cron.\n\n```ruby\nOnboardingDripper.perform!\n```\n\nAlternatively, you can run all of the registered drippers with `Caffeinate.perform!`.\n\n### Done\n\nYou're done. \n\n[Check out the docs](/docs/README.md) for a more in-depth guide that includes all the options you can use for more complex setups,\ntips, tricks, and shortcuts.\n\n## Using Without ActionMailer\n\nNow supports POROs \u003csup\u003ethat inherit from a magical class\u003c/sup\u003e! 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](docs/6-without-action-mailer.md).) for more.\n\n## But wait, there's more\n\nCaffeinate also...\n\n* ✅ Works with regular Ruby methods as of V2.3\n* ✅ Allows hyper-precise scheduled times. 9:19AM _in the user's timezone_? Sure! **Only on business days**? YES! \n* ✅ Periodicals\n* ✅ Manages unsubscribes\n* ✅ Works with singular and multiple associations\n* ✅ Compatible with every background processor\n* ✅ Tested against large databases at AngelList and is performant as hell\n* ✅ Effortlessly handles complex workflows\n    - Need to skip a certain mailing? You can!\n\n## Documentation\n\n* [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)\n* [Better-than-average code documentation](https://rubydoc.info/gems/caffeinate)\n\n## Upcoming features/todo\n\n[Handy dandy roadmap](https://github.com/joshmn/caffeinate/projects/1).\n\n## Alternatives\n\nNot a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:\n\n* https://github.com/honeybadger-io/heya\n* https://github.com/tarr11/dripper\n* https://github.com/Sology/maily_herald\n\n## Contributing\n\nThere's so much more that can be done with this. I'd love to see what you're thinking.\n\nIf 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](https://twitter.com/joshmn) or create an issue! I'd love to chat.\n\n## Contributors \u0026 thanks\n\n* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)\n* Thanks to [markokajzer](https://github.com/markokajzer) for listening to me talk about this most mornings.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshmn%2Fcaffeinate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshmn%2Fcaffeinate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshmn%2Fcaffeinate/lists"}