Skip to contentSkip to navigationSkip to topbar
Page toolsOn this page
Looking for more inspiration?Visit the

Workflow Automation with Ruby and Rails


A workflow defines how your service providers (agents, hosts, customer service reps, and administrators) interact with your customers.

This tutorial builds a Ruby on Rails web app for finding and booking vacation properties, 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

This app uses the Twilio REST API to send users messages at important junctures, such as sending an SMS when a reservation request arrives.

Application routes

application-routes page anchor

config/routes.rb

1
Rails.application.routes.draw do
2
3
get "login/", to: "sessions#login", as: 'login'
4
get "logout/", to: "sessions#logout"
5
post "login_attempt/", to: "sessions#login_attempt"
6
7
resources :users, only: [:new, :create, :show]
8
9
resources :vacation_properties, path: "/properties"
10
resources :reservations, only: [:new, :create]
11
post "reservations/incoming", to: 'reservations#accept_or_reject', as: 'incoming'
12
13
# Home page
14
root 'main#index', as: 'home'
15
16
end

The Vacation Property Model

the-vacation-property-model page anchor

The VacationProperty model belongs to the User who created it (the host) 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:

1
bin/rails generate scaffold VacationProperty
2

The Rails generator creates all of the routes, controllers, and views, giving you a fully functional CRUD interface out of the box.

app/models/vacation_property.rb

1
class VacationProperty < ActiveRecord::Base
2
belongs_to :user # host
3
has_many :reservations
4
has_many :users, through: :reservations #guests
5
end

The Reservation model is at the center of the workflow for this application. It keeps 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

1
class Reservation < ActiveRecord::Base
2
validates :name, presence: true
3
validates :phone_number, presence: true
4
5
enum status: [ :pending, :confirmed, :rejected ]
6
7
belongs_to :vacation_property
8
belongs_to :user
9
10
def notify_host(force = true)
11
@host = User.find(self.vacation_property[:user_id])
12
13
# Don't send the message if we have more than one or we aren't being forced
14
if @host.pending_reservations.length > 1 or !force
15
return
16
else
17
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
18
19
'#{self.message}'
20
21
Reply [accept] or [reject]."
22
23
@host.send_message_via_sms(message)
24
end
25
end
26
27
def confirm!
28
self.status = "confirmed"
29
self.save!
30
end
31
32
def reject!
33
self.status = "rejected"
34
self.save!
35
end
36
37
def notify_guest
38
@guest = User.find_by(phone_number: self.phone_number)
39
40
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
41
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
42
@guest.send_message_via_sms(message)
43
end
44
end
45
end

Since the reservation can only have one guest in this example, the model assigns a name and phone_number directly.


First, validate some key properties and define the associations so that you can later look up those relationships through the model. (For more context, the Rails guide(link takes you to an external page) explains models and associations.)

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 an integer in the table, while giving each status a searchable name. Here is an example:

1
# reservation.pending! status: 0
2
reservation.status = "confirmed"
3
reservation.confirmed? # => true
4

Validators and Foreign Key for the Reservation model

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

app/models/reservation.rb

1
class Reservation < ActiveRecord::Base
2
validates :name, presence: true
3
validates :phone_number, presence: true
4
5
enum status: [ :pending, :confirmed, :rejected ]
6
7
belongs_to :vacation_property
8
belongs_to :user
9
10
def notify_host(force = true)
11
@host = User.find(self.vacation_property[:user_id])
12
13
# Don't send the message if we have more than one or we aren't being forced
14
if @host.pending_reservations.length > 1 or !force
15
return
16
else
17
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
18
19
'#{self.message}'
20
21
Reply [accept] or [reject]."
22
23
@host.send_message_via_sms(message)
24
end
25
end
26
27
def confirm!
28
self.status = "confirmed"
29
self.save!
30
end
31
32
def reject!
33
self.status = "rejected"
34
self.save!
35
end
36
37
def notify_guest
38
@guest = User.find_by(phone_number: self.phone_number)
39
40
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
41
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
42
@guest.send_message_via_sms(message)
43
end
44
end
45
end

The Reservations Controller

the-reservations-controller page anchor

The reservations controller posts reservation details to the create route from the vacation property page.

After creating the reservation, the controller notifies the host that a request is pending. After the host accepts or rejects it, the controller notifies the guest of the result.

While the Reservation model handles the notification, keep these actions in the controller to make the intent clear.

The Reservation Controller

the-reservation-controller page anchor

Create a new reservation

1
class ReservationsController < ApplicationController
2
3
# GET /vacation_properties/new
4
def new
5
@reservation = Reservation.new
6
end
7
8
def create
9
@vacation_property = VacationProperty.find(params[:reservation][:property_id])
10
@reservation = @vacation_property.reservations.create(reservation_params)
11
12
if @reservation.save
13
flash[:notice] = "Sending your reservation request now."
14
@reservation.notify_host
15
redirect_to @vacation_property
16
else
17
flast[:danger] = @reservation.errors
18
end
19
end
20
21
# webhook for twilio incoming message from host
22
def accept_or_reject
23
incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
24
sms_input = params[:Body].downcase
25
begin
26
@host = User.find_by(phone_number: incoming)
27
@reservation = @host.pending_reservation
28
29
if sms_input == "accept" || sms_input == "yes"
30
@reservation.confirm!
31
else
32
@reservation.reject!
33
end
34
35
@host.check_for_reservations_pending
36
37
sms_reponse = "You have successfully #{@reservation.status} the reservation."
38
respond(sms_reponse)
39
rescue
40
sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
41
respond(sms_reponse)
42
end
43
end
44
45
private
46
# Send an SMS back to the Subscriber
47
def respond(message)
48
response = Twilio::TwiML::Response.new do |r|
49
r.Message message
50
end
51
render text: response.text
52
end
53
54
# Never trust parameters from the scary internet, only allow the white list through.
55
def reservation_params
56
params.require(:reservation).permit(:name, :phone_number, :message)
57
end
58
59
end

To notify the host, look them up and send them an SMS message. To ensure hosts respond to the correct reservation inquiry and don't get spammed, the app handles pending reservations carefully.

The app solves both problems as follows:

  • 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.

To do this, create a helper method on the User model to surface the pending_reservations method on a user.

If the host has only one pending reservation, the app sends an SMS 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

1
class Reservation < ActiveRecord::Base
2
validates :name, presence: true
3
validates :phone_number, presence: true
4
5
enum status: [ :pending, :confirmed, :rejected ]
6
7
belongs_to :vacation_property
8
belongs_to :user
9
10
def notify_host(force = true)
11
@host = User.find(self.vacation_property[:user_id])
12
13
# Don't send the message if we have more than one or we aren't being forced
14
if @host.pending_reservations.length > 1 or !force
15
return
16
else
17
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
18
19
'#{self.message}'
20
21
Reply [accept] or [reject]."
22
23
@host.send_message_via_sms(message)
24
end
25
end
26
27
def confirm!
28
self.status = "confirmed"
29
self.save!
30
end
31
32
def reject!
33
self.status = "rejected"
34
self.save!
35
end
36
37
def notify_guest
38
@guest = User.find_by(phone_number: self.phone_number)
39
40
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
41
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
42
@guest.send_message_via_sms(message)
43
end
44
end
45
end

A single User model represents both the guests and the hosts who use Airtng.

First, validate the uniqueness of the user. The phone_number attribute must be unique because the app uses it to look up User records on incoming SMS messages.

Then, set up the associations for querying reservations.

Validators and relationships of the User Model

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

app/models/user.rb

1
class User < ActiveRecord::Base
2
has_secure_password
3
4
validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
5
validates :name, presence: true
6
validates :country_code, presence: true
7
validates :phone_number, presence: true, uniqueness: true
8
validates_length_of :password, in: 6..20, on: :create
9
10
has_many :vacation_properties
11
has_many :reservations, through: :vacation_properties
12
13
def send_message_via_sms(message)
14
@app_number = ENV['TWILIO_NUMBER']
15
@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
16
phone_number = "+#{country_code}#{self.phone_number}"
17
sms_message = @client.account.messages.create(
18
from: @app_number,
19
to: phone_number,
20
body: message,
21
)
22
end
23
24
def check_for_reservations_pending
25
if pending_reservation
26
pending_reservation.notify_host(true)
27
end
28
end
29
30
def pending_reservation
31
self.reservations.where(status: "pending").first
32
end
33
34
def pending_reservations
35
self.reservations.where(status: "pending")
36
end
37
38
end

Send an SMS from the User model

send-an-sms-from-the-user-model page anchor

Because the app only sends text messages when communicating with specific users, the send_message_via_sms method lives on the User class. These seven lines are all you need to send an SMS with Ruby and Twilio, in two steps:

  1. Look up the app's phone number.
  2. Initialize the Twilio client and build the message.

Whenever the app needs to communicate with a user, whether host or guest, it passes a message to this method to send a text.

The helper methods for finding pending_reservations appear below this method.

Use Twilio client to send an SMS

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

app/models/user.rb

1
class User < ActiveRecord::Base
2
has_secure_password
3
4
validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
5
validates :name, presence: true
6
validates :country_code, presence: true
7
validates :phone_number, presence: true, uniqueness: true
8
validates_length_of :password, in: 6..20, on: :create
9
10
has_many :vacation_properties
11
has_many :reservations, through: :vacation_properties
12
13
def send_message_via_sms(message)
14
@app_number = ENV['TWILIO_NUMBER']
15
@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
16
phone_number = "+#{country_code}#{self.phone_number}"
17
sms_message = @client.account.messages.create(
18
from: @app_number,
19
to: phone_number,
20
body: message,
21
)
22
end
23
24
def check_for_reservations_pending
25
if pending_reservation
26
pending_reservation.notify_host(true)
27
end
28
end
29
30
def pending_reservation
31
self.reservations.where(status: "pending").first
32
end
33
34
def pending_reservations
35
self.reservations.where(status: "pending")
36
end
37
38
end

Handle Incoming Messages

handle-incoming-messages page anchor

The accept_or_reject controller handles the incoming Twilio request and does three things, so the host can accept or reject a request by SMS:

  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 includes parameters such as the From phone number and the message Body.

Use the From parameter to look up the host and check for pending reservations. If a pending reservation exists, use the message body to check whether the host accepted or rejected it.

Then, return a TwiML response to send a message back to the user.

A Rails controller usually has a template that renders a webpage. Here, the only request comes from Twilio's API, so no public page is needed. Instead, the controller uses Twilio's Ruby library to render a custom TwiML response as raw XML.

Webhook for handling Host's decision

webhook-for-handling-hosts-decision page anchor

app/controllers/reservations_controller.rb

1
class ReservationsController < ApplicationController
2
3
# GET /vacation_properties/new
4
def new
5
@reservation = Reservation.new
6
end
7
8
def create
9
@vacation_property = VacationProperty.find(params[:reservation][:property_id])
10
@reservation = @vacation_property.reservations.create(reservation_params)
11
12
if @reservation.save
13
flash[:notice] = "Sending your reservation request now."
14
@reservation.notify_host
15
redirect_to @vacation_property
16
else
17
flast[:danger] = @reservation.errors
18
end
19
end
20
21
# webhook for twilio incoming message from host
22
def accept_or_reject
23
incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
24
sms_input = params[:Body].downcase
25
begin
26
@host = User.find_by(phone_number: incoming)
27
@reservation = @host.pending_reservation
28
29
if sms_input == "accept" || sms_input == "yes"
30
@reservation.confirm!
31
else
32
@reservation.reject!
33
end
34
35
@host.check_for_reservations_pending
36
37
sms_reponse = "You have successfully #{@reservation.status} the reservation."
38
respond(sms_reponse)
39
rescue
40
sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
41
respond(sms_reponse)
42
end
43
end
44
45
private
46
# Send an SMS back to the Subscriber
47
def respond(message)
48
response = Twilio::TwiML::Response.new do |r|
49
r.Message message
50
end
51
render text: response.text
52
end
53
54
# Never trust parameters from the scary internet, only allow the white list through.
55
def reservation_params
56
params.require(:reservation).permit(:name, :phone_number, :message)
57
end
58
59
end

The final step in the workflow notifies the guest that their reservation was booked or rejected.

The reservations_controller calls this method when it updates the reservation status. The method does the following:

  • Looks up the guest with the reservation.phone_number.
  • If the status changed to an expected result, notifies the guest of the change.

To send the SMS message to the guest, the method calls 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

1
class Reservation < ActiveRecord::Base
2
validates :name, presence: true
3
validates :phone_number, presence: true
4
5
enum status: [ :pending, :confirmed, :rejected ]
6
7
belongs_to :vacation_property
8
belongs_to :user
9
10
def notify_host(force = true)
11
@host = User.find(self.vacation_property[:user_id])
12
13
# Don't send the message if we have more than one or we aren't being forced
14
if @host.pending_reservations.length > 1 or !force
15
return
16
else
17
message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:
18
19
'#{self.message}'
20
21
Reply [accept] or [reject]."
22
23
@host.send_message_via_sms(message)
24
end
25
end
26
27
def confirm!
28
self.status = "confirmed"
29
self.save!
30
end
31
32
def reject!
33
self.status = "rejected"
34
self.save!
35
end
36
37
def notify_guest
38
@guest = User.find_by(phone_number: self.phone_number)
39
40
if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
41
message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
42
@guest.send_message_via_sms(message)
43
end
44
end
45
end

Airtng now has an SMS-based workflow in place, and you're ready to add a workflow to your own application.


Here are other tutorials you might pursue with Ruby, Rails, and Twilio:

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.