Quick start
Welcome to the Dynamic Flow documentation! This quick start guide will introduce you to the most important concepts you need to understand to get started building complete product screens and flows with Dynamic Flow.
Note: Although this guide is web based, all of the examples will render the same way on the iOS and Android clients. The cross-platform nature of Dynamic Flow is one of its most powerful features, but it is difficult to show off here!
How to display information to the user
A Dynamic Flow needs two things to get started: a server to send a step definition, and a client to render it. The language used to communicate between the server and the client is JSON. The JSON is structured in a particular way, according to the Dynamic Flow specification.
A step being sent from the backend looks like this:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [],
"layout": [
{
"type": "alert",
"context": "warning",
"markdown": "Please note that exchange rates are unstable right now."
}
]
}This is a full Dynamic Flow step. title is a special field that is used to display a title for the step. The layout array is where you can specify the components you want to render on the screen.
So far this isn't a very useful screen: there's no way to move forward or back. Let's add a button to let our user continue.
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [],
"layout": [
{
"type": "alert",
"context": "warning",
"markdown": "Please note that exchange rates are unstable right now."
},
{
"type": "button",
"title": "Continue",
"control": "primary",
"behavior": {
"type": "action",
"action": {
"url": "/todo"
}
}
}
]
}Great! Now, when the user clicks the button, the specified behaviour will be performed - in this case, making a request to the /todo endpoint. For now, clicking the button will show an error - that's because the backend isn't set up to do anything in response.
The alert is just one of many components you can use to present information to the user. For example, you might want to add some additional information using a paragraph:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [],
"layout": [
{
"type": "alert",
"context": "warning",
"markdown": "Please note that exchange rates are unstable right now."
},
{
"type": "paragraph",
"text": "When exchange rates are volatile, we can only fix rates for a short time. Please keep this in mind when setting up your transfer."
},
{
"type": "button",
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/todo"
},
"type": "action"
}
}
]
}You can find a full list of content display components on the features page.
How to collect data from the user
So far our step only shows information to the user, but you'll often also want to collect data from them as well. Let's imagine you need to get confirmation from the user that they've read the warning about unstable exchange rates.
Your first thought may be that you need to add a checkbox component to the layout. But when it comes to data being submitted from the client, you need to take a slightly different approach:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [
{
"type": "boolean",
"title": "I understand that cancelling this transfer may incur a higher rate"
}
],
"layout": [
{
"type": "alert",
"context": "warning",
"markdown": "Please note that exchange rates are unstable right now."
},
{
"type": "paragraph",
"text": "When exchange rates are volatile, we can only fix rates for a short time. Please keep this in mind when setting up your transfer."
},
{
"type": "button",
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/todo"
},
"type": "action"
}
}
]
}You can see that a boolean schema has been added to the schemas array. This is where you need to define the shape of the data you want to collect from the user.
But where is the field? Well, you haven't told Dynamic Flow where you want this schema to appear in your layout yet.
You can do this by adding a form to your layout, which references your schema by id:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [
{
"type": "boolean",
"$id": "accept-terms",
"title": "I understand that cancelling this transfer may incur a higher rate"
}
],
"layout": [
{
"markdown": "Please note that exchange rates are unstable right now.",
"context": "warning",
"type": "alert"
},
{
"type": "paragraph",
"text": "When exchange rates are volatile, we can only fix rates for a short time. Please keep this in mind when setting up your transfer."
},
{
"type": "form",
"schemaId": "accept-terms"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/todo"
},
"type": "action"
},
"type": "button"
}
]
}When performing any POSTing action behavior, such as the one defined on the Continue button (POST is the default method for actions), Dynamic Flow will include the data collected from the user in the request body. The shape of the data will match what you have defined inside the schemas array.
It is pretty rare that you'll want to post raw data in the manner we have defined above. More often, you'll want to post an object with some properties. Let's update the schema:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [
{
"type": "object",
"$id": "accept-terms",
"properties": {
"terms": {
"type": "boolean",
"title": "I understand that cancelling this transfer may incur a higher rate"
}
},
"displayOrder": [
"terms"
]
}
],
"layout": [
{
"markdown": "Please note that exchange rates are unstable right now.",
"context": "warning",
"type": "alert"
},
{
"type": "paragraph",
"text": "When exchange rates are volatile, we can only fix rates for a short time. Please keep this in mind when setting up your transfer."
},
{
"type": "form",
"schemaId": "accept-terms"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/todo"
},
"type": "action"
},
"type": "button"
}
]
}The boolean schema is now wrapped in an object schema, and the $id referenced in the layout has been moved to the root schema. The submission will now change from "true" or "false" to {"terms": "true"} or {"terms": "false"}.
If you're eagle-eyed, you'll also notice one small difference from when we had the boolean schema at the root: it's no longer required. The checkbox was implicitly required in the previous example because all root schemas are required by default. You will learn more about validation below.
How to piece steps together into a flow
Often, you will want a user experience which is split across multiple screens. This is called a flow. To link steps together, you simply need to tell Dynamic Flow about the trigger (when should it request the next step), and where to look for it (what endpoint to make a request to).
If you had tried clicking the Continue button on any of the examples above, you will have seen an error message, as the backend had not been set up to handle the /todo endpoint. In the next example, a handler has been added for the /send-money endpoint.
When the server receives a POST to this endpoint (the default behaviour for an action), it will respond with a 200, passing back the below step:
{
"id": "success",
"title": "Your money is on its way",
"schemas": [],
"layout": [
{
"type": "alert",
"context": "success",
"markdown": "Your transfer has been set up!"
}
]
}In the example below, the step has been updated to post to the correct endpoint. Try clicking the Continue button and see what happens:
{
"id": "exchange-warning",
"title": "Before you continue",
"schemas": [
{
"type": "object",
"$id": "accept-terms",
"properties": {
"terms": {
"type": "boolean",
"title": "I understand that cancelling this transfer may incur a higher rate"
}
},
"displayOrder": [
"terms"
]
}
],
"layout": [
{
"markdown": "Please note that exchange rates are unstable right now.",
"context": "warning",
"type": "alert"
},
{
"type": "paragraph",
"text": "When exchange rates are volatile, we can only fix rates for a short time. Please keep this in mind when setting up your transfer."
},
{
"type": "form",
"schemaId": "accept-terms"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/send-money"
},
"type": "action"
},
"type": "button"
}
]
}This is a simple example that demonstrates a powerful concept. Based on your user's responses, you can decide to return different variations of the same step, or a completely different step altogether. With this pattern, Dynamic Flow allows you to build anything from a single screen to a complex flow with many steps and variations.
How to validate data
Client-side validation
Dynamic Flow has some built in validation rules that it can perform. These are executed on the client (i.e. without a request to the server), and will prevent actions from being performed if they fail. Perhaps to most common of these is required, which ensures a field is valued.
The following example uses required to ensure the user provides us with a name and email address. As mentioned earlier, root schemas are always required. To declare a property on an object schema as required, we simply add it to the required array. Try submitting the form without a name or email address to see what happens:
{
"id": "recipient",
"title": "Who are you sending to?",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"name": {
"type": "string",
"title": "Full name",
"autocompleteHint": [
"name"
]
},
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
]
}
},
"required": [
"name",
"email"
],
"displayOrder": [
"name",
"email"
]
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient"
},
"type": "action"
},
"type": "button"
}
]
}It's a good start, but this can be improved. There's not much more to validate about a name, but a pattern can be used to better validate the email address. Try submitting the form with an invalid email address to see what happens:
{
"id": "recipient",
"title": "Who are you sending to?",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"name": {
"type": "string",
"title": "Full name",
"autocompleteHint": [
"name"
]
},
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
],
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}
},
"required": [
"name",
"email"
],
"displayOrder": [
"name",
"email"
]
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient"
},
"type": "action"
},
"type": "button"
}
]
}Now the validation will catch malformed email addresses. But the message could be more helpful. Let's use the validationMessages property to provide a more helpful message. Try again:
{
"id": "recipient",
"title": "Who are you sending to?",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"name": {
"type": "string",
"title": "Full name",
"autocompleteHint": [
"name"
]
},
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
],
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"validationMessages": {
"pattern": "Please enter a valid email address"
}
}
},
"required": [
"name",
"email"
],
"displayOrder": [
"name",
"email"
]
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient"
},
"type": "action"
},
"type": "button"
}
]
}Different schemas support different validation rules. Check out the schema documentation for all the possibilities.
Server-side validation
Sometimes you will want to perform more sophisticated validation that requires server-side logic. Dynamic Flow supports performing validation on the server and returning a validation error to the client.
This is done by returning a 422 status code and a validation object in the response body. Suppose the requirement is to validate email address uniqueness. The server has been configured to return a validation error when a request is made to /recipient-validation. The response will look as follows:
{
"validation": {
"email": "This email address is already in use."
}
}Try submitting the form with a valid email address:
{
"id": "recipient",
"title": "Enter your email address",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
],
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"validationMessages": {
"pattern": "Please enter a valid email address"
}
}
},
"required": [
"email"
],
"displayOrder": [
"email"
]
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient-validation"
},
"type": "action"
},
"type": "button"
}
]
}How to pass data between steps
Dynamic Flow clients are stateless: each step is independent of the others and has no knowledge of the data that was collected in previous steps. This means that every step must be provided with all the data it needs to function.
So far, this guide hasn't talked about how to prepopulate a step with data. Let's return to the name and email example. Imagine that the name is already known, and only the email address needs to be collected. You can prepopulate the name by adding a model property to the step:
{
"id": "recipient",
"title": "Who are you sending to?",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"name": {
"type": "string",
"title": "Full name",
"autocompleteHint": [
"name"
]
},
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
],
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"validationMessages": {
"pattern": "Please enter a valid email address"
}
}
},
"required": [
"name",
"email"
],
"displayOrder": [
"name",
"email"
]
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient"
},
"type": "action"
},
"type": "button"
}
],
"model": {
"name": "Ada Lovelace"
}
}Now imagine that you want to split this step into two. It's a common pattern to build up a model across several steps, building on the data collected in previous steps.
Because Dynamic Flow is stateless, you need to pass the data collected in the first step to the second step. You can do this by adding a const schema to our schemas array, containing the data we want included. A const schema like this has no visual representation, but is included in the form submission.
{
"id": "recipient",
"title": "Who are you sending to?",
"schemas": [
{
"type": "object",
"$id": "recipient",
"properties": {
"email": {
"type": "string",
"title": "Email address",
"autocompleteHint": [
"email"
],
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"validationMessages": {
"pattern": "Please enter a valid email address"
}
}
},
"required": [
"email"
],
"displayOrder": [
"email"
]
},
{
"const": {
"name": "Ada Lovelace"
}
}
],
"layout": [
{
"type": "form",
"schemaId": "recipient"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/recipient"
},
"type": "action"
},
"type": "button"
}
]
}Now if you submit, you'll see that the submitted data includes both the form data and the const schema data.
How to choose the best component for the job
Dynamic Flow often has multiple ways of rendering the same component. For example, the oneOf schema lets the user choose one of a number of different options - but you can render this in several ways: a dropdown, a radio group or tabs. Dropdown is the default experience:
{
"id": "currency",
"title": "Select Currency",
"schemas": [
{
"$id": "#schema",
"type": "object",
"displayOrder": [
"currency"
],
"required": [
"currency"
],
"properties": {
"currency": {
"title": "Currency",
"oneOf": [
{
"title": "EUR",
"media": {
"content": [
{
"uri": "urn:wise:currencies:eur:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "Euro",
"const": "EUR"
},
{
"title": "GBP",
"media": {
"content": [
{
"uri": "urn:wise:currencies:gbp:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "British pound",
"const": "GBP"
},
{
"title": "USD",
"media": {
"content": [
{
"uri": "urn:wise:currencies:usd:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "United States dollar",
"const": "USD"
}
]
}
}
}
],
"layout": [
{
"type": "form",
"schemaId": "#schema"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/continue"
},
"type": "action"
},
"type": "button"
}
]
}Semantically, all of these are the same thing: the user is choosing from a number of different options, one of which will be submitted when you perform an action on the step. Dynamic Flow won't necessarily know which is the most appropriate component for your step, so you need to decide which you want.
When we have multiple ways of presenting the same semantic component, we provide a control property to hint at the renderer to which experience is preferred. Let's use a radio here, instead of a dropdown, since there are only 3 options to choose from:
{
"id": "currency",
"title": "Select Currency",
"schemas": [
{
"$id": "#schema",
"type": "object",
"displayOrder": [
"currency"
],
"required": [
"currency"
],
"properties": {
"currency": {
"title": "Currency",
"control": "radio",
"oneOf": [
{
"title": "EUR",
"media": {
"content": [
{
"uri": "urn:wise:currencies:eur:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "Euro",
"const": "EUR"
},
{
"title": "GBP",
"media": {
"content": [
{
"uri": "urn:wise:currencies:gbp:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "British pound",
"const": "GBP"
},
{
"title": "USD",
"media": {
"content": [
{
"uri": "urn:wise:currencies:usd:image",
"type": "uri"
}
],
"type": "avatar"
},
"description": "United States dollar",
"const": "USD"
}
]
}
}
}
],
"layout": [
{
"type": "form",
"schemaId": "#schema"
},
{
"title": "Continue",
"control": "primary",
"behavior": {
"action": {
"url": "/continue"
},
"type": "action"
},
"type": "button"
}
]
}You can find examples of all of our available components on the features page.