---
"@context": https://schema.org
"@type": TechArticle
"@id": https://www.twilio.com/docs/messaging/tutorials/workflow-automation/ruby#article
headline: Workflow Automation with Ruby and Rails
description: Build an SMS workflow automation app with Ruby on Rails and Twilio. Send and receive messages, handle reservation requests, and notify hosts and guests.
url: https://www.twilio.com/docs/messaging/tutorials/workflow-automation/ruby
inLanguage: en
dateModified: 2026-07-21T11:17:03.000Z
author:
  "@type": Organization
  name: Twilio Developer Education Team
publisher:
  "@type": Organization
  name: Twilio
---

# 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.](https://customers.twilio.com/en-us/airbnb)*

## Workflow Building Blocks

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

```rb title="Application routes" description="config/routes.rb"
Rails.application.routes.draw do

  get "login/", to: "sessions#login", as: 'login'
  get "logout/", to: "sessions#logout"
  post "login_attempt/", to: "sessions#login_attempt"

  resources :users, only: [:new, :create, :show]

  resources :vacation_properties, path: "/properties"
  resources :reservations, only: [:new, :create]
  post "reservations/incoming", to: 'reservations#accept_or_reject', as: 'incoming'

  # Home page
  root 'main#index', as: 'home'

end
```

## The Vacation Property Model

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](https://guides.rubyonrails.org/association_basics.html) 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](https://curriculum.railsbridge.org/intro-to-rails/CRUD_with_scaffolding) we'll need is to use the [Rails command line](https://guides.rubyonrails.org/command_line.html) tool:

```bash
bin/rails generate scaffold VacationProperty

```

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

```rb title="Vacation Property Model" description="app/models/vacation_property.rb"
class VacationProperty < ActiveRecord::Base
  belongs_to :user # host
  has_many :reservations
  has_many :users, through: :reservations #guests
end
```

## The Reservation Model

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

```rb title="The Reservation Model" description="app/models/reservation.rb"
class Reservation < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true

  enum status: [ :pending, :confirmed, :rejected ]

  belongs_to :vacation_property
  belongs_to :user

  def notify_host(force = true)
    @host = User.find(self.vacation_property[:user_id])

    # Don't send the message if we have more than one or we aren't being forced
    if @host.pending_reservations.length > 1 or !force
      return
    else
      message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:

      '#{self.message}'

      Reply [accept] or [reject]."

      @host.send_message_via_sms(message)
    end
  end

  def confirm!
    self.status = "confirmed"
    self.save!
  end

  def reject!
    self.status = "rejected"
    self.save!
  end

  def notify_guest
    @guest = User.find_by(phone_number: self.phone_number)

    if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
      message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
      @guest.send_message_via_sms(message)
    end
  end
end
```

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

## Reservation Status

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](https://guides.rubyonrails.org/active_record_basics.html) 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](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) allow us to store an integer in the table, while giving each status a searchable name. Here is an example:

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

```

```rb title="Validators and Foreign Key for the Reservation model" description="app/models/reservation.rb"
# !mark(1:9)
class Reservation < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true

  enum status: [ :pending, :confirmed, :rejected ]

  belongs_to :vacation_property
  belongs_to :user

  def notify_host(force = true)
    @host = User.find(self.vacation_property[:user_id])

    # Don't send the message if we have more than one or we aren't being forced
    if @host.pending_reservations.length > 1 or !force
      return
    else
      message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:

      '#{self.message}'

      Reply [accept] or [reject]."

      @host.send_message_via_sms(message)
    end
  end

  def confirm!
    self.status = "confirmed"
    self.save!
  end

  def reject!
    self.status = "rejected"
    self.save!
  end

  def notify_guest
    @guest = User.find_by(phone_number: self.phone_number)

    if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
      message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
      @guest.send_message_via_sms(message)
    end
  end
end
```

## The Reservations Controller

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.

```rb title="The Reservation Controller" description="Create a new reservation"
# !mark(8:19)
class ReservationsController < ApplicationController

  # GET /vacation_properties/new
  def new
    @reservation = Reservation.new
  end

  def create
    @vacation_property = VacationProperty.find(params[:reservation][:property_id])
    @reservation = @vacation_property.reservations.create(reservation_params)

    if @reservation.save
      flash[:notice] = "Sending your reservation request now."
      @reservation.notify_host
      redirect_to @vacation_property
    else
      flast[:danger] = @reservation.errors
    end
  end

  # webhook for twilio incoming message from host
  def accept_or_reject
    incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
    sms_input = params[:Body].downcase
    begin
      @host = User.find_by(phone_number: incoming)
      @reservation = @host.pending_reservation

      if sms_input == "accept" || sms_input == "yes"
        @reservation.confirm!
      else
        @reservation.reject!
      end

      @host.check_for_reservations_pending

      sms_reponse = "You have successfully #{@reservation.status} the reservation."
      respond(sms_reponse)
    rescue
      sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
      respond(sms_reponse)
    end
  end

  private
    # Send an SMS back to the Subscriber
    def respond(message)
      response = Twilio::TwiML::Response.new do |r|
        r.Message message
      end
      render text: response.text
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def reservation_params
      params.require(:reservation).permit(:name, :phone_number, :message)
    end

end
```

## Notify the Host

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.

### Pending Reservations

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.

### Sending the SMS

If the host has only one pending reservation, the app sends an SMS to the host immediately.

```rb title="Send an SMS to notify the host of a new reservation" description="app/models/reservation.rb"
# !mark(10:25)
class Reservation < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true

  enum status: [ :pending, :confirmed, :rejected ]

  belongs_to :vacation_property
  belongs_to :user

  def notify_host(force = true)
    @host = User.find(self.vacation_property[:user_id])

    # Don't send the message if we have more than one or we aren't being forced
    if @host.pending_reservations.length > 1 or !force
      return
    else
      message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:

      '#{self.message}'

      Reply [accept] or [reject]."

      @host.send_message_via_sms(message)
    end
  end

  def confirm!
    self.status = "confirmed"
    self.save!
  end

  def reject!
    self.status = "rejected"
    self.save!
  end

  def notify_guest
    @guest = User.find_by(phone_number: self.phone_number)

    if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
      message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
      @guest.send_message_via_sms(message)
    end
  end
end
```

## The User Model

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.

```rb title="Validators and relationships of the User Model" description="app/models/user.rb"
# !mark(1:11)
class User < ActiveRecord::Base
  has_secure_password

  validates :email,  presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
  validates :name, presence: true
  validates :country_code, presence: true
  validates :phone_number, presence: true, uniqueness: true
  validates_length_of :password, in: 6..20, on: :create

  has_many :vacation_properties
  has_many :reservations, through: :vacation_properties

  def send_message_via_sms(message)
    @app_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    phone_number = "+#{country_code}#{self.phone_number}"
    sms_message = @client.account.messages.create(
      from: @app_number,
      to: phone_number,
      body: message,
    )
  end

  def check_for_reservations_pending
    if pending_reservation
      pending_reservation.notify_host(true)
    end
  end

  def pending_reservation
    self.reservations.where(status: "pending").first
  end

  def pending_reservations
    self.reservations.where(status: "pending")
  end

end
```

## Send an SMS from the User model

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.

```rb title="Use Twilio client to send an SMS" description="app/models/user.rb"
# !mark(13:41)
class User < ActiveRecord::Base
  has_secure_password

  validates :email,  presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true
  validates :name, presence: true
  validates :country_code, presence: true
  validates :phone_number, presence: true, uniqueness: true
  validates_length_of :password, in: 6..20, on: :create

  has_many :vacation_properties
  has_many :reservations, through: :vacation_properties

  def send_message_via_sms(message)
    @app_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    phone_number = "+#{country_code}#{self.phone_number}"
    sms_message = @client.account.messages.create(
      from: @app_number,
      to: phone_number,
      body: message,
    )
  end

  def check_for_reservations_pending
    if pending_reservation
      pending_reservation.notify_host(true)
    end
  end

  def pending_reservation
    self.reservations.where(status: "pending").first
  end

  def pending_reservations
    self.reservations.where(status: "pending")
  end

end
```

## Handle Incoming Messages

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)

### Incoming Twilio request

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.

### TwiML response

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.

```rb title="Webhook for handling Host's decision" description="app/controllers/reservations_controller.rb"
# !mark(21:59)
class ReservationsController < ApplicationController

  # GET /vacation_properties/new
  def new
    @reservation = Reservation.new
  end

  def create
    @vacation_property = VacationProperty.find(params[:reservation][:property_id])
    @reservation = @vacation_property.reservations.create(reservation_params)

    if @reservation.save
      flash[:notice] = "Sending your reservation request now."
      @reservation.notify_host
      redirect_to @vacation_property
    else
      flast[:danger] = @reservation.errors
    end
  end

  # webhook for twilio incoming message from host
  def accept_or_reject
    incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')
    sms_input = params[:Body].downcase
    begin
      @host = User.find_by(phone_number: incoming)
      @reservation = @host.pending_reservation

      if sms_input == "accept" || sms_input == "yes"
        @reservation.confirm!
      else
        @reservation.reject!
      end

      @host.check_for_reservations_pending

      sms_reponse = "You have successfully #{@reservation.status} the reservation."
      respond(sms_reponse)
    rescue
      sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."
      respond(sms_reponse)
    end
  end

  private
    # Send an SMS back to the Subscriber
    def respond(message)
      response = Twilio::TwiML::Response.new do |r|
        r.Message message
      end
      render text: response.text
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def reservation_params
      params.require(:reservation).permit(:name, :phone_number, :message)
    end

end
```

## Notify the Guest

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.

```rb title="Let the guest have the host's decision" description="app/models/reservation.rb"
# !mark(37:44)
class Reservation < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true

  enum status: [ :pending, :confirmed, :rejected ]

  belongs_to :vacation_property
  belongs_to :user

  def notify_host(force = true)
    @host = User.find(self.vacation_property[:user_id])

    # Don't send the message if we have more than one or we aren't being forced
    if @host.pending_reservations.length > 1 or !force
      return
    else
      message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:

      '#{self.message}'

      Reply [accept] or [reject]."

      @host.send_message_via_sms(message)
    end
  end

  def confirm!
    self.status = "confirmed"
    self.save!
  end

  def reject!
    self.status = "rejected"
    self.save!
  end

  def notify_guest
    @guest = User.find_by(phone_number: self.phone_number)

    if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")
      message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."
      @guest.send_message_via_sms(message)
    end
  end
end
```

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

## Where to Next?

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

**[Masked Phone Numbers](/docs/messaging/tutorials/masked-numbers/ruby)**

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

**[Automated Survey](/blog/automated-survey-ruby-rails)**

Collect instant feedback from your customers with SMS or Voice.
