Mod lua

From FreeSWITCH Wiki
Revision as of 14:48, 10 April 2012 by Garmt (Talk | contribs)

Jump to: navigation, search



Write IVR scripts in Lua

It has a very easy to use syntax, see the Hello Lua script.

Serve configs (the same way mod_xml_curl does it)


Make API calls directly from Lua code



Stripped is 272k

Highly Embeddable

As far as embeddability goes - Python ranks a 2, Perl ranks a 4, JavaScript is a 5, and Lua is a 10!

at the CLI : lua and luarun

You can issue a "luarun /path/to/script.lua" and launch a thread which your lua script will run in. The "lua" command is for inline lua like from the dialplan i.e. ${lua(codehere)}. "luarun" will spawn a thread while "lua" will block until the code is complete. A "~" in front of the argument will run a single line lua command. Note that lua scripts executed with luarun cannot write to the console through stream:write API as there is no stream.

Passing Arguments

Arguments are passed as space-separated values:

luarun arg1 arg2 arg3

Arguments are accessed with "argv" like this:

my_first_var = argv[1];
my_next_var = argv[2];

And so on...

freeswitch@DVORAK> lua ~print(string.find("1234#5678", "(%d+)#(%d+)"))
1       9       1234    5678
freeswitch@DVORAK> luarun ~print(string.find("1234#5678", "(%d+)#(%d+)"))
1       9       1234    5678
freeswitch@DVORAK> luarun ~stream:write("1234#5678")
2011-06-20 13:35:35.274782 [ERR] mod_lua.cpp:191 [string "line"]:1: attempt to index global 'stream' (a nil value)
stack traceback:
       [string "line"]:1: in main chunk
freeswitch@DVORAK> lua ~stream:write("1234#5678")


For IVR use

Nothing should be needed here.

For making API calls

api = freeswitch.API();
digits = api:execute("regex", "testing1234|/(\\d+)/|$1");
-- The returned output of the API call is stored in the variable if you need it.
freeswitch.consoleLog("info", "Extracted digits: " .. digits .. "\n")

Note: Please take care to escape the arguments that you pass, as has been done to the regex string above.

To call another Lua script

api = freeswitch.API();
reply = api:executeString("luarun another.lua");

For serving configuration

mod_lua allows you to replace request for configuration data from a lookup in the static XML to your script.

See Mod_lua/Serving_Configuration for more information.

Lua scripts at startup

Here is a minimum configuration file:

<configuration name="lua.conf" description="LUA Configuration">
	The following options identifies a lua script that is launched
	at startup and may live forever in the background.
	You can define multiple lines, one for each script you 
	need to run.
    <!--<param name="startup-script" value="startup_script_1.lua"/>-->
    <!--<param name="startup-script" value="startup_script_2.lua"/>-->

The start-up script values represent lua scripts (located inside the scripts/ directory) that are launched when FreeSWITCH is started. The scripts live in their own thread. You can use them to run simple tasks (and then let them finish) or looping forever, watching (for example) for events, generating calls or whatever you like.

Sample Dialplan

<action application="lua" data="helloworld.lua arg1 arg2"/> 

NOTE: arguments can be accessed by using argv[1] argv[2] in your script
NOTE: for looking up the location of the helloworld.lua file, it looks in prefix/scripts by default

Sample IVR's

Hello Lua

-- answer the call

-- sleep a second

-- play a file

-- hangup

Lua Regex Example

using this method, you can execute regex conditions from inside your lua scripts. (thanks bkw_)

session:execute("set", "some_chan_variable=${regex(" .. destination .. "|^([0-9]{10})$)}")

if destination is a lua variable.. with your destination number in it... and your regex was ^([0-9]{10})$ the result will be put in "some_chan_variable" that you can get thorough a session:getVariable

you can also do

session:execute("set", "some_chan_variable=${regex(" .. destination .. "|^(0|61)([2,3,7,8][0-9]{8})$|$2)}")

to match and return parts of your regex.

Native Lua Pattern Matching

Lua supports a simple but powerful pattern matching syntax. It is less powerful than PCRE but it handles the majority of pattern matching cases will ever need in a simple script.

The following is a simple script that can be run with "luarun" from fs_cli and demonstrates the capturing of two values from a data string:

-- pattern.lua
data = "1234#5678";
_,_,var1,var2 = string.find(data,"(%d+)#(%d+)");
freeswitch.consoleLog("INFO","\ndata: " .. data .. "\nvar1: " .. var1 .. "\nvar2: " .. var2 .. "\n");

freeswitch@internal> luarun pattern.lua

2011-04-18 08:28:49.242080 [INFO] switch_cpp.cpp:1197 
data: 1234#5678
var1: 1234
var2: 5678

See also:

Misc. Samples

Run a shell command

When using session:execute() and api:execute() to execute a shell command (ie: bash script) it will only return the error code integer.

To return the output of a shell command use io.popen(). The following example Lua function is from

 function shell(c)
   local o, h
   h = assert(io.popen(c,"r"))
   o = h:read("*all")
   return o

More Samples


Where is my debug information?

Q: When I use static XML or xml_curl, I see the commands executed in the fs_cli and in the "application log" section of the xml_cdr file. However, when I run commands via my lua script, I don't see that information anywhere?

A: When you have a actual call session, you can use session:execute("$application","$data") just like in the static XML - it will then show up in the fs_cli and the xml_cdr application log. If you don't have a call session - e.g. if you are running lua as a background application or from the CLI, then you have to use other commands which may not be as easily logged.

Where do I put 3rd part Lua scripts/modules?

Q: Where do I need to stick my Lua classes for FreeSWITCH to see them. I am running a Lua script from within the /scripts folder, but it includes (requires) another Lua file. FreeSWITCH.

A: (short answer) /usr/local/share/lua/5.1/

Alternatively, you can install a system Lua (think apt-get or yum), and the Lua embedded in freeswitch will look in the add-ons directory.

To do this in the mod_lua.conf file (currently not possible), the following development would be needed.

* Install them to a nonstandard.
* Push a nonstandard place into the path of where it looks.

NOTE: Assuming you have <param name="module-directory" value="$${base_dir}/scripts/?.so"/> in lua.conf.xml, in the scripts, require statements in the form of:

require "luasql.mysql"

This will look for the shared object ${base_dir}/scripts/luasql/

NOTE: The native MySQL driver for Lua has a very bad memory leak. Do not use it.

How can I make it use the "system lua"

Q: I have Lua installed, but mod_lua seems to ignore the Lua binary.

A: Lua is so small that the whole ball of wax is statically linked into the module!

How can I get Lua to see my own libraries using "require"

Q: Can I use the require mechanism for including libraries with the Lua in FreeSWITCH?

A: You will need to alter the LUA_PATH variable to instruct the embedded Lua inside FreeSWITCH to find your libraries. A simple startup script to do this is:

export LUA_PATH=/usr/local/freeswitch/scripts/?.lua\;\;
ulimit -s 240
screen /usr/local/freeswitch/bin/freeswitch

Can I access a database via ODBC?

Yes, there are two ways for doing this: freeswitch.Dbh and LuaSQL. The latter is described here.

With LuaSQL, you can query an ODBC data source, but also native drivers for PostgreSQL, Oracle or MySQL are available. The LuaSQL module itself will make the connection to your database. LuaSQL does not provide connection pooling as is possible with freeswitch.Dbh.

See LuaSQL (LuaSQL 2.1.1) for more information.

Here is a short howto for Installing LuaSQL.

WARNING: On x64_86 you need to change in config a few things
tar xfvz luasql-2.1.1.tar.gz
cd luasql-2.1.1/

The file is divided into three broad sections: 1. DB Name 2. Paths to different libraries 3. Paths to DB related libraries

First check the #2 if it matches your flavor of OS.

Then, you have to select only *one* driver to compile at a time. Suppose you want to compile MySQL, then you have to uncomment only the MySQL line from #1 and #3. All other lines in both these sections should be commented.

NOTE: On RHEL / CentOS, the path for MySQL libs and include is not the same as the default config file that comes with LuaSQL. Please make the changes as follows:

DRIVER_LIBS= -L/usr/lib/mysql -lmysqlclient -lz
DRIVER_INCS= -I/usr/include/mysql

Example: to use Microsoft SQL Server:

sed -i 's/#T= odbc/T= odbc/g' config
sed -i 's/T=sqlite3/#T=sqlite3/g' config

DRIVER_LIBS= -L/usr/lib64 -lodbc


To allow compile properly:

sed -i 's/WARN= /WARN= -fPIC /g' config 


make install

First you need to download and install Lua, and download LuaSQL, and compile and install whichever database modules you want to use. Next you can do something like:

require "luasql.mysql"

env = assert (luasql.mysql())
con = assert (env:connect("database","username","password","localhost"))
cur = assert (con:execute"SELECT * FROM table")
row = cur:fetch ({}, "a")

session:setVariable("varname", tostring(row.column));


For ODBC, you need to edit the odbc.ini and odbcinst.ini (both in /etc on RHEL). A typical MySQL configuration looks like:

File: odbcinst.ini
# Driver from the MyODBC package
# Setup from the unixODBC package
Description     = ODBC for MySQL
Driver          = /usr/lib/
Setup           = /usr/lib/
FileUsage       = 1

Note that the *.so files should be in the folder.

File: odbc.ini
Driver     = MySQL
SERVER     = localhost
PORT       = 3306
DATABASE   = mydatabase
OPTION     = 67108864
SOCKET     = /var/lib/mysql/mysql.sock

NOTE:I found you also need to set some environment variables for scripts to run properly

echo export LUA_PATH=/usr/local/freeswitch/scripts/?.lua >> /etc/bashrc
echo export LUA_CPATH=/usr/local/freeswitch/scripts/?.so >> /etc/bashrc
echo export PATH=$PATH:/usr/local/freeswitch/bin >> ~/.bashrc

NOTE: you need to symlink the shared object (i.e., to /usr/local/lib/lua/5.1/luasql/

WARNING: This change breaks load of external Lua modules

NOTE: Fixed

How can I find useful undocumented Session Functions?

There may be times when a function gets added, but not documented. This simple Lua script may help.

-- This function simply tells us what function are available in Session
--   It just prints a list of all functions.  We may be able to find functions
--   that have not yet been documented but are useful.  I did :)
function printSessionFunctions( session )

   metatbl = getmetatable(session)
   if not metatbl then return nil end

   local f=metatbl['.fn'] -- gets the functions table
   if not f then return nil end

   print("\n***Session Functions***\n")
   for k,v in pairs(f) do print(k,v) end


new_session = freeswitch.Session() -- create a blank session


At freeswitch revision 16256 this is the output of the script:

process_callback_result function: 0x79b380
flushDigits     function: 0x798db0
execute function: 0x798ed0
setAutoHangup   function: 0x798de0
collectDigits   function: 0x798c30
mediaReady      function: 0x7993d0
speak   function: 0x79b2d0
setHangupHook   function: 0x799510
getDigits       function: 0x798c60
check_hangup_hook       function: 0x799380
playAndGetDigits        function: 0x798cf0
getState        function: 0x79b1e0
hangupState     function: 0x79b4e0
say     function: 0x79b150
recordFile      function: 0x79b210
waitForAnswer   function: 0x798ea0
sleep   function: 0x798d50
setEventData    function: 0x798f30
setPrivate      function: 0x79b540
setLUA  function: 0x7995d0
begin_allow_threads     function: 0x799320
setInputCallback        function: 0x7994e0
unsetInputCallback      function: 0x799470
run_dtmf_callback       function: 0x799400
get_cb_args     function: 0x799020
read    function: 0x798cc0
getPrivate      function: 0x79b570
answer  function: 0x79b450
setVariable     function: 0x79b510
getXMLCDR       function: 0x798f60
originate       function: 0x799570
destroy function: 0x7992f0
answered        function: 0x798e70
set_tts_parms   function: 0x79b300
sendEvent       function: 0x798f00
ready   function: 0x799540
preAnswer       function: 0x79b4b0
streamFile      function: 0x798d20
hangupCause     function: 0x79b1b0
setDTMFCallback function: 0x79b2a0
getVariable     function: 0x79b350
transfer        function: 0x798c90
get_uuid        function: 0x798ff0
hangup  function: 0x79b480
end_allow_threads       function: 0x799350
flushEvents     function: 0x798d80
sayPhrase       function: 0x79b180



These methods apply to generating events.


--Create Custom event
custom_msg = 	"dial_record_id: " .. dial_record_id .. "\n" .. 
		"call_disposition: " .. Disposition .. "\n" ..
		"campaign_number: "  .. Campaign .. "\n" ..
		"called_number: "    .. dial_num .."\n"  ;  
		local e = freeswitch.Event("custom", "dial::dial-result");


You can add as much data to the body as you like, in this case 4 items are to be sent.

The result will be

[Content-Length] => 555
    [Content-Type] => text/event-plain
    [Body] => Array
            [Event-Subclass] => dial::dial-result
            [Event-Name] => CUSTOM
            [Core-UUID] => 2dc7cc50-b157-4868-ae16-04e5f4b95dae
            [FreeSWITCH-Hostname] =>
            [FreeSWITCH-IPv4] =>
            [FreeSWITCH-IPv6] => ::1
            [Event-Date-Local] => 2009-02-17 23:15:49
            [Event-Date-GMT] => Tue, 17 Feb 2009 23:15:49 GMT
            [Event-Date-Timestamp] => 1234912549610060
            [Event-Calling-File] => switch_cpp.cpp
            [Event-Calling-Function] => fire
            [Event-Calling-Line-Number] => 297
            [Content-Length] => 85
            [Body] => Array
                    [dial_record_id] => 1234
                    [call_disposition] => AA
                    [campaign_number] => 20
                    [called_number] => 7777777






bkw gave the example of such...

local event = freeswitch.Event("message_waiting");
event:addHeader("MWI-Messages-Waiting", "no");
event:addHeader("MWI-Message-Account", "sip:1000@");
event:addHeader("Sofia-Profile", "internal");



This is a generic API call.


Or, This can be used inside of a dialplan.lua to get certain information




Use this to dump all available Headers to the console.

-- Print as text

-- Print as JSON

Or this to display them as an info message.



Sending an Event

Using luarun to execute this code you can toggle the MWI on a registered phone on and off.

local event = freeswitch.Event("message_waiting");                                                                                                              
event:addHeader("MWI-Messages-Waiting", "no");                                                                                                                 
event:addHeader("MWI-Message-Account", "sip:1002@");                                                                                                 


The following methods can be applied to existing sessions.


Answer the session:



- checks whether the session is flagged as answered (true anytime after the call has been answered)


Check to see if this session's channel is bridged to another channel.

if (session:bridged() == true) do
    -- Do something




Destroys the session and releases resources. This is done for you when your script ends, but if your script contains an infinite loop you can use this to terminate the session.


session:execute(app, data)

local mySound = "/usr/local/freeswitch/sounds/music/16000/partita-no-3-in-e-major-bwv-1006-1-preludio.wav"

session:execute("playback", mySound)

Callbacks (DTMF and friends) CAN NOT EXECUTE during an execute.





Get digits:

  • getDigits has three arguments: max_digits, terminators, timeout
  • max_digits: maximum number of DTMF tones that will be collected
  • terminators: buffer of characters that will terminate the digit collection
  • timeout: maximum time in milliseconds allowed for digit collection
  • return: buffer containing collected digits
  • The method blocks until one of the exit criteria is met.
digits = session:getDigits(5, "#", 3000);
freeswitch.consoleLog("info", "Got dtmf: ".. digits .."\n");


Get the call state, i.e. "CS_EXECUTE". The call states are described in "switch_types.h".



To get system variables such as ${hold_music}

local moh = session:getVariable("hold_music")
--[[ events obtained from "switch_channel.c"
 regards Monroy from Mexico


You can hang up a session and provide an optional Hangup causes.



session:hangup(); -- default normal_clearing


You can find the hangup cause of an answered call and/or the reason an originated call did not complete. See Hangup causes.

-- Initiate an outbound call

obSession = freeswitch.Session("sofia/")

-- Check to see if the call was answered

if obSession:ready() then
    -- Do something good here

else    -- This means the call was not answered ... Check for the reason

    local obCause = obSession:hangupCause()

    freeswitch.consoleLog("info", "obSession:hangupCause() = " .. obCause )

    if ( obCause == "USER_BUSY" ) then              -- SIP 486
       -- For BUSY you may reschedule the call for later
    elseif ( obCause == "NO_ANSWER" ) then
       -- Call them back in an hour
    elseif ( obCause == "ORIGINATOR_CANCEL" ) then   -- SIP 487
       -- May need to check for network congestion or problems
       -- Log these issues



session:insertFile(<orig_file>, <file_to_insert>, <insertion_sample_point>)

Inserts one file into another. All three arguments are required. The third argument is in samples, and is the number of samples into orig_file that you want to insert file_to_insert. The resulting file will be written at the sample rate of the session, and will replace orig_file.

Because the position is given in samples, you'll need to know the sample rate of the file to properly calculate how many samples are X seconds into the file. For example, to insert a file two seconds into a .wav file that has a sample rate of 8000Hz, you would use 16000 for the insertion_sample_point argument.

Note that this method requires an active channel with a valid session object, as it needs the sample rate and the codec info from the session.


On a ulaw channel, insert bar.wav one second into foo.wav:

 session:insertFile("foo.wav", "bar.wav", 8000)

Prepend bar.wav to foo.wav:

 session:insertFile("foo.wav", "bar.wav", 0)

Append bar.wav to foo.wav:

 session:insertFile("bar.wav", "foo.wav", 0)



session:originate is deprecated, use the following construct instead:

new_session = freeswitch.Session("sofia/gateway/gatewayname/18001234567", session);

The code below is here for the sake of history only; please do not use it going forward.

 -- this usage of originate is deprecated, use freeswitch.Session(dest, session)
 new_session = freeswitch.Session();
 new_session.originate(session, dest[, timeout]);

dest - quoted dialplan destination. For example: "sofia/internal/1000@" or "sofia/gateway/my_sip_provider/my_dest_number"
timeout - origination timeout in seconds

Note: session.originate expects at least 2 arguments.


Plays a file and collects DTMF digits. Digits are matched against a regular expression. Non-matching digits or a timeout can trigger the playing of an audio file containing an error message. Optional arguments allow you to transfer to an extension on failure, and store the entered digits into a channel variable.

digits = session:playAndGetDigits (
          min_digits, max_digits, max_attempts, timeout, terminators,
          prompt_audio_files, input_error_audio_files,
          digit_regex, variable_name, digit_timeout,
min_digits The minimum number of digits required.
max_digits The maximum number of digits allowed.
max_attempts The number of times this function waits for digits and replays the prompt_audio_file when digits do not arrive.
timeout The time (in milliseconds) to wait for a digit.
terminators A string containing a list of digits that cause this function to terminate.
prompt_audio_file The initial audio file to play. Playback stops if digits arrive while playing. This file is replayed after each timeout, up to max_attempts.
input_error_audio_file The audio file to play when a digit not matching the digit_regex is received. Received digits are discarded while this file plays. Specify an empty string if this feature is not used.
digit_regex The regular expression used to validate received digits.
variable_name (Optional) The channel variable used to store the received digits.
digit_timeout (Optional) The inter-digit timeout (in milliseconds). When provided, resets the timeout clock after each digit is entered, thus giving users with limited mobility the ability to slowly enter digits without causing a timeout. If not specified, digit_timeout is set to timeout.
transfer_on_failure (Optional) In the event of a failure, this function will transfer the session to an extension in the dialplan. The syntax is "extension-name [dialplan-id [context]]".
  • This function returns an empty string when all timeouts and retry counts are exhausted.
  • When the maximum number of allowable digits is reached, the function returns immediately, even if a terminator was not entered.
  • If the user forgets to press one of the terminators, but has made a correct entry, the digits are returned after the next timeout.
  • The session has to be answered before any digits can be processed. If you do not answer the call you, the audio will still play, but no digits will be collected.

Example 1:

digits = session:playAndGetDigits(2, 5, 3, 3000, "#", "/prompt.wav", "/error.wav", "\\d+")
freeswitch.consoleLog("info", "Got DTMF digits: ".. digits .."\n")

This example causes freeswitch to play prompt.wav and listen for between 2 and 5 digits, ending with the # key. If the user enters nothing (or something other than a digit, like the * key) error.wav is played, and the process is repeated another two times.

Example 2:

digits = session:playAndGetDigits(1, 1, 3, 3000, "", "/menu.wav", "/error.wav", "[134]", 3, "digits_received", "operator XML default")
freeswitch.consoleLog("info", "Got DTMF digits: ".. digits .."\n")

This time, we require only one digit, and it must be 1, 3 or 4. If the user does not comply after three attempts, they are transferred to the operator extension in the "default" XML dial plan.

If the user presses a correct key, that digit is returned to the caller, and the "digits_received" channel variable is set to the same value.

Reminder: If you need to match the * key in the regular expression, you will have to quote it twice:

digits = session:playAndGetDigits(2, 5, 3, 3000, "#", "/sr8k.wav", "", "\\d+|\\*");


Pre answer the session:



Play a file and get digits.

digits = session:read(5, 10, "/sr8k.wav", 3000, "#");                                                                                                           
freeswitch.consoleLog("info", "Got dtmf: ".. digits .."\n");         

session:read has 5 arguments: <min digits> <max digits> <file to play> <inter-digit timeout> <terminators>


- checks whether the session is still active (true anytime between call starts and hangup)
- also session:ready will return false if the call is being transferred. Bottom line is you should always be checking session:ready on any loops and periodicly throughout your script and exit asap if it returns false.

See #session:hangupCause for more detail on if NOT ready.

while (session:ready() == true) do                                                                                                                              
   -- do something here                                                                                                                                              


syntax is session:recordFile(file_name, max_len_secs, silence_threshold, silence_secs)

silence_secs is the amount of silence to tolerate before ending the recording.


function grabaEntrada(tablaGraba, tablaGrabaMod)
	local razon, OWNER, destino = '', '', '';
	local handle, k, v  = nil, nil, nil
	OWNER = 'grabaEntrada'
        function mkDir(dir)
	     local d = nil
	     local OWNER = 'mkDir'
	     local tmp = ''
	     if type(dir) == 'string' then 
		dir = backSlash(dir)
		for d in string.gfind(dir, ".-/") do
			tmp = tmp .. d
		dir = tmp
		os.execute(string.format('MKDIR %s', string.gsub(dir, '/', '%\\') ))

	function onInputCBF(s, _type, obj, arg)
		local k, v = nil, nil
		if tablaGrabaMod._debug then
			for k, v in pairs(tablaGrabaMod) do 
				print(string.format('tablaGrabaMod k-> %s v->%s\n', tostring(k), tostring(v)))
			for k, v in pairs(obj) do 
				print(string.format('obj k-> %s v->%s\n', tostring(k), tostring(v)))
			if _type == 'table' then
				for k, v in pairs(_type) do 
					print(string.format('_type k-> %s v->%s\n', tostring(k), tostring(v)))
			print(string.format('\n(%s == dtmf) and (obj.digit [%s] == [%s] tablaGrabaMod.dtmf)\n', _type, obj.digit, tablaGrabaMod.dtmf))
		if (_type == "dtmf") and (obj.digit == tablaGrabaMod.dtmf) then  print(string.format('\nBREAK!!!\n'))
			return 'break'
		else print(string.format('\nCONTINUE!!!\n'))
			return ''
	if type(tablaGraba) ~= 'table' then 
		razon= string.format('ERROR Parametros invalidos')
		return false 
	if type(tablaGrabaMod) ~= 'table' then
		tablaGrabaMod = {
			segundosMax 			= 7,
			umbralSilencio 			= 500,
			segundosInterrupcion	= 3,
			dtmf					= '#',
			_debug					= false
	if type(tablaGraba.dir ) ~= 'string'then 
		razon= string.format('ERROR tablaGraba.dir no es tipo string, Abortando')
		return false
	if type(tablaGraba.audioT) ~= 'table' then
		razon= string.format('ERROR tablaGraba.audioT no es tipo table, Abortando')
		return false
	if #tablaGraba.audioT == 0 then
		razon= string.format('WARNING tablaGraba.audioT NO tiene audios definidos, defina audios (tablaGraba.audioT = {\'aviso.wav\', \'beep.wav\' })')
	if (type(tablaGraba.nombre) ~= 'string' ) or (tablaGraba.nombre == '') then
		tablaGraba.nombre = dameNombreUnico(CUUID)..'.wav'
		razon= string.format('WARNING tablaGraba.nombre NO definido e esta VACIO, valor Asignado [%s]', tablaGraba.nombre)
	if type(tablaGrabaMod.dtmf) ~= 'string' or #tablaGrabaMod.dtmf == 0 then
		tablaGrabaMod.dtmf = '#'
	if type(tablaGrabaMod._debug) ~= 'boolean' then
		tablaGrabaMod._debug = false
	tablaGrabaMod.segundosMax 			= tonumber(tablaGrabaMod.segundosMax) 		or 7; 
	tablaGrabaMod.umbralSilencio 		= tonumber(tablaGrabaMod.umbralSilencio) 	or 500; 
	tablaGrabaMod.segundosInterrupcion	= tonumber(tablaGrabaMod.segundosInterrupcion) or 3;
	tablaGrabaMod.dtmf = tablaGrabaMod.dtmf:sub(1,1) --Solo un caracter
	if tablaGraba.dir:sub(#tablaGraba.dir) ~= '/' then
		tablaGraba.dir = tablaGraba.dir..'/'
	destino = string.format('%s%s', tablaGraba.dir, tablaGraba.nombre)
	handle =, 'w')
	if handle == nil then	--Trata de crear el directorio sino existe
		local tmp, d, dir = '', '', ''
		dir = destino
		for d in string.gfind(dir, ".-/") do
			tmp = tmp .. d
		dir = tmp
	razon= string.format('LISTO path [%s] FLUSH_DTMF EJECUTADO!!', destino)
	parametros = creaInforme(razon, string.format('INFO %s', razon), CONSOLE_W, OWNER)
	if session:ready() then
		for k, v in pairs(tablaGraba.audioT) do 
		session:setInputCallback('onInputCBF', ''); 
		session:recordFile(destino, tablaGrabaMod.segundosMax, tablaGrabaMod.umbralSilencio, tablaGrabaMod.segundosInterrupcion); 

Regards Arturo Monroy


Play a speech phrase macro.

session.sayPhrase(macro_name [,macro_data] [,language]);
  • macro_name - (string) The name of the say macro to speak.
  • macro_data - (string) Optional. Data to pass to the say macro.
  • language - (string) Optional. Language to speak macro in (ie. "en" or "fr"). Defaults to "en".

To capture events or DTMF, use it in combination with session:setInputCallback


function key_press(session, input_type, data, args)
  if input_type == "dtmf" then
    freeswitch.consoleLog("info", "Key pressed: " .. data["digit"])
    return "break"
if session:ready() then
  session:execute("sleep", "1000")
  session:setInputCallback("key_press", "")
  session:sayPhrase("voicemail_menu", "1:2:3:#", "en")

When used with setInputCallback, the return values and meanings are as follows:

  • true or "true" - Causes prompts to continue speaking.
  • Any other string value interrupts the prompt.



By default, lua script hangs up when it is done executing. If you need to run the next action in your dialplan after the lua script, you will need to setAutoHangup to false. The default value is true.



In your lua code, you can use setHangupHook to define the function to call when the session hangs up.

function myHangupHook(s, status, arg)
    freeswitch.consoleLog("NOTICE", "myHangupHook: " .. status .. "\n")
    -- close db_conn and terminate


session:setHangupHook("myHangupHook", "blah")

Other possibilities to exit the script (and throwing an error)

return "exit";
return "die";
s:destroy("error message");


function my_cb(s, type, obj, arg)                                                                                                                                  
   if (arg) then                                                                                                                                                
      io.write("type: " .. type .. "\n" .. "arg: " .. arg .. "\n");                                                                                             
      io.write("type: " .. type .. "\n");                                                                                                                       
   if (type == "dtmf") then                                                                                                                                     
      io.write("digit: [" .. obj['digit'] .. "]\nduration: [" .. obj['duration'] .. "]\n");                                                                     
      e = freeswitch.Event("message");                                                                                                                          
      e:add_body("you said " .. obj:get_body());                                                                                                                

session:setInputCallback("my_cb", "blah");                                                                                                                      


Set a variable on a session:

session:setVariable("varname", "varval");


  • This will allow callbacks to DTMF to occur and session:execute("sleep", "5000"), won't.


session:set_tts_parms("flite", "kal");
session:speak("Please say the name of the person you're trying to contact");


Plays pre-recorded sound files for things like numbers, dates, currency, etc. Refer to Misc. Dialplan Tools say for info about the say application.

Arguments: <lang><say_type><say_method>


session:say("12345", "en", "number", "pronounced");


Stream a file endless to the session


Stream a file endless to the session starting at sample_count?

session:streamFile("/tmp/blah.wav", <sample_count>);


Transfer the current session. The arguments are extensions, dialplan and context.

session:transfer("3000", "XML", "default");

execution of your lua script will immediately stop, make sure you set session:setAutoHangup(false) if you don't want your call to disconnect




Non-Session API

These methods are generic in that they do not apply to a session or an event. For example, printing data to the FreeSWITCH console is neither event- nor session-specific.


api = freeswitch.API();
-- get current time in milliseconds
time = api:getTime()


session1 = freeswitch.Session("sofia/internal/1001%");
session2 = freeswitch.Session("sofia/internal/1002%");
freeswitch.bridge(session1, session2);


freeswitch.consoleCleanLog("This Rocks!!!\n");


Log something to the freeswitch logger. Arguments are loglevel, message.

freeswitch.consoleLog("info",   "lua rocks\n");
freeswitch.consoleLog("notice", "lua rocks\n");
freeswitch.consoleLog("err",    "lua rocks\n");
freeswitch.consoleLog("debug",  "lua rocks\n");
freeswitch.consoleLog("warning","lua rocks\n");


Get an ODBC or core (sqlite) database handle from FreeSWITCH and perform an SQL query through it.

Advantage of this method is that it makes use of connection pooling provided by FreeSWITCH which gives a nice increase in speed when compared to creating a new TCP connection for each LuaSQL env:connect().

It works as follows:

local dbh = freeswitch.Dbh("dsn","user","pass") -- when using ODBC
-- OR --
local dbh = freeswitch.Dbh("core:my_db") -- when using sqlite

assert(dbh:connected()) -- exits the script if we didn't connect properly

dbh:test_reactive("SELECT * FROM my_table",
                  "DROP TABLE my_table",
                  "CREATE TABLE my_table (id INTEGER(8), name VARCHAR(255))")

dbh:query("INSERT INTO my_table VALUES(1, 'foo')") -- populate the table
dbh:query("INSERT INTO my_table VALUES(2, 'bar')") -- with some test data

dbh:query("SELECT id, name FROM my_table", function(row)
  stream:write(string.format("%5s : %s\n",,

dbh:query("UPDATE my_table SET name = 'changed'")
stream:write("Affected rows: " .. dbh:affected_rows() .. "\n")

dbh:release() -- optional
  • freeswitch.Dbh("dsn", "user", "pass") gets an ODBC db handle from the pool.
  • freeswitch.Dbh("core:dbname") gets a core db (sqlite) db handle from the pool (this automatically creates the db if it didn't exist yet).
  • dbh:connected() checks if the handle is still connected to the database, returns true if connected, false otherwise.
  • dbh:test_reactive("test_sql", "drop_sql", "reactive_sql") performs test_sql and if it fails performs drop_sql and reactive_sql (handy for initial table creation purposes)
  • dbh:query("query", function()) takes the query as a string and an optional Lua callback function that is called on each row returned by the db.
    • The callback function is passed a table representation of the current row for each iteration of the loop.
      Syntax of each row is: { ["column_name_1"] = "value_1", ["column_name_2"] = "value_2" }.
    • If you (optionally) return a number other than 0 from the callback-function, you'll break the loop.
  • dbh:affected_rows() returns the number of rows affected by the last run INSERT, DELETE or UPDATE on the handle. It does not respond to SELECT operations.
  • dbh:release() (optional) releases the handle back to the pool so it can be re-used by another thread. This is also automatically done when the dbh goes out of scope and is garbage collected (for example when your script returns).

Take a look here for some examples.

Send an email with optional (converted) attachment.

Note that for this to work you have to have an MTA installed on your server, you also need to have 'mailer-app' configured in your switch.conf.xml.

You can use as follows:, from, headers, body, file, convert_cmd, convert_ext)
  • to (mandatory) a valid email address
  • from (mandatory) a valid email address
  • headers (mandatory) for example "subject: you've got mail!\n"
  • body (optional) your regular mail body
  • file (optional) a file to attach to your mail
  • convert_cmd (optional) convert file to a different format before sending
  • convert_ext (optional) to replace the file's extension

For example:"",
                 "subject: Voicemail from 1234\n",
                 "Hello,\n\nYou've got a voicemail, click the attachment to listen to it.",

then the following system command will be executed before attaching "message.mp3" to the mail and send it on its way:

mp3enc message.wav message.mp3


This is firing a custom event my::event.

local event = freeswitch.Event("custom", "my::event"); 
event:addHeader("My-Header", "test");
--Another one
function FSMan:fire(nombreHeader, header, body)--Manda un evento MESSAGE a algun receptor
  local miEvento = freeswitch.Event("MESSAGE");
  nombreHeader = Utils:trim(nombreHeader); header = Utils:trim(header); body = Utils:trim(body);
  if (nombreHeader == false ) then nombreHeader="Nombre_Header_Generico" end
  if (header == false) then header="Header_Generico" end
  if (body == false) then body="Body_Generico" end
  miEvento:addHeader(nombreHeader, header);


Consumes events from FreeSWITCH.


con = freeswitch.EventConsumer("<event_name>"[,"<subclass type>"]);

-- pop() returns an event or nil if no events

-- pop(1) blocks until there is an event

-- pop(1,500) blocks for max half a second until there is an event


con = freeswitch.EventConsumer("all"); 
session = freeswitch.Session("sofia/default/");                                                                                                                                                                                                                                                                    
while session:ready() do                                                                                                                                        
   session:execute("sleep", "1000");                                                                                                                            
   for e in (function() return con:pop() end) do                                                                                                                
      print("event\n" .. e:serialize("xml"));                                                                                                                      

-- or
while session:ready() do
    for e in (function() return con:pop(1,1000) end) do
        print("event\n" .. e:serialize("xml"))

-- You may subscribe to specific events if you want to, and even subclasses
con = freeswitch.EventConsumer("CUSTOM");
con = freeswitch.EventConsumer("CUSTOM","conference::maintenance");

-- wait for a specific event but continue after 500 ms
function poll()
    -- create event and listener
    local event = freeswitch.Event("CUSTOM", "ping::running?")
    local con = freeswitch.EventConsumer("CUSTOM", "ping::running!")

    -- add text ad libitum
    event:addHeader("hi", "there")
    -- fire event
    -- and wait for reply but not very long
    if con:pop(1, 500) then
        print("reply received")
        return true
    print("no reply")
    return false


Retrieves a global variable

my_globalvar = freeswitch.getGlobalVariable("varname")


hash = {                                                                                                                                                        
   ["main"] = undef,                                                                                                                                            
   ["name"] = "top",                                                                                                                                            
   ["greet_long"] = "phrase:demo_ivr_main_menu",                                                                                                                
   ["greet_short"] = "phrase:demo_ivr_main_menu_short",                                                                                                            
   ["invalid_sound"] = "ivr/ivr-that_was_an_invalid_entry.wav",                                                                                                                          
   ["exit_sound"] = "voicemail/vm-goodbye.wav",                                                                                                                 
   ["confirm_macro"] = "undef",                                                                                                                                 
   ["confirm_key"] = "undef",                                                                                                                                   
   ["confirm_attempts"] = "3",                                                                                                                                  
   ["inter_digit_timeout"] = "2000",                                                                                                                            
   ["digit_len"] = "1",                                                                                                                                         
   ["timeout"] = "10000",                                                                                                                                       
   ["max_failures"] = "3"                                                                                                                                       
top = freeswitch.IVRMenu(hash["main"],                                                                                                                          
top:bindAction("menu-exec-app", "playback /tmp/swimp.raw", "2");                                                                                                
top:execute(session, "top");                          

When using SAY, 3 additional variables have to be set or you will get the following error:

> [ERR] mod_lua.cpp:182 Error in IVRMenu expected 16..16 args, got 13 stack traceback:
> [C]: in function 'IVRMenu'
> /usr/local/freeswitch/scripts/ivr.lua:19: in main chunk

These variables are:

    ["tts_engine"]          = "flite",
    ["tts_voice"]           = "rms",
    ["max_timeouts"]        = "2"


Tells script to sleep for a specified number of milliseconds.
NOTE: Do not use this on a session-based script or bad things will happen.

  -- Sleep for 500 milliseconds


Create a new session.

local session = freeswitch.Session("sofia/");
session:transfer("3000", "XML", "default"); 

Create a new session with execute_on_answer variable set.

local session = freeswitch.Session("[execute_on_answer=info notice]sofia/");


API commands

You can write FreeSWITCH API commands *in Lua* by using the lua FreeSWITCH API command to run a script and pass the arguments in, then whatever you write with the stream object is what you get as a reply to that command. For example, given a script in the scripts directory called hello.lua with the following content:

 stream:write("hello world")

Running 'lua hello.lua' from the FreeSWITCH console would return back "hello world".

Or calling it from the dialplan like so:

 <action application="set" data="foo=${lua(hello.lua)}"/>

Would set the channel variable "foo" to "hello world".

Web page interaction (via mod_xml_rpc)
-- lua/api.lua
-- enable mod_xml_rpc and try in your webbrowser
stream:write("Content-Type: text/html\n\n");
stream:write("<title>FreeSWITCH Command Portal</title>");
stream:write("<h2>FreeSWITCH Command Portal</h2>");
stream:write("<form method=post><input name=command size=40> ");
stream:write("<input type=submit value=\"Execute\">");
stream:write("</form><hr noshade size=1><br>");

command = env:getHeader("command");

if (command)  then
   api = freeswitch.API();
   reply = api:executeString(command);

   if (reply) then
      stream:write("<br><B>Command Result</b><br>" .. reply .. "\n");


env:addHeader("cool", "true");
stream:write(env:serialize() .. "\n\n");

Example: Call Control

-- cc.lua
-- call control lua script
 dialA = "sofia/gateway/fs1/9903"
 dialB = "user/1001"                               
 legA = freeswitch.Session(dialA)                                                     
 dispoA = "None"       
while(legA:ready() and dispoA ~= "ANSWER") do                 
 dispoA = legA:getVariable("endpoint_disposition")                                                 
 freeswitch.consoleLog("INFO","Leg A disposition is '" .. dispoA .. "'\n")
 os.execute("sleep 1")
end -- legA while
if( not legA:ready() ) then 
 -- oops, lost leg A handle this case 
 freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " disconnected...\n")
 legB = freeswitch.Session(dialB) 
 dispoB = "None"
 while(legA:ready() and legB:ready() and dispoB ~= "ANSWER") do
   if ( not legA:ready() ) then
     -- oops, leg A hung up or got disconnected, handle this case
     freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " disconnected...\n")
     os.execute("sleep 1")
     dispoB = legB:getVariable("endpoint_disposition")
     freeswitch.consoleLog("NOTICE","Leg B disposition is '" .. dispoB .. "'\n")
   end -- inner if legA ready
 end -- legB while   
 if ( legA:ready() and legB:ready() ) then
   -- oops, one of the above legs hung up, handle this case
  freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " or " .. dialB .. " disconnected...\n")
end -- outter if legA ready

Special Case: env object

When lua is called as the hangup hook there will be a special env object that contains all the channel variables from the channel that just disconnected.

Add an extension to test this feature:

 <extension name="lua-env-hangup-hook-test">
   <condition field="destination_number" expression="^(1234)$">
     <action application="answer"/>
     <action application="set" data="api_hangup_hook=lua hook-test.lua"/>
     <action application="set" data="my_custom_var=foobar"/> 
     <action application="sleep" data="10000"/>
     <action application="hangup"/>                   

Then add freeswitch/scripts/hook-test.lua:

-- hook-test.lua                 
-- demonstrates using env to look at channel variables in hangup hook script

-- See everything
dat = env:serialize()            
freeswitch.consoleLog("INFO","Here's everything:\n" .. dat .. "\n")

-- Grab a specific channel variable
dat = env:getHeader("uuid")      
freeswitch.consoleLog("INFO","Inside hangup hook, uuid is: " .. dat .. "\n")                            

-- Drop some info into a log file...
res = os.execute("echo " .. dat .. " >> /tmp/fax.log")
res = os.execute("echo YOUR COMMAND HERE >> /tmp/fax.log")

-- If you created a custom variable you can get it also...
dat = env:getHeader("my_custom_var")
freeswitch.consoleLog("INFO","my_custom_var is '" .. dat .. "'\n")

Watch the FS console and dial 1234 and then hangup. You'll see all your channel variables!

See Also