NewDash Dots are hereMini apps in Dash, with payments built inLearn more

Products · Developer guides

Flow Definition

The JSON format of a USSD flow: pages, component types, limits, page options and what happens when a page is done.

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." }
    }
  ]
}
FieldTypeRequiredDescription
versionstringYesFlow format version. Use "1.0".
pagesarrayYesThe 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" }
}
FieldTypeRequiredDescription
idstringYesThe page's ID. See the rules below.
componentobjectYesWhat the page shows and how the subscriber answers.
optionsobjectNoSkip the page, or end the flow after it. See Page Options.
on_doneobjectNoWhat happens when the subscriber finishes the page. See Page Completion.

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.

Component Types

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

TypeWhat it doesSubscriber input
displayShows a message and ends the flowNone
promptAsks for text, a number, or yes or noRequired
menuShows numbered optionsRequired
dynamicAsks your server what to show nextDepends on your server
templateRuns a built-in operation, such as taking a paymentDepends on the template
side_effectShows nothing; only runs its on_done and moves onNone

Limits

WhatLimitOver the limit
Page label, as the subscriber sees it (display, prompt, menu, template)130 characters, after every $(...) is filled inCut off at 130
Menu item label, as the subscriber sees it64 charactersCut off at 64
Label as written in the JSON, including $(...)1,000 charactersFails validation
Items in a fixed menu list1 to 16Fails validation
Pages in a flowAt least 1Fails validation
Context key in a mutation1 to 33 characters: letters, digits and underscores, not starting with a digitFails validation
Text written as-is in a mutation or dataExport100 charactersFails 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.

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."
  }
}
FieldTypeDescription
skipboolean 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.
terminalboolean or $(...)When true, the flow ends after this page.
terminal_messagestringThe 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.

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 } }
  }
}
FieldTypeDescription
action"next" or "exchange""next" (the default) moves on in the flow. "exchange" calls your server, which picks the next page.
next_pagestringOnly 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.
urlstringRequired with "exchange". Your endpoint, as a full fixed URL. $(...) is not filled in here.
dataExportobjectOnly with "exchange". Extra values to send to your endpoint. See Sending Extra Values.
session_context.mutationsobjectValues to save for the rest of this session, read later as $(sc.KEY).
recurring_context.mutationsobjectValues 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.

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. 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.

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 page, either directly or through a context key saved straight from that page. See 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.

  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. For naming and flow shapes, see Authoring Conventions.

Keep reading