> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voxworks.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Script Text Functions

> Text functions are tags you type straight into a script line: rand-pick varies the wording, rand-sort rotates the order options are read out, and bool shows or hides text based on a condition. This page covers the exact syntax and evaluation rules for all three.

## What are Script Text Functions?

Text functions are special `<<...>>` tags you type directly into the script text of a [voice step](/flows/steps-voice). They render fresh every time the assistant speaks the line, so the same script can sound different from call to call — and even from one attempt to the next within a call.

There are three functions:

* **`<<rand-pick[...]>>`** — speaks one option from a list, chosen at random
* **`<<rand-sort[...]>>`** — reads a list of options out in a random order
* **`<<bool[...]>>`** — includes a block of text only when a condition is true

When a tag is written correctly, the caller never hears it — the assistant speaks the rendered result. Text functions work alongside [variables](/flows/variables): the text inside a function can contain `{{...}}` variable references, inserted by typing `/` in the script editor as usual.

***

## How rendering works

Each time the assistant is about to speak a line, the platform renders it in a fixed order:

1. **Text functions render first.** Nested tags render innermost-first, so a function written inside another function's content is resolved before the outer one.
2. **Variables substitute afterwards.** Any `{{...}}` references left in the surviving text — including references inside a rand-pick option or a bool block — are replaced with their current values.

That ordering has three practical consequences:

* **Function content can contain variables.** `<<bool[...]:Thanks {{contact.first_name}}!>>` works — the name substitutes after the condition decides whether the text survives. (`{{contact.first_name}}` is a string, e.g. `Jane`.)
* **Variable values are never treated as functions.** If a variable's value happens to contain `<<...>>` syntax — say, text captured from a caller — it is spoken as plain text, never executed.
* **Rendering happens per delivery, not per call.** A step that fires again re-picks, re-shuffles and re-evaluates. Two calls to the same script — or two attempts at the same line — can produce different wording.

One behaviour to keep in mind while authoring: a **malformed tag is left in the line exactly as you typed it**, and the assistant may then read the raw tag text aloud. Other valid tags in the same line still render normally. See [When a tag doesn't render](#when-a-tag-doesnt-render) for the common causes.

***

## Random selection — `<<rand-pick>>`

Speaks exactly one option from a list, chosen at random each time the line is delivered.

```text theme={null}
<<rand-pick["No worries at all.","Not a problem.","All good.","Easy done.","Too easy.","That's fine.","Happy to help.","Of course."]>> Let me bring up your booking now, {{contact.first_name}}.
```

| Rule                  | Detail                                                                                   |
| --------------------- | ---------------------------------------------------------------------------------------- |
| Payload               | A JSON array of double-quoted strings: `["a","b","c"]`. Single quotes are not valid.     |
| Minimum options       | One — but aim for **8–10 or more**. Thin lists sound canned across repeat calls.         |
| Variables             | Options may contain `{{...}}` references; they substitute after the pick.                |
| Whitespace            | The chosen option is trimmed, so you can format the array across lines.                  |
| When the pick happens | Every time the line is spoken — including retries and repeated steps, not once per call. |

**Write each option as a complete, self-contained line.** The assistant speaks exactly one of them, so if the line asks a question, every option must carry the question — not just the first.

An invalid payload — an empty array, an option that isn't a quoted string, broken JSON, or a missing closing `>>` — leaves the whole tag in the text as literal words.

***

## Random ordering — `<<rand-sort>>`

Reads a list of options out in a shuffled order — most useful for survey-style questions where reading options in the same order every call would bias the answers.

The payload is **exactly two arrays**: the options to shuffle, then the filler words spoken between them.

```text theme={null}
<<rand-sort[["a coffee","a tea","a juice"],["or","or maybe"]]>>
```

This can render as *a juice or a coffee or maybe a tea* — the options shuffle, while the fillers stay in the order you wrote them and are slotted between the shuffled options with spacing handled for you.

| Rule                     | Detail                                                                                                |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| Payload shape            | `[[option, option, ...], [filler, filler, ...]]` — two JSON arrays of double-quoted strings.          |
| Filler count             | Exactly **one fewer filler than options**. Use `[",",","]`-style fillers if you only want separators. |
| What shuffles            | Only the options. Fillers keep their written order.                                                   |
| Variables                | Options and fillers may contain `{{...}}` references; they substitute after the shuffle.              |
| When the shuffle happens | Every time the line is spoken.                                                                        |

A flat single array — `<<rand-sort["a","b","c"]>>` — is **not valid** and is left in the text as a literal tag. The same goes for a filler-count mismatch.

**Keep fixed items out of the block.** Options that must always come last — an "Another party" catch-all, a "Don't know" you don't read out — are written after the tag, not inside it:

```text theme={null}
Read out the following parties in the order presented, then read "Another party":
<<rand-sort[["The Liberals","The Greens","One Nation","An Independent"],[",",",",","]]>>, Another party.
```

***

## Conditional text — `<<bool>>`

Includes a block of text only when an expression is true. When the expression is false, the tag renders as nothing at all.

```text theme={null}
<<bool[EXPRESSION]:CONTENT>>
```

The content runs from the `:` up to the next `>>` and can span multiple lines. There is **no else branch** — to branch both ways, write a pair of tags with opposite expressions.

### What an expression can contain

| Element             | Example                                     |   |                  |
| ------------------- | ------------------------------------------- | - | ---------------- |
| Variable references | `{{custom.ask_count}}`, `{{contact.email}}` |   |                  |
| Numbers             | `3`, `-2`, `3.5`                            |   |                  |
| Strings             | `"active"` — double quotes only             |   |                  |
| Booleans            | `true` / `false` (any capitalisation)       |   |                  |
| Null                | `null`                                      |   |                  |
| Comparisons         | `==` `!=` `>` `>=` `<` `<=`                 |   |                  |
| Logic               | `&&` (and), \`                              |   | `(or),`!\` (not) |
| Grouping            | `(` `)`                                     |   |                  |

Any other character makes the **whole expression invalid**, and the entire tag is left in the text as written. `!` binds tightest, then a single comparison, then `&&`, then `||`; use parentheses to group. Chained comparisons like `1 < {{custom.ask_count}} < 5` are not supported — write `{{custom.ask_count}} > 1 && {{custom.ask_count}} < 5`.

### How values are compared

* **Unset counts as null.** A variable that has never been written — or holds an empty string — reads as `null`. `{{contact.email}} == null` is true until an email is captured. A misspelt variable path also reads as null rather than breaking the expression.
* **Bare references use truthiness.** `<<bool[{{custom.vip_flag}}]:...>>` renders when the value is truthy (`{{custom.vip_flag}}` is a boolean, e.g. `true`). Falsy values: `null`, empty string, `0`, `false`, and the text `"false"`. Everything else is truthy.
* **Booleans compare with `== true` / `== false` — never `== 1` / `== 0`.** Comparing a boolean to a number makes the expression *invalid*, not false: the whole tag is left in the text and can be read aloud. The strings `"true"` and `"false"` count as booleans in equality checks.
* **Numbers compare numerically.** `"5" == 5` is true, and the ordering operators (`>` `>=` `<` `<=`) need both sides to be numeric — numeric text like `"5"` counts, anything else makes the expression invalid.
* **Everything else compares as text.** `{{custom.status}} == "active"` is a plain string comparison (`{{custom.status}}` is a string, e.g. `active`).

### Examples

A re-ask ladder that changes wording by attempt, driven by a per-call counter an earlier step writes (`{{custom.ask_count}}` is an integer, e.g. `2`):

```text theme={null}
<<bool[{{custom.ask_count}} == 1]:Could I grab your email address?>>
<<bool[{{custom.ask_count}} == 2]:Sorry, I didn't quite catch that — could you spell it out for me?>>
<<bool[{{custom.ask_count}} >= 3]:No trouble — I'll confirm it by text instead.>>
```

An insert-or-fallback pair around a variable that may not be set (`{{contact.email}}` is a string, e.g. `jane.citizen@example.com`):

```text theme={null}
<<bool[{{contact.email}} != null]:I have your email as {{contact.email}} — is that still right?>>
<<bool[{{contact.email}} == null]:What's the best email address for you?>>
```

A combined condition (`{{custom.plan}}` is a string, e.g. `premium`):

```text theme={null}
<<bool[{{custom.plan}} == "premium" && {{custom.ask_count}} < 3]:As a premium member you also get priority support.>>
```

### Nesting and coverage

* A bool block's content may contain further `<<bool>>` or `<<rand-pick>>` tags — they render first, innermost-out. Keep inner tags valid: an invalid inner tag stays in the text as literal words, and its `>>` then ends the outer block early.
* **A line made only of bool guards can render empty** when no condition matches — the assistant then improvises to cover the moment. Either make your conditions cover every state the line can be reached in, or keep plain fallback text outside the guards.

***

## When a tag doesn't render

If you hear the assistant read out something like *"less than less than bool"*, a tag was malformed and left in the text as literal words. Work through the usual causes:

| Symptom                                        | Likely cause                                                                                                                                              |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tag spoken aloud                               | Single quotes in the payload (must be JSON double quotes); missing closing `>>`; missing `:` after a bool expression; a stray character in the expression |
| Bool tag spoken aloud despite looking right    | `== 1` / `== 0` against a boolean — compare with `== true` / `== false`; or a chained comparison                                                          |
| rand-sort spoken aloud                         | A flat single array instead of two arrays, or the wrong filler count (options minus one)                                                                  |
| Bool always takes the fallback branch          | A misspelt variable path in the expression — unknown variables read as empty/null                                                                         |
| Line comes out empty and the assistant ad-libs | Every bool guard on the line was false — cover the missing state or add fallback text outside the guards                                                  |

The quickest check is a [test call](/scripts/testing): trigger the step a few times and listen for the tag rendering differently on each pass.

***

## Beyond text functions

For demanding use cases, the Voxworks team builds precision-engineered conversation flows on top of the same platform — deterministic phrase rotation that avoids back-to-back repeats, multi-attempt re-ask ladders, survey option ordering that removes position bias, and tightly validated data-capture sequences. These are authored with internal tooling rather than the script editor. Speak to the Voxworks team for further custom configuration.

***

## Next Steps

* [Voice Steps](/flows/steps-voice) — The script text these functions render inside
* [Variables in Flows](/flows/variables) — The `{{...}}` references you can use inside function content and conditions
* [Testing Your Script](/scripts/testing) — Hear your text functions render on a test call
* [Custom Variables](/custom-variables/overview) — Typed per-contact data your conditions can check
