# Authoring Conventions (/docs/products/ussd-flows/developers/conventions)

None of these conventions are enforced, but they make a flow easier for subscribers to use and easier for you to read, review and debug. The rules that are enforced are in [Flow Definition](/docs/products/ussd-flows/developers/flow-definition).

## Page IDs

Page IDs show up in `$(page.ID.value)`, in `next_page` and in your exchange requests, so make them say what the page does.

Use snake\_case and start with a verb or a role:

| Prefix                                    | Use for                                    | Usual page type                           |
| ----------------------------------------- | ------------------------------------------ | ----------------------------------------- |
| `start`                                   | The first page                             | `dynamic` or `menu`                       |
| `welcome_*`                               | A greeting that asks a first question      | `prompt`                                  |
| `enter_*`, `collect_*`                    | Asking for input                           | `prompt`                                  |
| `select_*`, `choose_*`                    | Picking from a list                        | `menu`                                    |
| `confirm_*`                               | A yes or no check before something happens | `prompt` with `data_type: "boolean"`      |
| `pay_*`, `charge_*`                       | Taking a payment                           | `template` (`collect_payment`)            |
| `lookup_*`, `validate_*`                  | Checking something on your server          | `dynamic`, or a `prompt` with an exchange |
| `prep_*`, `save_*`                        | Saving values without showing anything     | `side_effect`                             |
| `*_done`, `thank_you`                     | A successful end                           | `display`                                 |
| `*_not_found`, `*_failed`, `system_error` | An error end                               | `display`                                 |

Make IDs specific: `select_service_type` beats `select`, and `account_not_found` beats `error`. Avoid numbered IDs such as `page1` or `step_3`.

## Flow Shapes

Most flows follow one of three shapes.

### Collect, pay, thank

No server calls. Good for event tickets, donations and simple top-ups.

```text
welcome_get_name   (prompt)
select_ticket      (menu)
pay_ticket         (template: collect_payment)
thank_you          (display)
```

### Look up, choose, pay

Your server checks the subscriber or their account first. Good for bills, school fees and meter top-ups.

```text
start              (dynamic: your server decides what to show)
select_service     (menu, items from your server)
enter_reference    (prompt + exchange)
confirm_details    (boolean prompt)
pay_bill           (template: collect_payment)
payment_done       (display)
account_not_found  (display, chosen by your server)
system_error       (display, chosen by your server)
```

### Remember returning subscribers

Pages skip themselves when a saved value is already there. Good for memberships, weekly giving and loyalty schemes. See [Context Stores](/docs/products/ussd-flows/developers/context-stores).

```text
collect_display_name  (prompt, skipped if rc.display_name is set)
choose_option         (menu, items shown or hidden by rc values)
pay_option            (template: collect_payment)
option_done           (display)
```

## Labels

- Put a short greeting or context on the first line and the question on the next, separated by `\n`: `"Welcome to Kenema Water.\nEnter your meter number"`.
- Keep every label under 130 characters with real values filled in, and menu items under 64. Longer text is cut off. See [Limits](/docs/products/ussd-flows/developers/flow-definition#limits).
- Name the amount and currency: `SLE $(page.enter_amount.value)`.
- Use the subscriber's first name when you have it: `$(strings.split_index(rc.display_name, ' ', 0))`.
- For payment pages, follow the PIN label pattern in [collect\_payment](/docs/products/ussd-flows/developers/templates/collect-payment).

## Menus

- Use 2 to 6 items for a decision. Use more, up to 16, only for long lists such as packages.
- Put the most common choice first and Exit last.
- Write items as `Name (SLE amount)` or `Name - detail`: `"Standard (SLE 200)"`.
- Use a fixed list when the items never change, and `"items": "$(data.KEY)"` when your server supplies them.
- Hide items with `hide` rather than writing two flows: `"hide": "$(rc.tier != 'gold')"`.

## Confirm Before Paying

Before taking money, ask a yes or no question. Use `exit_message` to end the flow cleanly when the subscriber says no:

```json
{
  "id": "confirm_payment",
  "component": {
    "type": "prompt",
    "data_type": "boolean",
    "label": "Pay SLE $(page.enter_amount.value) to Kenema Water?",
    "exit_message": "Payment cancelled. Nothing was charged."
  }
}
```

The subscriber answers 1 or 2. On yes, the page's value is `true` and the flow carries on. See [prompt](/docs/products/ussd-flows/developers/page-types/prompt).

## Exit Options

Give every long menu an Exit item, and send it to a short goodbye page with `next_page`:

```json
{
  "id": "start_menu",
  "component": {
    "type": "menu",
    "label": "Welcome to Kenema Water.",
    "items": [
      { "label": "Pay bill", "value": "pay" },
      { "label": "Buy tokens", "value": "tokens" },
      { "label": "Exit", "value": "exit" }
    ]
  },
  "on_done": {
    "action": "next",
    "next_page": "$(page.start_menu.value == 'exit' ? 'say_goodbye' : 'enter_account')"
  }
}
```

`say_goodbye` is a display page, so the flow ends there.

## Errors

- Give each failure its own display page with a specific ID and a message that says what went wrong and what to do: "Meter number not found. Check the number on your meter and try again."
- Keep one `system_error` page for anything unexpected: "Sorry, we can't complete this now. Please try again later."
- Let your server choose the error page: when a lookup fails, the exchange response navigates to `account_not_found`. The flow only declares the pages. See the [Exchange API response](/docs/products/ussd-flows/developers/exchange-api/response).
- Make exchange handlers safe to run twice: handling the same request again should not record or charge anything twice.

More on failures is in [Error Handling](/docs/products/ussd-flows/developers/error-handling).

## Context Keys

- Use snake\_case, like page IDs.
- Name the value, not how you got it: `rc.has_signed_up`, not `rc.result_of_signup_check`.
- Use `sc` by default. Use `rc` only for what must last until the next visit.
- Give `rc` and `sc` values a default when you use them in an expression: `$(rc.usual_amount ?? 50)`.
- Test for a saved name or text with `strings.is_blank(rc.KEY)`, not with a bare `rc.KEY`.

## Flow Length

Keep flows short, ideally under 10 pages. Every extra screen is a chance for the session to time out or the subscriber to give up. If a journey is long, save progress in `rc` so a returning subscriber can skip what they already did.

## Exchange Endpoints

- Serve them over HTTPS.
- Reply quickly, and show a clear error page when something fails.
- One endpoint can serve every page. The request says which page it came from, so it's fine to repeat the same `url`.

## Checklist Before Publishing

- Page IDs are snake\_case and start with a verb or role.
- No page is called `error`, and each failure has its own message.
- Every label fits in 130 characters, and every menu item in 64, with real values filled in.
- Every payment page follows the [collect\_payment](/docs/products/ussd-flows/developers/templates/collect-payment) label and amount rules.
- Every `rc` and `sc` value used in an expression has a `??` default.
- Long menus have an Exit item.
- Prompts have sensible length or value limits and a clear `pattern_mismatch_message`.
- You walked through every path in the Simulator. See [Editing and Publishing](/docs/products/ussd-flows/editing-and-publishing).
