Tweet later with Delayed Job

One of my favorite uses of delayed_job is scheduling jobs to be run in the future. We’ve done this on a few client projects, mostly with emails. For example, we needed to send appointment reminder emails 15 minutes before an appointment began. A relatively simple exercise. On creation of the appointment, schedule a job to be run at 15 minutes before the appointment begins.

What if you wanted to have a message scheduled to be tweeted 10 minutes before a blog was published? Let’s walk through that scenario.

Twitter Integration

First and foremost, register an application with Twitter to obtain your OAuth credentials. Make a note of your consumer_key and consumer_secret and add them to an initializer.

# config/initializers/twitter.rb

Twitter.configure do |config|
  config.consumer_key = "CONSUMER_KEY"
  config.consumer_secret = "CONSUMER_SECRET"

Next, add the Twitter and OAuth gems to our Gemfile so we can interact with the Twitter API. Run bundle to install the gems.

gem 'twitter'
gem 'oauth'

Then, emulate what’s happening in the example twitter-app. You’ll find that they add a few methods to the application_controller.rb that sets up the OAuth Consumer and a Twitter Client that can be used on subsequent requests.

Take a look at the sessions_controller.rb to see how they handle the connect/destroy workflow. On the callback, you’ll want to save the access_token.token, access_token.secret, and the access_token.params[:screen_name] so we can use those to send tweets with later.

Delayed Job Integration

Let’s move on to the fun stuff! Not that OAuth isn’t fun… who doesn’t love a good dance?

Not every blog post needs to have a scheduled tweet. If you haven’t already, you’ll probably want to add a few fields to your model; add something like schedule_tweet, tweet_content, tweet_at, and tweeted. This’ll help define what needs to get tweeted, when, and if it successfully tweeted.

After you’ve added your fields, install delayed_job by following the directions in the readme.

You’ll also want to add a few fields to your view. Here’s what I came up with:

  Twitter Announcement

  <%= form.check_box :schedule_tweet %> <%= form.label :schedule_tweet, "Tweet this?" %>

  <%= text_field_tag :minutes %> <%= label_tag :minutes, "minutes before" %>

  <%= form.text_area :tweet_content %>

I used a text_field_tag for the minutes for flexibility and so we could subtract them from the future published time of the blog post and then set our tweet_at timestamp to the result.

@post.tweet_at = @post.publish_at - params[:minutes].to_i.minutes if @post.schedule_tweet?

The next part is where the Delayed Job magic gets to happen. After the post is saved, schedule the delivery of the tweet.

def deliver_tweet
  client = => user.twitter_oauth_token, :oauth_token_secret => user.twitter_oauth_secret)
  update_attributes :tweeted => true
handle_asynchronously :deliver_tweet, :run_at => {|p| p.tweet_at }

handle_asynchronously allows for the same options as you can pass to delay. In addition, though, and here’s where the awesome is, the values can be Proc objects allowing call time evaluation of the value.

So, in this example, we schedule a job for the future to be run at our tweet_at time (X minutes before the publish_at time). When the job is run, the method checks to make sure the post has not already been tweeted and time has passed before it instantiates a Twitter client with the users’ credentials, publishes the tweet, and updates the tweeted attribute.

Changing the time

What happens if you come back later and want to change the scheduled delivery to be farther in the future? You might be tempted to go into Delayed Job and modify the job, or delete it and add a new on. We recommend a much simpler pattern: make the jobs smart.

We let it add a new job on every save, then wrap the job’s logic in a conditional. If the conditions are still the same as when it was scheduled, go ahead and do it. If not, we assume this job is unneeded and just let it complete without doing anything. The other scheduled job will handle it.

def deliver_tweet
  if !tweeted? && >= tweet_at
    client = => user.twitter_oauth_token, :oauth_token_secret => user.twitter_oauth_secret)
    update_attributes :tweeted => true
handle_asynchronously :deliver_tweet, :run_at => {|p| p.tweet_at }

Testing with Cucumber

Let me provide you with a quick scenario on how I’d test this with Cucumber using FakeWeb and Timecop. Factory Girl step definitions handle most of this scenario; we only need a few custom steps.

Scenario: Can manage blog posts
  Given the following user exists:
    | email             | password |
    | | testing  |
  And I sign in with ""
  And I am tracking twitter traffic

  When I follow "New Post"
  And I fill in "Title" with "My First Pet"
  And I fill in "Content" with "My first pet was a cat named Snowball…"
  And I fill in "Publish At" with "6:00 PM"
  And I check "Tweet this?"
  And I fill in "minutes before" with "10"
  And I fill in "Tweet content" with "Get ready to learn about my first pet!"
  And I press "Save"

  Then I should see "My First Pet"
  And "My First Pet" should have a scheduled tweet for "5:50 PM"

  When the time is "5:51 PM"
  Then a tweet should be posted with "Get ready to learn about my first pet!"

Register the twitter status update uri with FakeWeb.

# features/step_definitions/common_steps.rb

Given 'I am tracking twitter traffic' do
  FakeWeb.register_uri(:post, '', :status => ["200", "OK"])

Ensure that after the post is created, the tweet_at is scheduled close to the time we think it should be.

# features/step_definitions/post_steps.rb

Then /^"([^"]*)" should have a scheduled tweet for "([^"]*)"$/ do |title, time|
  post = Post.where(:title => title).first

  post.tweet_at.should be_close(, 1.minute)

Using Timecop, travel through time and run the delayed jobs.

# features/step_definitions/common_steps.rb

When /^the time is "([^"]*)"$/ do |time|
  Given "delayed jobs are run"

Given 'delayed jobs are run' do
  success, fail =
    puts Delayed::Job.all.reject{|j| j.last_error.blank? }.to_yaml
    raise "#{fail}/#{success} delayed jobs failed."

Then, we have one last step to make sure that the tweet was delivered and was caught by FakeWeb.

# features/step_definitions/post_steps.rb

Then /^a tweet should be posted with "([^"]*)"$/ do |tweet|
  URI.decode(FakeWeb.last_request.body).gsub('+', ' ').should include(tweet)

Whew! We did it. Now go schedule whatever you need.

Photo of Brian Ryckbost

Brian is vice president and a veteran software developer. He pairs his technical vision with an entrepreneurial mind to drive the company forward. A graduate of Calvin College with a degree in Computer Science, he lives in Holland, MI with his wife and three kids.

He has never seen the classic 1984 comedy Ghostbusters.


Post a Comment

(optional — will be included as a link.)
  1. Hey Brian,

    I’m using delayed job in a similar fashion to you.  I set each jobs run_at field to be an hour after the last job.  However, when I run rake jobs:worker it immediately processes all jobs rather than waiting to run at the run_at time.  I’m running in development, do you have any idea if that would matter?  Or have you had any similar problem?
    blake johnson
    June 12, 2011 at 0:27 AM
  2. Would be nice to have a full code demonstrating this somewhere
    Aditya H
    August 09, 2011 at 5:14 AM
  3. @Aditya: I’ll try and build out an example app this weekend.

    August 11, 2011 at 13:03 PM
  4. @blake, my guess is that your run_at time is being set to UTC and it is in the past. I know your comment is old, but for the sake of anyone else who may come across this here is a post from Greg Benedict with a solution

    October 16, 2011 at 13:17 PM
  5. Thanks for sharing…its intersting visit naukri baato
    part time jobs

    June 14, 2017 at 9:27 AM
  6. Thank you for the useful information. You might be interested in Chandigarh Municipal Corporation JE Previous Year Question Papers

    September 07, 2017 at 12:53 PM
  7. Realy Nice website and Thanks for sharing the details here

    September 09, 2017 at 11:33 AM
  8. Graet

    September 09, 2017 at 11:35 AM
    sadhana reddy
    September 09, 2017 at 11:40 AM