Reading a Five9 export by hand
· Updated · ivrloom team
A Five9 IVR export is a .zip containing XML. If you have ever wanted to answer “what changed?” without opening the Script Designer, it helps to know what is inside.
The shape of the file
Scripts live under SCRIPTS/ with a .five9ivr extension. Each one is a single XML document. The flow is in modules; everything else is script-level context. In the production exports we have examined, the top level looks like this:
<ivrScript>
<domainId>…</domainId>
<properties/>
<modules>…</modules>
<modulesOnHangup>…</modulesOnHangup>
<userVariables>…</userVariables>
<multiLanguagesPrompts>…</multiLanguagesPrompts>
<multiLanguagesMenuChoices>…</multiLanguagesMenuChoices>
<languages>…</languages>
<defaultLanguage>…</defaultLanguage>
<functions>…</functions>
<defaultFetchTimeout>…</defaultFetchTimeout>
<timeoutInMilliseconds>…</timeoutInMilliseconds>
<version>…</version>
</ivrScript>
Three of those sections are worth knowing about before you look at a single module:
multiLanguagesPromptsis the script’s prompt library — every recording and TTS entry the modules reference by id. It is large, and it is the section an editor is most likely to drop if it only models the flow. (We did, once. Every re-imported bundle lost its prompts until a real export caught it.)userVariablesdeclares the script’s own variables with their defaults.modulesOnHangupis a second, smaller flow: the modules that run when the caller hangs up. It is not reachable from the main entry point, so a reachability check that starts atincomingCallwill report every one of them as orphaned unless it knows about this section.
Element names are the node types
This is the part that surprises people coming from the visual designer: each module is an element named after its type, in lowerCamelCase.
<modules>
<incomingCall>…</incomingCall>
<menu>…</menu>
<getDigits>…</getDigits>
<ifElse>…</ifElse>
<case>…</case>
<setVariable>…</setVariable>
<play>…</play>
<foreignScript>…</foreignScript>
<skillTransfer>…</skillTransfer>
<hangup>…</hangup>
</modules>
If you are scanning a diff, the element name tells you what kind of thing changed before you read a single attribute.
Some sense of proportion helps too. Across two dozen production scripts from one tenant, the most common modules were setVariable (over 250), ifElse (around 180), foreignScript (over 130), skillTransfer (around 100), hangup and play (around 90 each), case (around 60) and menu (around 50). getDigits appeared only 13 times. The picture that paints — a flow that is mostly variable assignment and branching, with a lot of calls into other scripts — is typical, and it is why “the IVR” is rarely one file.
Every module has an id, a name and connections
<menu>
<ascendants>AAAAAAAAAAAAAAAAAA000001AAAAAAAA</ascendants>
<exceptionalDescendant>FFFFFFFFFFFFFFFFFF000009FFFFFFFF</exceptionalDescendant>
<moduleName>Main Menu</moduleName>
<locationX>320</locationX>
<locationY>140</locationY>
<moduleId>BBBBBBBBBBBBBBBBBB000002BBBBBBBB</moduleId>
<data>…</data>
</menu>
moduleName is what you see on the canvas. moduleId is what the flow actually points at — a 32-character identifier. The connections come in three flavours:
singleDescendant— the one next module, for linear nodes likeplayandsetVariable.exceptionalDescendant— the error exit: where the call goes when a menu’s retries run out, a collection fails, or a sub-script raises.branches(insidedata) — named exits forifElse(IF/ELSE),case(one entry per condition) andmenu(one entry per option), each with adescholding the target id.
ascendants lists who points at this module. It is derived information — the forward connections are the truth — and real exports contain stale entries: a parent still listed after its branch was rerouted, or a real parent missing. Read it as a hint, not a fact.
Two things follow from ids being the reference:
- Renaming a module is safe. Connections reference the id.
- A rewire is invisible in the names. If a branch now points somewhere else, nothing about the module names changes. You have to compare the
desctargets.
That second point is the single best argument for diffing exports rather than eyeballing them.
Position is data too
locationX and locationY are the canvas coordinates. They carry no behaviour, but they carry intent — the person who built it arranged those nodes for a reason. Any tool that rewrites your export should leave them alone unless you moved something.
Conditions and variables
An ifElse or case condition is verbose but regular. Each clause has a comparisonType, a left operand and a right operand, and each operand says whether it is a variable or a literal:
<conditions>
<comparisonType>EQUALS</comparisonType>
<leftOperand>
<isVarSelected>true</isVarSelected>
<variableName>__BUFFER__</variableName>
</leftOperand>
<rightOperand>
<isVarSelected>false</isVarSelected>
<stringValue><value>1</value><id>0</id></stringValue>
</rightOperand>
</conditions>
EQUALS dominates. In the exports we have looked at it accounted for roughly three quarters of all comparisons, with LESS_THAN, MORE_THAN and LIKE making up most of the rest and IS_NULL and REGEXP rare. Variable names carry their scope as a prefix: __BUFFER__-style names are Five9 built-ins (the digit buffer, the day and time), names with a dot are call variables — the built-in Call. group, Contact. fields from the CRM record, and any group your team created, such as Agency.Skill — and bare names are script-local.
What to look at first
When you open an unfamiliar export — whether by hand or in an editor (the FAQ covers how the export gets into and out of ivrloom) — three passes get you oriented quickly:
- Find
incomingCall. That is the entry point; the flow reads outward from there. - Count the
hangupnodes. Every one is a way the call ends. If there are more than you expected, some of them are probably error paths worth understanding. Look at each one’sreturnToCallingModuletoo: in a sub-script, a hangup with that set totruedoes not end the call at all — it returns to whoever called the script. - Scan for
foreignScript. Those are calls into other scripts. The flow you are reading is not the whole flow. Each one names its target underivrScriptand carriespassCRM/returnCRMflags saying whether variables flow across the boundary (in the exports we have seen, almost alwaysfalse).
The parts that are easy to lose
An export contains elements that a tool may not model — vendor-specific blocks, newer node types, fields added since whatever built your tooling was written. skillTransfer alone has over forty fields covering queue timeouts, callback offers and voicemail fallbacks. The safe behaviour is to preserve everything you do not understand byte-for-byte and re-emit it unchanged.
We wrote about that decision in more detail in reverse-engineering the XML: the rule we settled on is that anything we do not model is copied verbatim rather than normalised, because a normalisation you did not intend is indistinguishable from data loss. The test for it is mechanical — import, export, compare bytes — and it is the one test we would keep if we could keep only one.