Automated Survey with Ruby and Rails

Have you ever wondered how to create an automated survey that can be answered by phone or SMS?

This tutorial will show how to do it using the Twilio API.

Here's how it works at a high level

Automated Survey Diagram

  1. The end user calls or texts the survey's phone number.
  2. Twilio gets the call or SMS and makes an HTTP request to your application asking for instructions on how to respond.
  3. Your web application serves up TwiML instructions to Gather or Record the user input over the phone, or prompts for text input with Message.
  4. After each question, Twilio makes another request to your server with the user's input. Your application stores this input in its database.
  5. Your application returns a TwiML response to Twilio with instructions to either ask the next question or complete the survey.

Instacart uses Twilio to power their customer service surveys and integrate that feedback into their customer database. Read more here.

Creating a Survey

For your convenience, this application's repository already includes one survey that can be loaded into the database. This survey will be loaded into the database when the app starts as long as the database is configured correctly.

You can modify the survey questions by editing the seeds.rb file located in the root of the repository and re-running the app. You can see more about seeding your database here.

Loading Code Samples...
Language
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
app/controllers/surveys_controller.rb
Automated Survey controller

app/controllers/surveys_controller.rb

We want users to take our survey, so we need to implement a handler for SMS and calls. First, let's take a moment to understand the flow of a Twilio-powered survey as an interview loop.

The Interview Loop

It is helpful to visualize your interaction with a user during a survey as a loop. The chart below shows how that interaction is handled:

Survey Flow Chart

The user can enter an answer for your survey over the phone using either their phone's keypad or by speaking. After each interaction, Twilio will make an HTTP request to your web application with either the string of keys the user pressed or a URL to a recording of their voice input.

It's up to the application to process, store, and respond to the user's input.

Let's dive into this flow to see how it actually works.

Configuring a Twilio Number

To initiate the interview process, we need to configure one of our Twilio numbers to send our web application an HTTP request when we get an incoming call or text.

Click on one of your numbers and configure your Voice and Message URLs to point to your server. In our code, the routes are /surveys/voice and /surveys/sms, respectively.

Automated Survey Webhook Setup 

If you don't already have a server configured to use as your webhook, ngrok is a great tool for testing webhooks locally.

Loading Code Samples...
Language
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
app/controllers/surveys_controller.rb
Voice and SMS endpoints for user survey

app/controllers/surveys_controller.rb

Lets see how we'll handle these webhooks.

Responding to the Twilio Request

After receiveing a call or SMS, Twilio will send a request to the URL specified in that phone number's configuration:

  • /surveys/voice for calls.
  • /surveys/sms for SMS.

Each of these endpoints will receive the request and will use Twilio::TwiML::Response to return a welcome message to the user. For callers, the constructed message will contain a Say verb. For requests coming from a text we'll respond with a Message which will respond with an SMS.

Our response will also include a Redirect verb to redirect to our question enpoint in order to continue the survey flow.

Loading Code Samples...
Language
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
app/controllers/surveys_controller.rb
Welcome message for voice surveys

app/controllers/surveys_controller.rb

Now that we know how to handle requests to our webhooks, let's learn how to keep track of the survey's state.

Question Controller

This controller is used for calls only. It produces the TwiML with the questions in our survey.

The sequence of questions is modeled through the action verb attribute.

Loading Code Samples...
Language
class QuestionsController < ApplicationController
  def show
    question = Question.find(params[:id])
    render xml: Voice::CreateResponse.for(question)
  end
end
app/controllers/questions_controller.rb
Question controller for voice surveys

app/controllers/questions_controller.rb

Let's see how to create the voice response.

Creating the Voice Response

If there is no question (Question::NoQuestion), we'll respond with an exit message.

If the question is  "numeric" in nature, then we use the <Gather> verb. However, if we expect voice input from the user, we use the <Record> verb. Both verbs take an action attribute and a method attribute.

Twilio will use both attributes to define our response's endpoint to use as callback. That endpoint will be responsible for receiving and storing the caller's answer.

During the Record verb creation, we also ask Twilio for a Transcription. Twilio will process the voice recording and extract useful text, making a request to our response's endpoint when the transcription is complete.

Loading Code Samples...
Language
module Voice
  class CreateResponse
    INSTRUCTIONS = {
      'free'    => 'Please record your answer after the beep and then hit the pound sign',
      'numeric' => 'Please press a number between 0 and 9 and then hit the pound sign',
      'yes_no'  => 'Please press the 1 for yes and the 0 for no and then hit the pound sign'
    }.freeze

    def self.for(question)
      new(question).response
    end

    def initialize(question)
      @question = question
    end

    def response
      return exit_message if question == Question::NoQuestion

      Twilio::TwiML::Response.new do |r|
        r.Say question.body
        r.Say INSTRUCTIONS.fetch(question.type)
        if question.free?
          r.Record action: answers_path(question.id),
                   transcribe: true,
                   transcribeCallback: transcriptions_path(question.id)
        else
          r.Gather action: answers_path(question.id)
        end
      end.to_xml
    end

    private

    attr_reader :question

    def exit_message
      Twilio::TwiML::Response.new do |r|
        r.Say 'Thanks for your time. Good bye'
        r.Hangup
      end.to_xml
    end

    def answers_path(question_id)
      "/answers?question_id=#{question_id}"
    end

    def transcriptions_path(question_id)
      "/transcriptions?question_id=#{question_id}"
    end
  end
end
lib/voice/create_response.rb
Create a voice response or exit the survey

lib/voice/create_response.rb

Now let's see how to handle the response that comes in from our user.

Handling Responses

When the user finishes their response, Twilio sends us a request that tells us what happened and asks for further instructions on how to proceed.

At this point, we need to recover data from Twilio's request parameters using the create method on Answer.

Recovered parameters vary according to what we asked during the survey:

  • From contains the caller's phone number.
  • RecodingUrl contains the URL for listening to a recorded message.
  • Digits contains the keys pressed for a numeric question.
  • CallSid contains the unique identifier for the call.

Finally we redirect to our question controller, which will ask the next question in the interview loop. This is done via the Voice::CreateResponse.for method.

Loading Code Samples...
Language
class AnswersController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    Answer.create(answer_params)
    current_question = Question.find(params[:question_id])
    next_question    = FindNextQuestion.for(current_question)
    render xml: Voice::CreateResponse.for(next_question)
  end

  private

  def answer_params
    {
      from:        params[:From],
      content:     params[:RecordingUrl] || params[:Digits],
      question_id: params[:question_id],
      call_sid:    params[:CallSid]
    }
  end
end
app/controllers/answers_controller.rb
Handle user responses

app/controllers/answers_controller.rb

We're almost done! Now let's find a way to visualize our survey results.

Displaying the Survey Results

For this route we simply query the database using the Active Record Query Interface.

We display a panel for every question from the survey, and inside every panel we list the responses from different calls.

You can access this page in the application's root route.

Loading Code Samples...
Language
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
app/controllers/surveys_controller.rb
Display survey results at the application index

app/controllers/surveys_controller.rb

That's it! If you configured one of your Twilio numbers to work with the application built in this tutorial you should be able to take the survey and see the results under the root route of the application. We hope you found this sample application useful.

Where to Next?

If you're a Ruby/Rails developer working with Twilio, you might enjoy these other tutorials:

Appointment Reminders

Automate the process of reaching out to your customers in advance of an upcoming appointment.

Click to Call

Click-to-call enables your company to convert web traffic into phone calls with the click of a button.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Reach out to us on Twitter and let us know what you build!

Agustin Camino
Kat King
Jose Oliveros
Andrew Baker

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end
class QuestionsController < ApplicationController
  def show
    question = Question.find(params[:id])
    render xml: Voice::CreateResponse.for(question)
  end
end
module Voice
  class CreateResponse
    INSTRUCTIONS = {
      'free'    => 'Please record your answer after the beep and then hit the pound sign',
      'numeric' => 'Please press a number between 0 and 9 and then hit the pound sign',
      'yes_no'  => 'Please press the 1 for yes and the 0 for no and then hit the pound sign'
    }.freeze

    def self.for(question)
      new(question).response
    end

    def initialize(question)
      @question = question
    end

    def response
      return exit_message if question == Question::NoQuestion

      Twilio::TwiML::Response.new do |r|
        r.Say question.body
        r.Say INSTRUCTIONS.fetch(question.type)
        if question.free?
          r.Record action: answers_path(question.id),
                   transcribe: true,
                   transcribeCallback: transcriptions_path(question.id)
        else
          r.Gather action: answers_path(question.id)
        end
      end.to_xml
    end

    private

    attr_reader :question

    def exit_message
      Twilio::TwiML::Response.new do |r|
        r.Say 'Thanks for your time. Good bye'
        r.Hangup
      end.to_xml
    end

    def answers_path(question_id)
      "/answers?question_id=#{question_id}"
    end

    def transcriptions_path(question_id)
      "/transcriptions?question_id=#{question_id}"
    end
  end
end
class AnswersController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    Answer.create(answer_params)
    current_question = Question.find(params[:question_id])
    next_question    = FindNextQuestion.for(current_question)
    render xml: Voice::CreateResponse.for(next_question)
  end

  private

  def answer_params
    {
      from:        params[:From],
      content:     params[:RecordingUrl] || params[:Digits],
      question_id: params[:question_id],
      call_sid:    params[:CallSid]
    }
  end
end
class SurveysController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
    @survey = Survey.includes(:questions).first
  end

  def voice
    survey = Survey.first
    render xml: welcome_message_for_voice(survey)
  end

  def sms
    user_response = params[:Body]
    from          = params[:From]
    render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
  end

  private

  def welcome_message_for_voice(survey)
    Twilio::TwiML::Response.new do |r|
      r.Say "Thank you for taking the #{survey.title} survey"
      r.Redirect question_path(survey.first_question.id), method: 'GET'
    end.to_xml
  end
end