VoIP.info Guide

FreePBX 17 macro-to-Gosub migration for custom dialplan

Inventory every custom Macro dependency before a FreePBX 17 restore, translate its call and return semantics into explicit Gosub frames, and prove the loaded route without editing generated dialplan.

Reviewed 2026-10-14Sangoma FreePBX 17 migration and platform guidance plus Asterisk 20 Macro semantics and current branch-23 Gosub, Return, LOCAL, LOCAL_PEEK and StackPop documentation reviewed October 14, 2026; Sangoma says the supported FreePBX 17 install defaults to Asterisk 22, where Macro is unavailable and custom dialplan or AGI macro calls require manual conversion, but installed FreePBX, Asterisk, modules, custom files, includes, generated dialplan, subroutine stack, variables, channels, routes and calls remain deployment specific, and no PBX, backup, restore, file, dialplan, AGI script, channel, route or call was inspected or testedAdvancedDocumentation based · not lab tested

Treat the Asterisk engine change as a compatibility gate

Sangoma says the supported FreePBX 17 install defaults to Asterisk 22 and that Asterisk 22 does not support the Macro dialplan application. Its migration page warns that restored custom dialplan or AGI scripts using macros will not work until those calls are converted manually to Gosub. FreePBX-generated dialplan has already moved away from Macro, but that does not rewrite site-owned files or external integrations.

Record the source FreePBX and Asterisk versions, target versions, backup hash, restore transaction, installed module inventory and every custom include. A successful restore proves archive processing, not that every custom application exists or every route reached its former destination.

Inventory callers, macro contexts and hidden contracts

Search supported custom include files, Custom Destinations, AGI source and deployment tooling for Macro(, MacroIf, MacroExit, MacroExclusive, macro- contexts and MACRO_ variables. Preserve each caller, argument order, branch, destination and expected continuation before changing syntax. Do not edit FreePBX-generated files, because Apply Config can replace them.

Asterisk 20 documented Macro as entering extension s in macro-name, exposing ARG1 onward and saving the original location in MACRO_EXTEN, MACRO_CONTEXT and MACRO_PRIORITY. A mechanical name change does not recreate those hidden values, MacroExit behavior, offset continuation or exclusivity.

Translate the call target and return path explicitly

Current Asterisk documents Gosub([[context,]extension,]priority(args)) as a jump that saves a return address. Convert one caller at a time to a complete context, extension and priority, pass reviewed arguments in parentheses and end every returning branch with Return(). A fall-through, Goto() away from the subroutine or hangup is a different outcome.

Return(value) removes the last Gosub stack frame and stores the value in GOSUB_RETVAL. Preserve whether the legacy caller expected a variable side effect, an offset, a jump, a hangup or a value. Do not invent a return contract simply because the new subroutine reaches its last priority.

Replace implicit Macro variables deliberately

Gosub arguments remain available as ARG1, ARG2 and so on, but Macro location variables are not a portable substitute for the caller identity in a Gosub frame. Pass the original extension or context as explicit arguments when the business rule genuinely needs them, and document the exact owner.

Use LOCAL(name) for scratch values that must belong only to the current Gosub frame; Asterisk says the value disappears or restores its previous value after Return(). LOCAL_PEEK() can inspect a variable hidden by a specified number of frames, but an empty result can also mean the requested depth does not exist. Ordinary channel variables, inherited variables and local frame variables remain separate scopes.

Preserve FreePBX ownership and supported entry points

Keep the converted routine in an approved custom include or other site-controlled source. Point the existing FreePBX Custom Destination or authorized hook to the stable public entry point, then Apply Config and inspect the loaded dialplan. Do not paste a replacement into module-generated contexts or rename a module-owned destination merely to match the old macro label.

Draw the complete call path from inbound or internal source through the FreePBX destination, custom entry, Gosub frame, returned value or side effect and next supported destination. A subroutine that works from asterisk -rx inspection is not proof that the production route enters it on the intended channel.

Accept the migration with stack and route evidence

On an isolated restored target, load the exact custom revision, confirm no obsolete Macro application is referenced, inspect the public entry and subroutine priorities, and run a bounded matrix covering normal return, each conditional branch, invalid input, hangup and nested calls. Correlate channel, unique and linked identifiers, Gosub entry, arguments, local values, GOSUB_RETVAL, return priority and final destination.

Use StackPop() only when the intended behavior is explicitly to discard the most recent return address; it is not a generic repair for an unbalanced routine. Keep the prior target available for rollback and do not register production trunks or route public calls from an isolated restore. No PBX, restore, dialplan, channel or call was tested for this guide.

Asterisk custom include — explicit Gosub contract
[from-custom-entry]
exten => migrate-check,1,Gosub(site-route,s,1(${EXTEN},normal))
 same => n,NoOp(Subroutine result: ${GOSUB_RETVAL})
 same => n,Goto(from-internal,${EXTEN},1)

[site-route]
exten => s,1,Set(LOCAL(original_extension)=${ARG1})
 same => n,Set(LOCAL(mode)=${ARG2})
 same => n,Return(ok)

Sources & applicability

Primary references for the technical details above. Operational examples and planning checklists are VoIP.info editorial guidance.

Examples require adaptation to your topology. No live PBX or hardware testing is claimed.