This post is a rework of one of my very popular posts from years ago, finally updated for FreePBX 17 and up. This should give you a very good start on leveraging dialplan hooks for adding your own custom dialplan to FreePBX. None of this will work as written in FreePBX versions earlier than 17
Despite the frequency with which it arises here in the forum, there is not yet a good resource for learning to use dialplan hooks in FreePBX. This will probably end up in the wiki at some point, but until that happens, here are the broad strokes for leveraging dialplan hooks in FreePBX 14.
What is a dialplan hook?
A dialplan hook refers to several pre-defined FreePBX contexts that exist solely for users to add their own Asterisk subroutines to be run at specific locations in the call flow. They mostly occur immediately before a Dial()
application and are used to run custom dialplan to perform some function not supported by the GUI. As a user, you define dialplan hooks as contexts in the file /etc/asterisk/extensions_custom.conf
. Use whatever method you are comfortable with for editing the file; beginners may find it useful to use the Config Edit module. You may be aware of something in Asterisk called a âPredial Handlerâ. Despite the name similarity with the macros that follow, they are completely separate things having nothing to do with each other.
When do I use a dialplan hook and when do I use a Custom Destination?
If youâre able to structure your call flow such that the custom dialplan you want to execute looks something like this:
Inbound Route -> Time Condition -> <custom dialplan> -> Ring Group
Then you donât need or want to use a dialplan hook. The above is better done with a Custom Destination (Admin â Custom Destination). Custom Destinations are generally preferred over dialplan hooks, as itâs easier to debug and probably more future proof. In FreePBX, I recommend using the GUI wherever possible.
A word about Macro and Gosub
Macro and Gosub are Asterisk dialplan applications. Since the early days of FreePBX, the dialplan hooks have relied on the Macro application, but now that Asterisk has officially deprecated and removed app_macro, dialplan hooks in FreePBX 15+ rely exclusively on Gosub. Older documentation may still reference Macro and MacroExit, and if so, it will need to be adjusted to work with current versions.
Note that the dialplan hooks that follow have the word âmacroâ in the name but they are not macros. They are dialplan subroutines.
How do I do X on every outbound call?
The first (and for the longest time, the only) dialplan hook going back more than a decade is macro-dialout-trunk-predial-hook
. There are many examples floating around the 'net on how to use it, and itâs existence is testament to all the configuration edge cases for the various SIP providers around the world. If your provider requires some specific setting that canât be done via the GUI, you would use this hook in order to make the necessary channel modifications prior to the trunk dial. A bare bones example would look like this:
[macro-dialout-trunk-predial-hook]
exten => s,1,NoOp(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
; additional lines here
exten => s,n,Return
The above example is trivial, it only records a single line in the Asterisk full log and then exits sending the call back to the FreePBX generated dialplan. An extension of s
is used as is required for all Macros. You can see the above in action by saving the change, reloading the dialplan and make a call after running the following at the bash prompt:
[root@freepbx ~]# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 10:15:06] VERBOSE[1462625][C-0000000f] pbx.c: Executing [s@macro-dialout-trunk:23] Gosub("PJSIP/4002-00000012", "macro-dialout-trunk-predial-hook,s,1()") in new stack
[2025-05-15 10:15:06] VERBOSE[1462625][C-0000000f] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:1] NoOp("PJSIP/4002-00000012", "Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 10:15:06] VERBOSE[1462625][C-0000000f] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:2] Return("PJSIP/4002-00000012", "") in new stack
Once the bare bones dialplan is in place and confirmed working with tail
, the hard part begins where you actually write useful dialplan that both works for all edge cases and doesnât break all the other edge cases.
How do I do X on every local call?
Itâs only a little less straight forward to perform similar action on local extension calls. There are several dialplan hooks provided by the core module that are used for calls to local extensions depending on the ring strategy used to reach the extension. You can see the dialplan hooks in use on local calls by running the same tail
from above. So focusing on a simple case where local extension 5004 calls local extension 5005 with no FMFM enabled, you would see the following:
[root@freepbx ~]# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 10:21:20] VERBOSE[1463188][C-00000010] pbx.c: Executing [s@macro-dial-one:53] Gosub("PJSIP/4002-00000014", "macro-dialout-one-predial-hook,s,1()") in new stack
[2025-05-15 10:21:20] VERBOSE[1463188][C-00000010] pbx.c: Executing [s@macro-dialout-one-predial-hook:1] Return("PJSIP/4002-00000014", "") in new stack
The above shows us that just before a call goes to a local extension, the subroutine macro-dialout-one-predial-hook,s,1
is called. Using the same technique as above, we can add our own dialplan such as:
[macro-dialout-one-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
; add additional lines here
exten => s,n,Return
After save and reload, the tail
confirms itâs working:
[root@freepbx ~]# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 10:25:19] VERBOSE[1463586][C-00000011] pbx.c: Executing [s@macro-dial-one:53] Gosub("PJSIP/4002-00000016", "macro-dialout-one-predial-hook,s,1()") in new stack
[2025-05-15 10:25:19] VERBOSE[1463586][C-00000011] pbx.c: Executing [s@macro-dialout-one-predial-hook:1] NoOp("PJSIP/4002-00000016", "Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 10:25:19] VERBOSE[1463586][C-00000011] pbx.c: Executing [s@macro-dialout-one-predial-hook:2] Return("PJSIP/4002-00000016", "") in new stack
How do I apply custom dialplan selectively on some calls?
There is no single simple answer to this and sometimes the answer is âyou canâtâ. But usually one can identify a channel variable that is set to a unique value for all the calls you want to modify, and if so, that allows you to write dialplan that only acts on those calls. A simple example would be dialplan that does something different if the local call originates from extension 4002:
[macro-dialout-one-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
exten => s,n,GotoIf($["${AMPUSER}"="4002"]?special)
exten => s,n,Return
exten => s,n(special),NoOp(Incoming call from exension 4002)
exten => s,n,Return
Call from 4002:
[root@freepbx ~]# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx.c: Executing [s@macro-dial-one:53] Gosub("PJSIP/4002-00000018", "macro-dialout-one-predial-hook,s,1()") in new stack
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx.c: Executing [s@macro-dialout-one-predial-hook:1] NoOp("PJSIP/4002-00000018", "Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx.c: Executing [s@macro-dialout-one-predial-hook:2] GotoIf("PJSIP/4002-00000018", "1?special") in new stack
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx_builtins.c: Goto (macro-dialout-one-predial-hook,s,4)
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx.c: Executing [s@macro-dialout-one-predial-hook:4] NoOp("PJSIP/4002-00000018", "Incoming call from exension 4002") in new stack
[2025-05-15 10:27:45] VERBOSE[1463862][C-00000012] pbx.c: Executing [s@macro-dialout-one-predial-hook:5] Return("PJSIP/4002-00000018", "") in new stack
Call from 4003:
[root@freepbx ~]# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 10:34:26] VERBOSE[1465645][C-00000016] pbx.c: Executing [s@macro-dial-one:53] Gosub("PJSIP/4003-0000001f", "macro-dialout-one-predial-hook,s,1()") in new stack
[2025-05-15 10:34:26] VERBOSE[1465645][C-00000016] pbx.c: Executing [s@macro-dialout-one-predial-hook:1] NoOp("PJSIP/4003-0000001f", "Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 10:34:26] VERBOSE[1465645][C-00000016] pbx.c: Executing [s@macro-dialout-one-predial-hook:2] GotoIf("PJSIP/4003-0000001f", "0?special") in new stack
[2025-05-15 10:34:26] VERBOSE[1465645][C-00000016] pbx.c: Executing [s@macro-dialout-one-predial-hook:3] Return("PJSIP/4003-0000001f", "") in new stack
And that leads to the obvious question, how do I know which channel variable to check and for what value? Each situation is unique so you need to figure that out for yourself. In doing so the DumpChan
dialplan application is essential. Otherwise start a thread asking for ideas.
How do I add a hook only on trunk calls for a specific trunk
Itâs not unusual to have multiple providers configured as trunks, and it may be necessary to alter calls selectively depending on which trunk is in use. This dialplan will display the name of the trunk as entered in the fpbx GUI.
[macro-dialout-trunk-predial-hook]
exten => s,1,NoOp(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
exten => s,n,ExecIF($["${OUT_${DIAL_TRUNK}_SUFFIX}"!=""]?Set(trunk_name=${OUT_${DIAL_TRUNK}_SUFFIX:1}):Set(trunk_name=${OUT_${DIAL_TRUNK}}))
exten => s,n,Noop(Dialing out on trunk: ${trunk_name})
; additional lines here
exten => s,n,Return
Asterisk full log lines
[2025-05-15 10:53:13] VERBOSE[1468669][C-00000020] pbx.c: Executing [s@macro-dialout-trunk:23] Gosub("PJSIP/4002-0000002e", "macro-dialout-trunk-predial-hook,s,1()") in new stack
[2025-05-15 10:53:13] VERBOSE[1468669][C-00000020] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:1] NoOp("PJSIP/4002-0000002e", "Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 10:53:13] VERBOSE[1468669][C-00000020] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:2] ExecIf("PJSIP/4002-0000002e", "1?Set(trunk_name=1.us-central.clearlyip.com):Set(trunk_name=PJSIP)") in new stack
[2025-05-15 10:53:13] VERBOSE[1468669][C-00000020] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:3] NoOp("PJSIP/4002-0000002e", "Dialing out on trunk: 1.us-central.clearlyip.com") in new stack
[2025-05-15 10:53:13] VERBOSE[1468669][C-00000020] pbx.c: Executing [s@macro-dialout-trunk-predial-hook:4] Return("PJSIP/4002-0000002e", "") in new stack
How do I perform a custom action after hangup?
Nothing to it. Using the techniques above, you would add a hangup handler to the channel. A hangup handler, as the name implies, is a subroutine that runs after the channel hangs up. A simple example:
The dialplan:
[macro-dialout-one-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
exten => s,n,Set(CHANNEL(hangup_handler_push)=lgaetz-do-this-on-hangup,s,1)
exten => s,n,Return
[lgaetz-do-this-on-hangup]
exten => s,1,Noop(Entering user defined context lgaetz-do-this-on-hangup in extensions_custom.conf)
; additional lines
exten => s,n,Return
The log entries:
[root@freepbx asterisk]# tail -f /var/log/asterisk/full | grep lgaetz
[2025-05-15 10:37:45] VERBOSE[1466011][C-00000018] pbx.c: Executing [s@macro-dialout-one-predial-hook:2] Set("PJSIP/4002-00000024", "CHANNEL(hangup_handler_push)=lgaetz-do-this-on-hangup,s,1") in new stack
[2025-05-15 10:37:50] VERBOSE[1466011][C-00000018] app_stack.c: PJSIP/4002-00000024 Internal Gosub(lgaetz-do-this-on-hangup,s,1) start
[2025-05-15 10:37:50] VERBOSE[1466011][C-00000018] pbx.c: Executing [s@lgaetz-do-this-on-hangup:1] NoOp("PJSIP/4002-00000024", "Entering user defined context lgaetz-do-this-on-hangup in extensions_custom.conf") in new stack
[2025-05-15 10:37:50] VERBOSE[1466011][C-00000018] pbx.c: Executing [s@lgaetz-do-this-on-hangup:2] Return("PJSIP/4002-00000024", "") in new stack
[2025-05-15 10:37:50] VERBOSE[1466011][C-00000018] app_stack.c: PJSIP/4002-00000024 Internal Gosub(lgaetz-do-this-on-hangup,s,1) complete GOSUB_RETVAL=
Dialplan hook in the Allowlist module
All of the dialplan hooks discussed to this point are provided by the core module. While Iâm not aware of any others, thereâs nothing stopping anyone from adding hooks elsewhere in the fpbx dialplan, and in fact the contributor of the Allowlist has done just that. If you have the module installed and enabled for an inbound route, doing a tail
for predial will result in lines like this in the asterisk full log:
# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 12:10:40] VERBOSE[1475682][C-00000027] pbx.c: Executing [s@app-allowlist-check:1] GosubIf("PJSIP/2.us-east.clearlyip.com-00000036", "0?app-allowlist-check-predial-hook,s,1()") in new stack
Knowing the subroutine context is called app-allowlist-check-predial-hook
we can populate that context in extensions_custom.conf
. Thereâs even a mechanism in the Allowlist module to set a channel variable if to allow the call, so you can offload your allowlist lookups to something more complicated than what the GUI offers.
[app-allowlist-check-predial-hook]
exten => s,1,NoOp(Entering user defined context app-allowlist-check-predial-hook in extensions_custom.conf)
exten => s,n,Set(callerallowed=1)
exten => s,n,Return
Now an inbound call generates log lines like this:
# tail -f /var/log/asterisk/full | grep predial
[2025-05-15 12:16:17] VERBOSE[1476198][C-00000028] pbx.c: Executing [s@app-allowlist-check:1] GosubIf("PJSIP/2.us-east.clearlyip.com-00000037", "1?app-allowlist-check-predial-hook,s,1()") in new stack
[2025-05-15 12:16:17] VERBOSE[1476198][C-00000028] pbx.c: Executing [s@app-allowlist-check-predial-hook:1] NoOp("PJSIP/2.us-east.clearlyip.com-00000037", "Entering user defined context app-allowlist-check-predial-hook in extensions_custom.conf") in new stack
[2025-05-15 12:16:17] VERBOSE[1476198][C-00000028] pbx.c: Executing [s@app-allowlist-check-predial-hook:2] Set("PJSIP/2.us-east.clearlyip.com-00000037", "callerallowed=1") in new stack
[2025-05-15 12:16:17] VERBOSE[1476198][C-00000028] pbx.c: Executing [s@app-allowlist-check-predial-hook:3] Return("PJSIP/2.us-east.clearlyip.com-00000037", "") in new stack