Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Workflow Automation with Ruby and Rails


One of the more abstract concepts you'll handle when building your business is what the workflow will look like.

At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.

To illustrate a very real-world example, today we'll build a Ruby on Rails webapp for finding and booking vacation properties — tentatively called Airtng.

Here's how it'll work:

  1. A host creates a vacation property listing
  2. A guest requests a reservation for a property
  3. The host receives an SMS notifying them of the reservation request. The host can either Accept or Reject the reservation
  4. The guest is notified whether a request was rejected or accepted

Learn how Airbnb used Twilio SMS to streamline the rental experience for 60M+ travelers around the world.(link takes you to an external page)


Workflow Building Blocks

workflow-building-blocks page anchor

We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:

  • Sending Messages with Twilio API

Application routes

application-routes page anchor

config/routes.rb


_16
Rails.application.routes.draw do
_16
_16
get "login/", to: "sessions#login", as: 'login'
_16
get "logout/", to: "sessions#logout"
_16
post "login_attempt/", to: "sessions#login_attempt"
_16
_16
resources :users, only: [:new, :create, :show]
_16
_16
resources :vacation_properties, path: "/properties"
_16
resources :reservations, only: [:new, :create]
_16
post "reservations/incoming", to: 'reservations#accept_or_reject', as: 'incoming'
_16
_16
# Home page
_16
root 'main#index', as: 'home'
_16
_16
end


The Vacation Property Model

the-vacation-property-model page anchor

The VacationProperty model belongs to the User who created it (we'll call this user the host moving forward) and contains only two properties, a description and an image_url.

It has two associations(link takes you to an external page) in that it has many reservations and therefore many users through those reservations.

The best way to generate the model and all of the basic CRUD scaffolding(link takes you to an external page) we'll need is to use the Rails command line(link takes you to an external page) tool:


_10
bin/rails generate scaffold VacationProperty
_10
description:string image_url:string

One of the benefits of using the Rails generator is that it creates all of our routes, controllers and views so that we have a fully functional CRUD interface out of the box. Nifty!

app/models/vacation_property.rb


_10
class VacationProperty < ActiveRecord::Base
_10
belongs_to :user # host
_10
has_many :reservations
_10
has_many :users, through: :reservations #guests
_10
end

Let's jump into the stew and look next at the Reservation mode.


The Reservation model is at the center of the workflow for this application. It is responsible for keeping track of:

  • the VacationProperty it is associated with
  • the User who owns that vacation property (the host )
  • the guest name and phone number

app/models/reservation.rb


_45
class Reservation < ActiveRecord::Base
_45
validates :name, presence: true
_45
validates :phone_number, presence: true
_45
_45
enum status: [ :pending, :confirmed, :rejected ]
_45
_45
belongs_to :vacation_property
_45
belongs_to :user
_45
_45
def notify_host(force = true)
_45
@host = User.find(self.vacation_property[:user_id])
_45
_45
# Don't send the message if we have more than one or we aren't being forced
_45
if @host.pending_reservations.length > 1 or !force
_45
return
_45
else
_45
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
_45
_45
'#{self.message}'
_45
_45
Reply [accept] or [reject]."
_45
_45
@host.send_message_via_sms(message)
_45
end
_45
end
_45
_45
def confirm!
_45
self.status = "confirmed"
_45
self.save!
_45
end
_45
_45
def reject!
_45
self.status = "rejected"
_45
self.save!
_45
end
_45
_45
def notify_guest
_45
@guest = User.find_by(phone_number: self.phone_number)
_45
_45
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
_45
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
_45
@guest.send_message_via_sms(message)
_45
end
_45
end
_45
end

Since the reservation can only have one guest for our example we simplified the model by assigning a name and phone_number directly.

We'll cover how we did this later. Next, however, we'll zoom in on the reservation status.


First we validate some key properties and define the associations so that we can later lookup those relationships through the model. (If you'd like more context, the Rails guide(link takes you to an external page) explains models and associations quite well.)

The main property we need to enable a reservation workflow is some sort of status that we can monitor. This is a perfect candidate for an enumerated status attribute.

Enumerated Attributes

enumerated-attributes page anchor

Enumerated attributes(link takes you to an external page) allow us to store a simple integer in the table, while giving each status a searchable name. Here is an example:


_10
# reservation.pending! status: 0
_10
reservation.status = "confirmed"
_10
reservation.confirmed? # => true

Validators and Foreign Key for the Reservation model

validators-and-foreign-key-for-the-reservation-model page anchor

app/models/reservation.rb


_45
class Reservation < ActiveRecord::Base
_45
validates :name, presence: true
_45
validates :phone_number, presence: true
_45
_45
enum status: [ :pending, :confirmed, :rejected ]
_45
_45
belongs_to :vacation_property
_45
belongs_to :user
_45
_45
def notify_host(force = true)
_45
@host = User.find(self.vacation_property[:user_id])
_45
_45
# Don't send the message if we have more than one or we aren't being forced
_45
if @host.pending_reservations.length > 1 or !force
_45
return
_45
else
_45
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
_45
_45
'#{self.message}'
_45
_45
Reply [accept] or [reject]."
_45
_45
@host.send_message_via_sms(message)
_45
end
_45
end
_45
_45
def confirm!
_45
self.status = "confirmed"
_45
self.save!
_45
end
_45
_45
def reject!
_45
self.status = "rejected"
_45
self.save!
_45
end
_45
_45
def notify_guest
_45
@guest = User.find_by(phone_number: self.phone_number)
_45
_45
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
_45
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
_45
@guest.send_message_via_sms(message)
_45
end
_45
end
_45
end

Once we have an attribute that can trigger our workflow events, it's time to write some callbacks. Let's look there next.


The Reservations Controller

the-reservations-controller page anchor

We'll be posting our reservation details to the create route from the vacation property page.

After we create the reservation we want to notify the host that she has a request pending. After she accepts or rejects it we want to notify the guest of the news.

While the Reservation model handles the notification, we want to keep all these actions in the controller to show our intentions.

The Reservation Controller

the-reservation-controller page anchor

Create a new reservation


_59
class ReservationsController < ApplicationController
_59
_59
# GET /vacation_properties/new
_59
def new
_59
@reservation = Reservation.new
_59
end
_59
_59
def create
_59
@vacation_property = VacationProperty.find(params[:reservation][:property_id])
_59
@reservation = @vacation_property.reservations.create(reservation_params)
_59
_59
if @reservation.save
_59
flash[:notice] = "Sending your reservation request now."
_59
@reservation.notify_host
_59
redirect_to @vacation_property
_59
else
_59
flast[:danger] = @reservation.errors
_59
end
_59
end
_59
_59
# webhook for twilio incoming message from host
_59
def accept_or_reject
_59
incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
_59
sms_input = params[:Body].downcase
_59
begin
_59
@host = User.find_by(phone_number: incoming)
_59
@reservation = @host.pending_reservation
_59
_59
if sms_input == "accept" || sms_input == "yes"
_59
@reservation.confirm!
_59
else
_59
@reservation.reject!
_59
end
_59
_59
@host.check_for_reservations_pending
_59
_59
sms_reponse = "You have successfully #{@reservation.status} the reservation."
_59
respond(sms_reponse)
_59
rescue
_59
sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
_59
respond(sms_reponse)
_59
end
_59
end
_59
_59
private
_59
# Send an SMS back to the Subscriber
_59
def respond(message)
_59
response = Twilio::TwiML::Response.new do |r|
_59
r.Message message
_59
end
_59
render text: response.text
_59
end
_59
_59
# Never trust parameters from the scary internet, only allow the white list through.
_59
def reservation_params
_59
params.require(:reservation).permit(:name, :phone_number, :message)
_59
end
_59
_59
end

Next up, let's take a look at how exactly we'll notify the lucky host.


In theory, notifying the host should be as simple as looking up the host User and send her an SMS. However:

How do we ensure our hosts are: a) responding to the correct reservation inquiry and b) not getting spammed?

Our simple solution to both problems:

  • We only notify the host of the oldest pending reservation.
  • We don't send another SMS until the host has dealt with the last reservation.

The easiest way to surface pending_reservations for a user is to create a helper method on the User model. We'll go over that in the next step.

If in fact the host only has one pending reservation, we're going to fire an SMS off to the host immediately.

Send an SMS to notify the host of a new reservation

send-an-sms-to-notify-the-host-of-a-new-reservation page anchor

app/models/reservation.rb


_45
class Reservation < ActiveRecord::Base
_45
validates :name, presence: true
_45
validates :phone_number, presence: true
_45
_45
enum status: [ :pending, :confirmed, :rejected ]
_45
_45
belongs_to :vacation_property
_45
belongs_to :user
_45
_45
def notify_host(force = true)
_45
@host = User.find(self.vacation_property[:user_id])
_45
_45
# Don't send the message if we have more than one or we aren't being forced
_45
if @host.pending_reservations.length > 1 or !force
_45
return
_45
else
_45
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
_45
_45
'#{self.message}'
_45
_45
Reply [accept] or [reject]."
_45
_45
@host.send_message_via_sms(message)
_45
end
_45
end
_45
_45
def confirm!
_45
self.status = "confirmed"
_45
self.save!
_45
end
_45
_45
def reject!
_45
self.status = "rejected"
_45
self.save!
_45
end
_45
_45
def notify_guest
_45
@guest = User.find_by(phone_number: self.phone_number)
_45
_45
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
_45
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
_45
@guest.send_message_via_sms(message)
_45
end
_45
end
_45
end

Let's now take a look at the User model.


We have one model for both the guests and the hosts who are using Airtng.

When Airtng takes off it will merit creating two more classes that inherit from the base User class. Since we're still on the ground this should suit us fine (to boldly stay...).

First, we validate the 'uniqueness' of our user - they should be unique, just like everyone else. Specifically, it is important we ensure that the phone_number attribute is unique since we will use this to look up User records on incoming SMSes.

After that, we set up our associations for when we need to query for reservations.

Validators and relationships of the User Model

validators-and-relationships-of-the-user-model page anchor

app/models/user.rb


_38
class User < ActiveRecord::Base
_38
has_secure_password
_38
_38
validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
_38
validates :name, presence: true
_38
validates :country_code, presence: true
_38
validates :phone_number, presence: true, uniqueness: true
_38
validates_length_of :password, in: 6..20, on: :create
_38
_38
has_many :vacation_properties
_38
has_many :reservations, through: :vacation_properties
_38
_38
def send_message_via_sms(message)
_38
@app_number = ENV['TWILIO_NUMBER']
_38
@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
_38
phone_number = "+#{country_code}#{self.phone_number}"
_38
sms_message = @client.account.messages.create(
_38
from: @app_number,
_38
to: phone_number,
_38
body: message,
_38
)
_38
end
_38
_38
def check_for_reservations_pending
_38
if pending_reservation
_38
pending_reservation.notify_host(true)
_38
end
_38
end
_38
_38
def pending_reservation
_38
self.reservations.where(status: "pending").first
_38
end
_38
_38
def pending_reservations
_38
self.reservations.where(status: "pending")
_38
end
_38
_38
end

Arguably the most important task delegated to our User model is to send an SMS to the user when our app requests it, let's take a look.


Send Messages Into the Void

send-messages-into-the-void page anchor

Since we only send text messages in our application when we're communicating with specific users, it makes sense to create this function on the User class. And yes: these 7 lines are all you need to send SMSes with Ruby and Twilio! It's really just two steps:

  1. We look up our app's phone number.
  2. We initiate our Twilio client and build the message.

Now whenever we need to communicate with a user, whether host or guest, we can pass a message to this user method and... Voilà! We've sent them a text.

(If you peek below this method you'll see the helper methods for finding pending_reservations that we mentioned previously.)

Use Twilio client to send an SMS

use-twilio-client-to-send-an-sms page anchor

app/models/user.rb


_38
class User < ActiveRecord::Base
_38
has_secure_password
_38
_38
validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
_38
validates :name, presence: true
_38
validates :country_code, presence: true
_38
validates :phone_number, presence: true, uniqueness: true
_38
validates_length_of :password, in: 6..20, on: :create
_38
_38
has_many :vacation_properties
_38
has_many :reservations, through: :vacation_properties
_38
_38
def send_message_via_sms(message)
_38
@app_number = ENV['TWILIO_NUMBER']
_38
@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
_38
phone_number = "+#{country_code}#{self.phone_number}"
_38
sms_message = @client.account.messages.create(
_38
from: @app_number,
_38
to: phone_number,
_38
body: message,
_38
)
_38
end
_38
_38
def check_for_reservations_pending
_38
if pending_reservation
_38
pending_reservation.notify_host(true)
_38
end
_38
end
_38
_38
def pending_reservation
_38
self.reservations.where(status: "pending").first
_38
end
_38
_38
def pending_reservations
_38
self.reservations.where(status: "pending")
_38
end
_38
_38
end

Now we need a way to handle incoming texts so our lucky host can accept or reject a request. Let's look there next.


Handle Incoming Messages

handle-incoming-messages page anchor

The accept_or_reject controller handles our incoming Twilio request and does three things:

  1. Check for a pending reservation the user owns
  2. Update the status of the reservation
  3. Respond to the host (and guest)

An incoming request from Twilio comes with some helpful parameters including the From phone number and the message Body.

We'll use the From parameter to lookup the host and check if she has any pending reservations. If she does, we'll use the message body to check if she accepted or rejected the reservation.

Then we'll redirect the request to a TwiML response to send a message back to the user.


Usually a Rails controller has a template associated with it that renders a webpage. In our case, the only request being made will be by Twilio's API so we don't need a public page. Instead we're using Twilio's Ruby API to render a custom TwiML response as raw XML on the page.

Webhook for handling Host's decision

webhook-for-handling-hosts-decision page anchor

app/controllers/reservations_controller.rb


_59
class ReservationsController < ApplicationController
_59
_59
# GET /vacation_properties/new
_59
def new
_59
@reservation = Reservation.new
_59
end
_59
_59
def create
_59
@vacation_property = VacationProperty.find(params[:reservation][:property_id])
_59
@reservation = @vacation_property.reservations.create(reservation_params)
_59
_59
if @reservation.save
_59
flash[:notice] = "Sending your reservation request now."
_59
@reservation.notify_host
_59
redirect_to @vacation_property
_59
else
_59
flast[:danger] = @reservation.errors
_59
end
_59
end
_59
_59
# webhook for twilio incoming message from host
_59
def accept_or_reject
_59
incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
_59
sms_input = params[:Body].downcase
_59
begin
_59
@host = User.find_by(phone_number: incoming)
_59
@reservation = @host.pending_reservation
_59
_59
if sms_input == "accept" || sms_input == "yes"
_59
@reservation.confirm!
_59
else
_59
@reservation.reject!
_59
end
_59
_59
@host.check_for_reservations_pending
_59
_59
sms_reponse = "You have successfully #{@reservation.status} the reservation."
_59
respond(sms_reponse)
_59
rescue
_59
sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
_59
respond(sms_reponse)
_59
end
_59
end
_59
_59
private
_59
# Send an SMS back to the Subscriber
_59
def respond(message)
_59
response = Twilio::TwiML::Response.new do |r|
_59
r.Message message
_59
end
_59
render text: response.text
_59
end
_59
_59
# Never trust parameters from the scary internet, only allow the white list through.
_59
def reservation_params
_59
params.require(:reservation).permit(:name, :phone_number, :message)
_59
end
_59
_59
end

Next up, let's see how to notify the guest.


The final step in our workflow is to notify the guest that their reservation has been booked (or, ahem, rejected).

We called this method earlier from the reservations_controller when we updated the reservation status. Here's what it does:

  • We lookup the guest with the reservation.phone_number
  • If the status was changed to an expected result we notify the guest of the change.

And of course all we need to do to send the SMS message to the guest is call the send_message_via_sms method that is present on all users.

Let the guest have the host's decision

let-the-guest-have-the-hosts-decision page anchor

app/models/reservation.rb


_45
class Reservation < ActiveRecord::Base
_45
validates :name, presence: true
_45
validates :phone_number, presence: true
_45
_45
enum status: [ :pending, :confirmed, :rejected ]
_45
_45
belongs_to :vacation_property
_45
belongs_to :user
_45
_45
def notify_host(force = true)
_45
@host = User.find(self.vacation_property[:user_id])
_45
_45
# Don't send the message if we have more than one or we aren't being forced
_45
if @host.pending_reservations.length > 1 or !force
_45
return
_45
else
_45
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
_45
_45
'#{self.message}'
_45
_45
Reply [accept] or [reject]."
_45
_45
@host.send_message_via_sms(message)
_45
end
_45
end
_45
_45
def confirm!
_45
self.status = "confirmed"
_45
self.save!
_45
end
_45
_45
def reject!
_45
self.status = "rejected"
_45
self.save!
_45
end
_45
_45
def notify_guest
_45
@guest = User.find_by(phone_number: self.phone_number)
_45
_45
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
_45
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
_45
@guest.send_message_via_sms(message)
_45
end
_45
end
_45
end

Thank you so much for your help! Airtng now has a nice SMS based workflow in place and you're ready to add a workflow to your own application.

Let's look at some other features you might enjoy adding for your use cases.


Ruby and Rails and Twilio: what an excellent combo. Here are a couple other ideas you might pursue:

Masked Phone Numbers

Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS.

Automated Survey

Collect instant feedback from your customers with SMS or Voice.

Thanks for checking this tutorial out! Tweet to us @twilio(link takes you to an external page) with what you're building!


Rate this page: