# Flow Definition (/docs/products/ussd-flows/developers/flow-definition)

A flow is a JSON document with a list of pages. The subscriber moves through the pages in order unless a page sends them somewhere else.

## Top-Level Structure

```json
{
  "version": "1.0",
  "pages": [
    {
      "id": "welcome",
      "component": { "type": "display", "label": "Welcome to Kenema Water." }
    }
  ]
}
```

| Field     | Type   | Required | Description                        |
| --------- | ------ | -------- | ---------------------------------- |
| `version` | string | Yes      | Flow format version. Use `"1.0"`.  |
| `pages`   | array  | Yes      | The pages, in order. At least one. |

Any other top-level field fails validation.

## Pages

```json
{
  "id": "enter_amount",
  "component": { "type": "prompt", "data_type": "integer", "label": "Enter amount in SLE" },
  "options": { "skip": false },
  "on_done": { "action": "next" }
}
```

| Field       | Type   | Required | Description                                                                                          |
| ----------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `id`        | string | Yes      | The page's ID. See the rules below.                                                                  |
| `component` | object | Yes      | What the page shows and how the subscriber answers.                                                  |
| `options`   | object | No       | Skip the page, or end the flow after it. See [Page Options](#page-options).                          |
| `on_done`   | object | No       | What happens when the subscriber finishes the page. See [Page Completion](#page-completion-on_done). |

### Page ID Rules

- Start with a lowercase letter.
- Use only lowercase letters, digits and underscores, for example `enter_amount`.
- Make each ID unique within the flow.

Not every rule is checked when you save. An ID that breaks them can't be used in `on_done.next_page`, and an ID with a hyphen can't be read with `$(page.ID.value)`. For naming style, see [Authoring Conventions](/docs/products/ussd-flows/developers/conventions#page-ids).

## Component Types

`component.type` sets the kind of page. There are six:

| Type                                                                         | What it does                                        | Subscriber input        |
| ---------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------- |
| [`display`](/docs/products/ussd-flows/developers/page-types/display)         | Shows a message and ends the flow                   | None                    |
| [`prompt`](/docs/products/ussd-flows/developers/page-types/prompt)           | Asks for text, a number, or yes or no               | Required                |
| [`menu`](/docs/products/ussd-flows/developers/page-types/menu)               | Shows numbered options                              | Required                |
| [`dynamic`](/docs/products/ussd-flows/developers/page-types/dynamic)         | Asks your server what to show next                  | Depends on your server  |
| [`template`](/docs/products/ussd-flows/developers/page-types/template)       | Runs a built-in operation, such as taking a payment | Depends on the template |
| [`side_effect`](/docs/products/ussd-flows/developers/page-types/side-effect) | Shows nothing; only runs its `on_done` and moves on | None                    |

## Limits

| What                                                                    | Limit                                                                          | Over the limit   |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------- |
| Page label, as the subscriber sees it (display, prompt, menu, template) | 130 characters, after every `$(...)` is filled in                              | Cut off at 130   |
| Menu item label, as the subscriber sees it                              | 64 characters                                                                  | Cut off at 64    |
| Label as written in the JSON, including `$(...)`                        | 1,000 characters                                                               | Fails validation |
| Items in a fixed menu list                                              | 1 to 16                                                                        | Fails validation |
| Pages in a flow                                                         | At least 1                                                                     | Fails validation |
| Context key in a mutation                                               | 1 to 33 characters: letters, digits and underscores, not starting with a digit | Fails validation |
| Text written as-is in a mutation or `dataExport`                        | 100 characters                                                                 | Fails validation |

Labels and menu items are cut off, not rejected. Check the longest version of each label, with real names and amounts filled in. For the limits on stored values, such as the 16-key limit on recurring context, see [Context Stores](/docs/products/ussd-flows/developers/context-stores).

## Page Options

`options` decides whether a page runs and whether the flow ends after it.

```json
{
  "options": {
    "skip": "$(rc.has_signed_up)",
    "terminal": false,
    "terminal_message": "Thanks. See you next week."
  }
}
```

| Field              | Type                | Description                                                                                                                                       |
| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skip`             | boolean or `$(...)` | When `true`, the page is passed over. Its `on_done` does not run, and the flow moves to the next page in the list.                                |
| `terminal`         | boolean or `$(...)` | When `true`, the flow ends after this page.                                                                                                       |
| `terminal_message` | string              | The last message the subscriber sees when `terminal` ends the flow. Can contain `$(...)`. If empty, the subscriber sees "Flow session cancelled." |

A `$(...)` in `skip` or `terminal` must give exactly `true` to take effect. Other text, such as a name, counts as false. To test whether a value is set, use a function: `"$(!strings.is_blank(rc.display_name))"`. See [Expressions](/docs/products/ussd-flows/developers/expressions).

### Skip

```json
{
  "id": "collect_display_name",
  "component": { "type": "prompt", "data_type": "text", "label": "Enter your name" },
  "options": { "skip": "$(!strings.is_blank(rc.display_name))" }
}
```

A skipped page always moves to the next page in the list, even if its `on_done` has a `next_page`. If a skipped page is the last page, the flow stops with an error.

### Terminal

```json
{
  "id": "confirm_signup",
  "component": { "type": "prompt", "data_type": "boolean", "label": "Join Bo Fresh Rewards for free?" },
  "options": {
    "terminal": "$(page.confirm_signup.value == 'true')",
    "terminal_message": "You are in. Dial again to see your rewards."
  },
  "on_done": {
    "action": "next",
    "recurring_context": { "mutations": { "has_signed_up": "$(page.confirm_signup.value)" } }
  }
}
```

`terminal` is checked after `on_done` runs, so the page's mutations are saved before the flow ends. `terminal` has no effect on a display page, because a display page always ends the flow.

## Page Completion (on\_done)

`on_done` runs when the subscriber finishes a page. Without it, the flow moves to the next page in the list.

```json
{
  "on_done": {
    "action": "next",
    "next_page": "confirm_payment",
    "session_context": { "mutations": { "chosen_term": "$(page.select_term.label)" } },
    "recurring_context": { "mutations": { "has_paid_before": true } }
  }
}
```

| Field                         | Type                     | Description                                                                                                                  |
| ----------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `action`                      | `"next"` or `"exchange"` | `"next"` (the default) moves on in the flow. `"exchange"` calls your server, which picks the next page.                      |
| `next_page`                   | string                   | Only with `"next"`. The page to go to instead of the next one in the list: a page ID, or a `$(...)` that gives a page ID.    |
| `url`                         | string                   | Required with `"exchange"`. Your endpoint, as a full fixed URL. `$(...)` is not filled in here.                              |
| `dataExport`                  | object                   | Only with `"exchange"`. Extra values to send to your endpoint. See [Sending Extra Values](#sending-extra-values-dataexport). |
| `session_context.mutations`   | object                   | Values to save for the rest of this session, read later as `$(sc.KEY)`.                                                      |
| `recurring_context.mutations` | object                   | Values to save for this subscriber across sessions, read later as `$(rc.KEY)`.                                               |

### Mutations Run Before Navigation

When a page is done, Monime saves its `session_context` mutations, then its `recurring_context` mutations, and only then moves to the next page or calls your server. The next page and your server can read the new values.

A display page's mutations run when the page is shown. The rules for each store are in [Context Stores](/docs/products/ussd-flows/developers/context-stores).

### next

Moves to the next page in the list. This is what happens when `on_done` is left out.

### next with next\_page

Jumps to another page without calling your server:

```json
{
  "id": "enter_amount",
  "component": { "type": "prompt", "data_type": "integer", "label": "Enter amount in SLE" },
  "on_done": { "action": "next", "next_page": "confirm_payment" }
}
```

`next_page` can also be a `$(...)` that gives a page ID. Use it to branch on an answer:

```json
{
  "on_done": {
    "action": "next",
    "next_page": "$(page.start_menu.value == 'exit' ? 'say_goodbye' : 'enter_account')"
  }
}
```

A written page ID must follow the [page ID rules](#page-id-rules). An empty `next_page` fails validation. If the ID doesn't match any page, the flow stops with an error when it gets there.

### exchange

Calls your server with the session details and every answer so far. Your server replies with the next page to show (and optional data for it), or with a message that ends the flow. See the [Exchange API](/docs/products/ussd-flows/developers/exchange-api/overview).

```json
{
  "id": "enter_meter_number",
  "component": { "type": "prompt", "data_type": "text", "label": "Enter your meter number" },
  "on_done": {
    "action": "exchange",
    "url": "https://api.example.com/ussd/meter"
  }
}
```

`next_page` can't be used with `"exchange"`.

### Sending Extra Values (dataExport)

Every exchange request already carries the answers from earlier pages in `flowData`. Use `dataExport` when your server also needs a stored value or a computed one. Monime sends it as `exportedData` in the request.

```json
{
  "on_done": {
    "action": "exchange",
    "url": "https://api.example.com/ussd/meter",
    "dataExport": {
      "meter_number": "$(sc.meter_number)",
      "district": "Bo",
      "is_new_customer": "$(strings.is_blank(rc.last_meter))",
      "retry_count": 1
    }
  }
}
```

- Keys are your own names.
- Each value is `null`, `true` or `false`, a number, text of up to 100 characters, or a `$(...)`.
- Objects and lists are not allowed.
- A `$(...)` value is always sent as text. Above, `is_new_customer` arrives as `"true"` or `"false"`.

If a value is the subscriber's own phone number, your server receives it masked. The exception is a number that came from a [`get_subscriber_msisdn`](/docs/products/ussd-flows/developers/templates/get-subscriber-msisdn) page, either directly or through a context key saved straight from that page. See [Subscriber Identification](/docs/products/ussd-flows/developers/exchange-api/subscriber-identification).

## Complete Example

A rewards flow that remembers returning subscribers. New subscribers give their name and pay a joining fee. Members choose a bundle.

```json
{
  "version": "1.0",
  "pages": [
    {
      "id": "collect_display_name",
      "component": {
        "type": "prompt",
        "data_type": "text",
        "label": "Welcome to Bo Fresh Rewards.\nEnter your name",
        "min_length": 2,
        "max_length": 40,
        "max_invalid_input_attempts": 3
      },
      "options": {
        "skip": "$(!strings.is_blank(rc.display_name))"
      },
      "on_done": {
        "action": "next",
        "recurring_context": {
          "mutations": { "display_name": "$(strings.title(page.collect_display_name.value))" }
        }
      }
    },
    {
      "id": "choose_bundle",
      "component": {
        "type": "menu",
        "label": "Hi $(strings.split_index(rc.display_name, ' ', 0)), pick an option",
        "items": [
          { "label": "Join (SLE 100)", "value": "100", "hide": "$(rc.is_member == 'true')" },
          { "label": "Small bundle (SLE 20)", "value": "20", "hide": "$(rc.is_member != 'true')" },
          { "label": "Large bundle (SLE 50)", "value": "50", "hide": "$(rc.is_member != 'true')" }
        ]
      }
    },
    {
      "id": "charge_bundle",
      "component": {
        "type": "template",
        "template_name": "collect_payment",
        "label": "Enter your $(session.network_provider_name) PIN to pay for $(page.choose_bundle.label).",
        "template_data": {
          "amount": "$(page.choose_bundle.value)",
          "payment_target_ref": "YOUR_PAYMENT_CODE",
          "receipt_name": "Bo Fresh Rewards",
          "reference": "$(rc.display_name)"
        }
      },
      "options": {
        "terminal": "$(page.choose_bundle.value == '100')",
        "terminal_message": "Welcome to Bo Fresh Rewards, $(strings.split_index(rc.display_name, ' ', 0))."
      },
      "on_done": {
        "action": "next",
        "recurring_context": {
          "mutations": { "is_member": "$(page.choose_bundle.value == '100' || rc.is_member == 'true')" }
        }
      }
    },
    {
      "id": "bundle_done",
      "component": {
        "type": "display",
        "label": "Thanks $(strings.split_index(rc.display_name, ' ', 0)). Your $(page.choose_bundle.label) is active."
      }
    }
  ]
}
```

Replace `YOUR_PAYMENT_CODE` with a Payment Code or UVAN from your Monime Space. The payment page itself is covered in [collect\_payment](/docs/products/ussd-flows/developers/templates/collect-payment).

1. **collect\_display\_name** asks for a name, unless one is already saved. It saves the name in title case as `rc.display_name`.
2. **choose\_bundle** shows only "Join" to new subscribers, and only the bundles to members.
3. **charge\_bundle** takes the payment and saves `rc.is_member`. A joining payment ends the flow with a welcome message.
4. **bundle\_done** confirms a bundle and ends the flow.

On the next dial, a member skips the name prompt and sees only the bundles.

For more complete flows, see [Example Flows](/docs/products/ussd-flows/developers/examples). For naming and flow shapes, see [Authoring Conventions](/docs/products/ussd-flows/developers/conventions).
