Adhearsion 2: The New Menu DSL

Rapid and painless creation of complex IVRs has always been one of the defining features of Adhearsion for beginning and advanced programmers alike. Through the #menu DSL method, the framework abstracts and packages the output and input management and the complex state machine needed to implement a complete menu with audio prompts, digit checking, retries and failure handling, making creating menus a breeze.

The menu DSL has received a major overhaul in Adhearsion 2.0, with the goals of clarifying syntax and adding functionality.

Call Controllers

Adhearsion 2.0 is now using a new routing architecture with powerful and flexible call controllers. Call controllers allow your code to be grouped into classes, making testing easier and improving the readability of your code. For more information see Ben Langfeld’s excellent introduction to call controllers and routing.

The Old Way

The Adhearsion 1.x menu structure used to work as follows, with the now obsolete concept of a dialplan and context.

foo {
  menu "Press 1 for Administration", "Press 2 for Tech Support",
       :timeout => 8.seconds, :tries => 3 do |link|
    link.admin 1 2
    link.on_invalid do
      speak 'Invalid input'

    link.on_premature_timeout do
      speak 'Sorry'

    link.on_failure do
      speak 'Goodbye'

admin {
  speak 'You have reached the Administration department'

tech {
  speak 'You have reached the Technical Support office'

The main shortcomings of this structure were mostly related to the inherent difficulty of maintaining a complex dialplan all in a single file, or breaking it up in helper components with no imposed logical structure. The old DSL was also not very intuitive, as you were specifying the target for the link before the pattern, in addition to having the block argument as a basically redundant part.

The New & Improved Way

The focus for the menu DSL in Adhearsion 2.0 was primarily on improving its functionality to work with call controllers and to fit the new framework structure. Working towards those goals, the menu definition block was streamlined while keeping readability and the general functionality of 1.x.

class MyController < Adhearsion::CallController
  def run
    menu "Where can we take you today?",
         :timeout => 8.seconds, :tries => 3 do
      match 1, BooController
      match "2", MyOtherController
      match 3, 4, { pass YetAnotherController }
      match 5, FooController
      match 6..10 do |dialed|
        say_dialed dialed

      timeout { do_this_on_timeout }

      invalid do
        invoke InvalidController

      failure do
        speak 'Goodbye'

    speak "This code gets executed unless #pass is used"

  def say_dialed(dialed)
    speak "#{dialed} was dialed"

  def do_this_on_timeout
    speak 'Timeout'

The first arguments to #menu are a list of sounds to play, as accepted by #play, including strings for TTS, Date and Time objects, and file paths. #play and the other input and output methods, all renovated, will be covered in a subsequent post. Sounds will be played at the beginning of the menu and after each timeout or invalid input, if the maximum number of tries has not been reached yet.

The :tries and :timeout options respectively specify the number of tries before going into failure, and the timeout in seconds allowed before the first and each subsequent digit input.

The most important section is the following block, which specifies how the menu will be constructed and handled.

The #match method takes an Integer, a String, a Range or any number of them as the required input(s) for the match payload to be executed. The last argument to a #match is either the name of a CallController, which will be invoked, or a block to be executed. Matched input is passed in to the associated block, or to the controller through it instance variable @options[:extension].

#menu executes the payload for the first exact unambiguous match it finds after each input or timing out. In a situation where there might be overlapping patterns, such as 10 and 100, #menu will wait for timeout after the second digit.

Internally, the state machine has been re-implemented without using exceptions as a mean for flow control, which was a concern for #menu usage in begin..rescue blocks.

#timeout, #invalid and #failure replace #on_invalid, #on_premature_timeout and #on_failure. All of them only accept blocks as payload, but #pass or #invoke can be used to execute controllers inside them.

#invalid has its associated block executed when the input does not possibly match any pattern. #timeout block is run when time expires before or between input digits, without there being at least one exact match. #failure runs its block when the maximum number of tries is reached without an input match.

Execution of the current context resumes after #menu finishes. If you wish to jump to an entirely different controller, #pass can be used. #menu will return :failed if failure was reached, or :done if a match was executed.


The menu DSL is the only the first of many consumer-facing innovations Adhearsion 2.0 contains.

It showcases the driving force behind the new version: to have Adhearsion move out of its box as a companion to Asterisk to become a general purpose telephony framework.

More posts and examples will be published showing what you can build with Adhearsion 2.0. Stay tuned!

Subscribe to our mailing list

* indicates required
I want to read about...
Email Format

2 thoughts on “Adhearsion 2: The New Menu DSL

  1. I am new user of adhearsion, and trying to use this new DSL for match. But I am not able to find a way to write specs for this. Don’t know how to stub here. So it would very helpful if you could explain that part also.

    • The general recommendation here is to use integration testing (via NuBot or some similar tool or even human QA). If you wish to write unit tests, do so on the complex parts of your domain model, and try to keep complexity out of your controllers. You do not need to do functional tests of your menus, and you can simply trust that Adhearsion’s #menu will work correctly from a unit test standpoint. I’ll be writing a post about this testing strategy soon.

What do you think?