# Callflows: how calls are routed

Apply the [authorization policy](../SKILL.md#authorization-and-working-style) to every step,
including local files, sends, paid features and test calls. Proceed within existing authority;
ask only for missing decisions or authority, or report blocked steps when unattended.

A **callflow** maps dialed numbers to a tree of actions ("modules"). When a call arrives for a
number (a phone number in E.164 like `+16045550123`, or an extension like `1001`), Rinzo
finds the callflow listing it and runs `flow`.

Field names, enums and defaults below come from the platform schemas Rinzo runs
(`callflows.<module>.json`) or from documents SmartPBX wrote. A module marked *unverified*
exists in the platform but hasn't been tested on Rinzo yet. Verify with an authorized test call or
report the live check pending; simulation is not live proof.

For a main number with hours, holidays and menus, `rinzo auto-attendant create` writes all of
this from one spec (recipes.md 2). Whatever you build, `rinzo callflows simulate NUMBER --at TIME
--keys …` predicts configured routing: use it before any authorized test call.

## Document

```json
{
  "name": "Sales line",
  "numbers": ["2000", "+16045550123"],
  "flow": {
    "module": "ring_group",
    "data": { "...": "module settings" },
    "children": {
      "_": { "module": "voicemail", "data": {"id": "VMBOX_ID"}, "children": {} }
    }
  }
}
```

- `flow` is required, plus at least one entry in `numbers` or `patterns` (regexes, e.g.
  `"^\\*97([0-9]*)$"` for feature codes). A callflow only other steps jump to (a ring group made
  with `--extension none`) carries a pattern no dialled number matches.
- Every number is unique across the account's callflows. E.164 numbers must belong to the
  account (`rinzo numbers list`).
- `children` keys are branch names. `"_"` is the default branch: it runs when the module ends
  without a more specific match (nobody answered, no rule matched, and so on).
- Optional: `owner_id` (user), `type` (SmartPBX labels such as `mainUserCallflow`, `baseGroup`,
  `userGroup`, `main`), `contact_list: {"exclude": false}`.
- Special numbers: `no_match` (anything not matched goes out to carriers: every account has
  `{"numbers":["no_match"],"flow":{"module":"offnet","data":{},"children":{}}}`; leave it alone).

## Modules

### user: ring all of a user's phones
```json
{"module": "user", "data": {"id": "USER_ID", "timeout": 20, "can_call_self": false}}
```
`timeout` default 20 s, `delay` 0, `strategy` `simultaneous`|`single`. Rings every device owned
by the user (desk phones, softphones, cell phone devices), honouring the user's call forwarding
(Rinzo, `kz_directory_cfwd`; verified with live calls): `call_forward` rings the number *instead
of* their phones (`substitute` defaults to true; `"substitute": false` rings both), ring-group calls
included unless `"direct_calls_only": true`; `call_failover` dials its number in place of a phone
that is not registered (in ring groups too; a phone that rings unanswered does not fail over). The
number can be an outside number or an extension of the account. With `require_keypress` (the
default) the person answering must press 1, so a cell's own voicemail cannot take the call.
Children: `"_"` when nobody answers. Verified live.

### device: ring one phone
```json
{"module": "device", "data": {"id": "DEVICE_ID", "timeout": 20}}
```
Note: the device module's schema default `timeout` is 0. Always set it.

### ring_group: ring several users/devices
```json
{"module": "ring_group", "data": {
  "name": "Sales",
  "strategy": "simultaneous",
  "timeout": 20,
  "endpoints": [
    {"endpoint_type": "user", "id": "USER_ID_1", "timeout": 20, "delay": 0},
    {"endpoint_type": "user", "id": "USER_ID_2", "timeout": 20, "delay": 0},
    {"endpoint_type": "device", "id": "DEVICE_ID", "timeout": 20, "delay": 5}
  ]}}
```
`strategy`: `simultaneous` (all at once), `single` (hunt: one after another in list order,
each for its `timeout`), `weighted_random`. `endpoint_type`: `user`|`device`|`group`. On Rinzo a
`user` endpoint rings all of that user's phones at once (verified live). With `simultaneous`, each
endpoint rings from its `delay` for its `timeout` (a backup person: `"delay": 20, "timeout": 15`),
cut off at the group's `timeout`; SmartPBX sets the group's `timeout` to the largest
`delay + timeout`, and `ring-groups create --member ID,delay=20,timeout=15` does the same (verified
live). `single` ignores `delay`. `repeats` (default 1), `ringback`, `ignore_forward` (default
true: a phone answering with a SIP redirect is not followed; members' Rinzo call forwarding still
applies). Children: `"_"` when no one answers.

### voicemail: leave or check a message
```json
{"module": "voicemail", "data": {"id": "VMBOX_ID"}}
```
`action`: `compose` (default: caller leaves a message) or `check` (owner listens).
`max_message_length` default 500 s. Verified live, including voicemail-to-email.

### menu: IVR / auto attendant
```json
{"module": "menu", "data": {"id": "MENU_ID"},
 "children": {
   "1": {"module": "ring_group", "data": {"...": "sales"}, "children": {}},
   "2": {"module": "user", "data": {"id": "USER_ID", "timeout": 20}, "children": {}},
   "0": {"module": "voicemail", "data": {"id": "VMBOX_ID"}, "children": {}},
   "timeout": {"module": "user", "data": {"id": "RECEPTION_USER_ID", "timeout": 20}, "children": {}},
   "_": {"module": "voicemail", "data": {"id": "VMBOX_ID"}, "children": {}}
 }}
```
Child keys: a digit string (`"1"`, `"2"`, …, `"0"`), `"timeout"`, `"max_retries"` and `"_"`. The menu
document (`rinzo menus create`) holds the prompts and behaviour:
```json
{"name": "Main menu", "retries": 2, "timeout": 10000, "max_extension_length": 1, "hunt": false,
 "media": {"greeting": "MEDIA_ID", "invalid_media": true, "exit_media": true, "transfer_media": true}}
```
How a menu behaves (Rinzo's `cf_menu`):
- `retries` is the **total number of times the greeting plays** (default 3). "Play it again once,
  then go to Reception" is `retries: 2`.
- A wrong key plays "invalid" and the greeting again; that uses up a play.
- Silence: a `"timeout"` key runs at the **first** silence (`timeout`, default 10000 ms). Without
  a `"timeout"` key, silence uses up a play like a wrong key.
- When the plays run out, Rinzo plays its exit prompt (`media.exit_media`, on by default), then the
  `"max_retries"` key if there is one; otherwise it announces a transfer and continues to `"_"`, or
  says goodbye and hangs up when there is no `"_"`.
- `max_extension_length` (default 4) is how many digits it collects. For one-key choices without
  dialling extensions, use 1: with 4, every key waits the interdigit timeout before routing.
- `hunt` defaults to **false**. `hunt: true` lets callers dial any extension during the greeting
  (SmartPBX's menus set it); then keep `max_extension_length` at your longest extension.
  `hunt_allow` / `hunt_deny` are regexes the dialled digits must / must not match, e.g.
  `"hunt_allow": "^(?:110[1-7]|21[0-9][0-9])$"` (an auto-attendant spec writes this from
  `"dial_extensions": ["1101-1107", "21XX"]`). Only real extensions are reached, never `no_match`.
- `name` is required. `timeout` and `interdigit_timeout` are in milliseconds. `media.greeting` is a media ID
(upload with `rinzo media create --name … --file greeting.wav`; `rinzo media get ID` shows whether
its audio is stored). Verified live.

### temporal_route: business hours, holidays
```json
{"module": "temporal_route",
 "data": {"timezone": "America/Vancouver", "rules": ["HOLIDAY_RULE_ID", "OPEN_RULE_ID"]},
 "children": {
   "HOLIDAY_RULE_ID": {"module": "voicemail", "data": {"id": "VMBOX_ID"}, "children": {}},
   "OPEN_RULE_ID": {"module": "menu", "data": {"id": "MENU_ID"}, "children": {"...": "..."}},
   "_": {"module": "voicemail", "data": {"id": "VMBOX_ID"}, "children": {}}
 }}
```
- `rules` lists temporal-rule IDs (or rule-set IDs). **They are checked in list order and the
  first match wins**: put holidays before open hours.
- Each child key is a rule ID; `"_"` runs when no rule matches (after hours).
- `timezone`: an IANA name. If absent, the account `timezone` is used. Rules themselves have no
  time zone: `rinzo rules create --open/--close` times are read in this one.
- Time windows include both ends: with 12:00–13:00 lunch, 13:00:00 is still lunch. A holiday rule
  (`--date`) without times covers the whole day.
- A rule-set ID as a child key covers every rule in the set (one branch for all holidays).
- Verified live. `rinzo callflows simulate NUMBER --at "2026-12-25 10:00"` lists every rule with
  whether it applies at that time and which one wins.

### play: play audio
```json
{"module": "play", "data": {"id": "MEDIA_ID"}}
```
`id` is a media ID, a built-in prompt like `system_media/vm-not_available_no_voicemail`, or
`tone_stream://%(1000,0,440)`. `endless_playback` false by default. Verified live. Any key stops
a `play` and moves on to the next step (`terminators`, all keys by default), so a caller can skip
a lunch or holiday greeting.

### tts
Reads text aloud using Rinzo's configured speech provider:
```json
{"module":"tts","data":{"text":"Welcome to our office.","voice":"female","language":"en-us"}}
```
For reusable menu or voicemail greetings, create saved media and use its ID:
```sh
rinzo media create --name "Welcome" --set media_source=tts --set tts.text="Welcome to our office." --set tts.voice=female/en-us
```
Use `male/en-us` for a male saved voice; dynamic `tts` uses `voice: "male"` and `language: "en-us"`.
Saved greetings are generated when created or edited; playback reuses the saved audio.

### callflow: jump to another callflow
```json
{"module": "callflow", "data": {"id": "CALLFLOW_ID"}}
```
Rinzo keeps the jump step's `"_"` child and runs it **after the target callflow finishes**. SmartPBX
ring groups rely on this: a group's dial callflow jumps to its base callflow (the `ring_group`), and
the jump's `"_"` is the overflow when nobody answers. So a menu key that jumps to a ring group's
dial callflow keeps that group's overflow. Don't add a `"_"` when the target already ends the call
(it ends in voicemail, say): it would run after the caller has left a message.

### conference: conference bridge
```json
{"module": "conference", "data": {"id": "CONFERENCE_ID"}}
```
Without `id` callers are asked for a conference number. Conference document:
`{"name": "Team bridge", "member": {"pins": ["4321"], "join_muted": false}, "moderator":
{"pins": ["9876"]}}` (`name` required; pins are strings; `member.join_muted` defaults to **true** in
Rinzo, so set it false for a meeting room). With member pins, callers are asked
for one before they join (verified live). `rinzo conferences create --name … --pin-file PATH`
makes the conference, its extension and a PIN in one step.

### offnet / resources: call an outside number
```json
{"module": "offnet", "data": {"to_did": "+16045551234"}}
```
Without `to_did` it dials the number the caller dialed (that's what `no_match` does). With
`to_did` it always calls that number: a fixed external forward, billed as an outbound call.
If the outside number doesn't answer the call ends: Rinzo never goes on to this step's `"_"`.
(Target notation: `number:+16045551234`.)

### set_cid / prepend_cid: caller ID
```json
{"module": "set_cid", "data": {"caller_id_name": "Support", "caller_id_number": "+16045550123"}}
{"module": "prepend_cid", "data": {"caller_id_name_prefix": "Sales: ", "action": "prepend"}}
```
Outbound caller ID must be one of the account's numbers. `prepend_cid` is handy before a
ring group so staff see which line the call came in on.

### record_call: record a call
```json
{"module": "play", "data": {"id": "/system_media/rinzo-recording-notice-en", "terminators": []},
 "children": {"_": {"module": "record_call",
                    "data": {"action": "start", "format": "mp3", "time_limit": 3600, "label": "rinzo", "should_follow_transfer": true},
                    "children": {"_": …the rest of the flow…}}}}
```
`action` required (`start`|`stop`). Without a `url`, Rinzo keeps the finished file for 90 days:
`rinzo recordings list|show|get ID --out FILE|delete ID --yes` (the Control panel plays it too).
With a `url`, Rinzo uploads the file there and keeps no copy. Kazoo plays no notice of its own;
the `play` node ahead of `record_call` is Rinzo's notice prompt (`rinzo-recording-notice-en|es|fr`).
Don't hand-write these nodes: `rinzo recording enable --number N [--no-notice] [--language es]`
adds exactly this pair to the top of the number's callflow (and `disable` removes exactly it);
`rinzo recording status` shows which numbers record. To record everything one person handles,
`rinzo recording enable --user ID` sets the user's `call_recording` (no notice: tell the business).

### Others
- `response`: `{"code": 486, "message": "Busy"}` hangs up with a SIP code (`code` required).
- `hangup`, `answer`, `sleep` (`{"duration": 2, "unit": "s"}`).
- `transfer`: `{"target": "1001", "transfer_type": "blind"}`.
- `page_group` (*unverified*): `{"audio": "one-way", "endpoints": [{"endpoint_type": "device", "id": "…"}]}`.
- `park` (*unverified*): `{"action": "park"}`. SmartPBX's feature codes `*3`, `*4`, `*5` use it.
- `webhook` (*unverified*): `{"uri": "https://…", "http_verb": "post"}` fires an HTTP request
  mid-call without changing routing.
- `pivot` (HTTP call control, `voice_url`) is **not verified** on Rinzo. Don't build on it
  without testing.
- `faxbox`, `receive_fax` (*unverified*).

## Time-of-day rules (`rinzo rules`)

```json
{"name": "Open hours", "cycle": "weekly", "interval": 1,
 "wdays": ["monday", "tuesday", "wednesday", "thursday", "friday"],
 "time_window_start": 32400, "time_window_stop": 61200}
```
- Required: `name`, `cycle` (`date`|`daily`|`weekly`|`monthly`|`yearly`).
- `time_window_start`/`stop`: seconds after local midnight (9:00 = 32400, 17:00 = 61200,
  12:00 = 43200, 13:00 = 46800).
- `wdays`: `monday` … `sunday`.
- Yearly holiday: `{"name": "Christmas", "cycle": "yearly", "interval": 1, "month": 12, "days": [25]}`.
- Floating holiday: `{"name": "Labour Day", "cycle": "yearly", "interval": 1, "month": 9,
  "ordinal": "first", "wdays": ["monday"]}`. `ordinal`: `every`, `first`, …, `fifth`, `last`.
- **Don't set `enabled`** on a rule: `true` forces the rule always on, `false` always off. Leave it
  out for normal time-based behaviour. (That's how "closed today" overrides work.)
- Different hours on different days need one rule per distinct window (for example Mon–Fri 9–17
  and Sat 10–14), both pointing at the same child.

Rule sets (`rinzo rule-sets`): `{"name": "Holidays", "temporal_rules": ["RULE_ID", …]}`. Put the
set ID in a `temporal_route`'s `rules` and use it as the child key, so one branch covers every
holiday.

## SmartPBX-created structures

If the account was set up in SmartPBX, you'll see callflows like `MainCallflow` (`type: "main"`,
module `temporal_route`, number placeholder `undefinedMainNumber` until a main number is
added) and helpers `MainOpenHours`, `MainAfterHours`, `MainLunchHours`, `MainHolidays`, plus
menus `MainOpenHoursMenu` etc. SmartPBX's Call routing page edits these. Either use SmartPBX's
page for the main number, or build your own callflow and leave SmartPBX's in place unused. Don't
rewrite their internals, because SmartPBX expects that exact shape.

Each user's extension is a callflow with `type: "mainUserCallflow"` and `owner_id`; numbers
added to it ring that user. `rinzo numbers assign N --user USER_ID` does exactly that.
