Skip to content

Reading a Five9 export by hand

· Updated · ivrloom team

#five9 #engineering #review

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:

  • multiLanguagesPrompts is 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.)
  • userVariables declares the script’s own variables with their defaults.
  • modulesOnHangup is 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 at incomingCall will 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 like play and setVariable.
  • exceptionalDescendant — the error exit: where the call goes when a menu’s retries run out, a collection fails, or a sub-script raises.
  • branches (inside data) — named exits for ifElse (IF / ELSE), case (one entry per condition) and menu (one entry per option), each with a desc holding 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 desc targets.

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:

  1. Find incomingCall. That is the entry point; the flow reads outward from there.
  2. Count the hangup nodes. 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’s returnToCallingModule too: in a sub-script, a hangup with that set to true does not end the call at all — it returns to whoever called the script.
  3. Scan for foreignScript. Those are calls into other scripts. The flow you are reading is not the whole flow. Each one names its target under ivrScript and carries passCRM / returnCRM flags saying whether variables flow across the boundary (in the exports we have seen, almost always false).

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.