Build Status

Telephonist makes it easy to design state machines for Twilio calls. These state machines bring TwiML and logic together in one place, making call flows easier to maintain.


Get it from Hex by adding it to your deps in mix.exs:

def deps do
  [{:telephonist, "~> 0.1.0"}]

Run mix deps.get to install the package. Then, add :telephonist to your applications list. For example:

def application do
  [mod: {YourApp, []},
   applications: [:logger, :telephonist]]

This will ensure that all Telephonist's processes are started and supervised properly.


Basic Concepts

Like most state machines, Telephonist state machines are based on two concepts: state and transitions.


A state is represented by the Telephonist.State struct.

  machine: MachineName,
  name: :state_name,
  options: [],
  twiml: "<?xml ..."

States are primarily used to define what TwiML should be displayed to Twilio for a given call at a particular time. Telephonist provides a simple macro to make generating and returning Telephonist.State structs easy:

defmodule CustomStateMachine do
  use Telephonist.StateMachine, initial_state: :introduction

  state :introduction, _twilio, _options do
    say "Welcome to my phone tree!"

The state/3 macro is just sugar, and defines a function like this:

def state(:introduction, _twilio, options) do
  xml = twiml do
    say "Welcome to my phone tree!"

    machine: __MODULE__,
    name: :introduction,
    options: options,
    twiml: xml

The three arguments are as follows:

Whenever Telephonist wants to get a particular state out of your module, it will call the state/3 function generated by the state/3 macro, like so:

# twilio  -> a map of parameters that came from Twilio
# options -> any custom options that are appended to the call over time
CustomStateMachine.state(:introduction, twilio, options)

You can pattern match with the state/3 struct just like a function definition.

state :introduction, _twilio, %{error: msg} do
  say "An error occurred! #{msg}"

state :introduction, _twilio, _options do
  say "Welcome to my phone tree!"


Transitions are handled through the transition/3 function. It takes the same three arguments as the state/3 function or macro.

You can define it on your state machines like so:

defmodule CustomCallFlow do
  use Telephonist.StateMachine, initial_state: :choose_language

  state :choose_language, twilio, options do
    say "#{options[:error]}" # say any error, if present
    gather timeout: 10 do
      say "For English, press 1"
      say "Para español, presione 2"

  state :english, twilio, options do
    say "Proceeding in English..."

  state :spanish, twilio, options do
    say "Procediendo en español..."

  # If the user pressed "1" on their keypad, transition to English state
  def transition(:choose_language, %{Digits: "1"} = twilio, options) do
    state :english, twilio, options

  # If the user pressed "2" on their keypad, transition to Spanish state
  def transition(:choose_language, %{Digits: "2"} = twilio, options) do
    state :spanish, twilio, options

  # If neither of the above are true, append an error to the options and
  # remain on the current state
  def transition(:choose_language, twilio, options) do
    options = Map.put(options, :error, "You pressed an invalid digit. Please try again.")
    state :choose_language, twilio, options

Note that transition/3 must return a Telephonist.State. This is easily done by simply calling the state/3 function. Also, note that you can easily switch to another state machine by simply calling state on it:

def transition(:choose_language, %{Digits: "1"} = twilio, options) do
  EnglishCallFlow.state(:introduction, twilio, options)

def transition(:choose_language, %{Digits: "2"} = twilio, options) do
  SpanishCallFlow.state(:introduction, twilio, options)

Control of the call will then be passed to the other state machine. This allows you to keep your state machines small, focused, and potentially reusable.


When a call completes, Telephonist will call the on_complete/3 callback. It will receive the Telephonist.State of the call at the time it completed, Twilio's final request parameters, and the custom options the call accumulated during its life:

def on_complete({sid, twilio_call_status, state}, twilio, options) do

This is a good place to put any cleanup logic that you need to perform after a call completes.


This callback will be run if a transition fails due to an exception. This will most often occur when you fail to define a transition or state, or if your pattern matching left a case out. It provides you an opportunity to recover the call and prevent the user from hearing a Twilio error message.

def on_transition_error(exception, state_name, twilio, options) do
  # To prevent an error, return a new state:
  state :recover, twilio, options

The default implementation of on_transition_error/4 that comes with Telephonist.StateMachine will simply re-raise the error.

Processing Calls

Once you've defined a state machine, you can process calls through it using Telephonist.CallProcessor.

# The web framework shown here is pseudo-code
def index(conn, twilio) do
  options = %{} # Whatever I want to be able to use in my states and transitions
  state = Telephonist.CallProcessor.process(MyStateMachine, twilio, options)
  render conn, xml: state.twiml

That's it! New calls will start off in MyStateMachine.initial_state and progress from there. Existing calls will be looked up in an ETS table managed by Telephonist.OngoingCall and will progress from where they left off.

Under the Hood

Subscribing to Events

Telephonist publishes events via GenEvent. In fact, Telephonist.Logger is simply a subscriber to these events. Look there for an example of how to implement your own subscriber.

Other Twilio Libraries

See these other Elixir libraries I've written for Elixir:


Telephonist is under the MIT license. See the LICENSE file for more details.