NAV Navbar

Introduction

Zenoo provides a niche platform for building, defining and orchestrating Digital Onboarding (DO) processes. The Zenoo Hub provides the ability to reconfigure the process to alter the orchestration.

Purpose

The Zenoo Platform has been built from the ground up by experienced developers, product managers and UX experts to solve a number of challenges facing businesses that need to onboard customers.

Zenoo is:

Our aim is to arm developers with a toolkit that makes building, managing and optimizing customer interactions less burdensome and more enjoyable, while improving the bottom line for businesses who embrace our approach. We do this by ensuring each customer interaction is unique and optimized to maximise conversions.

Easy onboarding with Zenoo

The Zenoo architecture has been built with an understanding that not all onboarding channels or customers are the same. With this in mind, The Zenoo Hub can initiate an onboarding experience either when a customer takes action (such as clicking on calculator) or through an API (onboard this customer).

As an example, if a customer applies for a loan on a partner website, the process would be as follows:

  1. Customer visits your website and is asked to complete a DO process.
  2. The client initiates a specific DO process by redirecting Customer to Zenoo Hub Client
  3. The Zenoo Hub Client engages the Hub Backend API to manage the DO process orchestration, processing data, performing checks, and other functions.
  4. The website receives the DO process result and responds with a redirect URL.
  5. The customer is redirected back to a Website using the redirect URL

Architectural Overview

At the heart of Zenoo Platform, Hub is a workflow engine. Each DO process is defined by a workflow using a custom DSL (Domain Specific Language). The workflow engine is used for orchestrating the corresponding DO process, i.e. a series of pages (routes), external calls, etc. This approach makes it possible to change a DO process configuration on-the-fly without rebuilding and redeploying the Zenoo Hub.

The Zenoo stack consists of several components:

Hub overview classic

Digital HUB

Workflow engine

Each workflow execution is assigned an execution context and a (UUID). The UUID identifies a Customer throughout the whole workflow execution.

The execution context stores the current state of a workflow execution as a set of context attributes.

Context attributes

The context attributes define a data model for the corresponding workflow definition. The attributes are hierarchical, and are used for sharing data between different workflow commands and making flow control decisions.

Each attribute can be accessed by its key, such as client.address.city

There are two ways that the context attributes can be set and updated:

config.logo << 'http://logo.png'
products << [product1: "Product1", product2: "Product2"]
route('products') { namespace product }
client.basic << route('basic info')
credit << exchange('credit check')

Execution UUID

Execution UUID is a predefined context attribute which can be used to distinguish a user session or as a specific info in a connector request to provide a relation to each execution for an external or internal service.

uuid - context unique identifier

Usage:

exchange('sendinfo') {
   tosend.sessionId << uuid
}

Workflow Execution

Workflow is managed using the following:

Executor life-cycle

A separate executor handles each workflow execution, including:

DSL

Each DO process is defined by a workflow using a custom DSL (Domain Specific Language).

The workflow DSL provides the following commands:

See the subsections below for an explanation of each.

route

A route corresponds to a page or screen, depending on the Hub Client implementation. The purpose is to display route-specific information and gather customer input.

A route payload, submitted via Hub Client, is validated using the validate payload specification and stored as Context attributes using the namespace.

route('name') {
    uri '/uri'
    export data
    function 'fnc1', 'fnc2, ...
    validate { payload specification }
    namespace namespace
    checkpoint()
    terminal(result)
}

Parameters:

See route examples for more details.

exchange

Makes an external (API) call using a connector (specified by a type) with a connector-specific configuration. To handle a connector failures, a timeout, retries and fallback can be specified. A randomized exponential backoff strategy is used for connector retries. An exchange is executed asynchronously when marked with async(). The connector result is validated using a payload specification and stored as a context attribute under the namespace.

exchange('name') {
    connector 'type'
    config configuration
    fallback {
        workflow
    }
    async()
    timeout 30
    retry 3
    retryBackoff 5
    validate { payload specification }
    namespace namespace
}

Parameters:

path

Executes a registered path (sub-workflow) specified by a name.

path 'name'

function

A function makes it possible to query dynamic data, perform complex calculations or make external calls using exhange(). Functions be used within a workflow as well as by a Hub client via Hub Client API. A function scope, either 'global' or 'route', controls the corresponding function availability via the REST API. Global functions are available throughout the whole workflow execution, whereas a 'route' function is only available within a specific route.

function('name') {
    script
}

match

Executes a workflow when an expression is true. The expression can contain context attributes.

match (expression) {
    workflow
}

exist

Executes a workflow when an attribute is set.

exist (attribute) {
    workflow
}

switch / case

The switch statement matches expression with cases and executes the matching case.

It's a fallthrough switch-case. You can share the same code for multiple matches or use the break command.

It uses different kinds of matching like, collection case, regular expression case, closure case and equals case.

switch (expression) {
    case "bar":
        route "Bar"

    case ~/fo*/:
        route "Foo"
        break

    case [4, 5, 6, 'inList']:
        route "Matched
        break

    default:
        route "Default"
}

loop-until

Executes a workflow until an expression is true. Optionally, you can specify a maximum number of the workflow block is executed.

loop {
    workflow
} until (expression)

loop(number) {
    workflow
} until (expression)

lookup

Looks up existing (active or expired) execution context matching a condition. The condition is specified as a context attribute. An execution context is matched if it contains the condition attribute. Active executions are tried first. If no active execution context matches the condition, the expired executions are tried. This applied only when execution are persisted, i.e. the logger profile is active.

lookup(client.mobile)
existing << lookup(pco.applicationId)

Command registration

In a command registration, you register and use a command configuration by employing a name. The purpose is to enable reuse of a command configuration in a workflow script.

The following command configurations can be registered: - route(name) - exhange(name) - path(name) - function(name) - global function(name)

Here's a general example of registration for a route command:

register {
    route('basic') {
        uri '/basic'
    }
}

route 'basic'
route('basic') {
    terminal()
}

In addition, a registered command configuration is useful as a template that may be refined further in a workflow script.

The following example illustrates a command registration, usage (as-is), and configuration refinement.

As seen in this example, a global exchange configuration can be registered to configure a default exchange fallback, timeout, and retry policy.

register {
    exchange {
        retry 5
        timeout 15
        fallback {
            route 'error'
        }
    }
}

Payload validation

The validate block is used for route and exchange results validation.

It specifies a result (fields) structure and data constrains.

A field is defined by a name, data constrains (validators) and nested fields. The default validator for a field is mandatory, i.e. a field is mandatory if listed.

You can use the following validators - string() a field must be a string - number() a field must be a number - truefalse() a field must be a boolean - file() a field must be a file descriptor - file mimeType a field must be a file descriptor and match the mime type, e.g. application/pdf, image, etc. - oneOf value1, value2, ... a list of values, a field data must be one of the values - regex ~/pattern/ a regular expression to match a field data, the expression can be a string or a groovy regex pattern syntax

Here is an example:

validate {
    firstname
    lastname
    address {
            city { values "Prague", "Paris" }
            zip { regex ~/^[0-9]{5}(?:-[0-9]{4})?$/ }
        }
}

See validate examples for more details.

Workflow validation

Connectors

Connectors are the integration points of the entire workflow orchestration. They are wrapped by exchange commands to be used within the DSL.

Throughout the workflow execution, external/internal providers can be called by means of exchanges that trigger the connectors. The connectors fetch results and decide in each step what to do with the provider responses accordingly.

For a typical onboarding journey, connector use cases include the following:

Connector DSL examples

Here are some examples of common connector usage:

Sending one-time 2FA passcode

exchange('Send One-Time Passcode') {
    timeout 5
    connector('send-otp')
}

Authenticating user ID

exchange("Authenticate Document") {
    namespace upload
    timeout 60
    connector('authenticate-document')
}

Getting loan information

exchange('Loan Summary') {
    namespace loan
    connector('fetch-loan-summary')
    config clientKey: personal.clientKey
    validate {
        loanId
        loanAmount
        loanTerm
        totalRepayment
        monthlyRepayment
    }
}

Calling credit bureau

exchange('Check Credit Bureau') {
    connector('credit-check')
    config clientKey: personal.clientKey
    validate {
        creditCheckResult {
            oneOf 'SUCCESS', 'FAILED', 'ERROR'
        }
    }
}

Fetch decision

exchange('Contract Decision') {
    namespace decision
    connector('fetch-contract-decision')
    validate {
        responseFlag {
            oneOf 'ACCEPT', 'DECLINE', 'REFER', 'SOLVENCY_CHECK', 'COUNTER_OFFER'
        }
    }
    timeout 30
}

Send reminder

exchange('Send Reminder') {
    namespace crm
    connector('send-reminder')
    config type: reminderType
}

How to use it in workflow

route('Mobile number')
exchange("send OTP")

route('OTP entry')
exchange("verify OTP")

route('Authenticate Document')
exchange("Authenticate Document")

Metrics

The Zenoo Hub employs Micrometer—a vendor-neutral application metrics facade—to integrate with the most popular monitoring systems.

Micrometer has a built-in support for AppOptics, Azure Monitor, Netflix Atlas, CloudWatch, Datadog, Dynatrace, Elastic, Ganglia, Graphite, Humio, Influx/Telegraf, JMX, KairosDB, New Relic, Prometheus, SignalFx, Google Stackdriver, StatsD, and Wavefront.

The following metrics will automatically register:

Executor metrics

Execution metrics

JVM metrics

In addition, you can register custom metrics in a workflow script using the metrics DSL.

Hub Backend REST API

The Hub Backend provides a REST API for Hub Clients and Backoffice.

Hub Client API

The Hub Client API is used by Hub Clients to start, resume, and get the current state of a workflow execution.

Route resource

Route resource represents a route that will be rendered by a Hub client. It contains these fields:

Security

The execution API endpoints are secured using JWT tokens. A token is generated when a workflow execution starts. This token is then used for accessing the corresponding workflow execution, identified by uuid.

The token expiration is set to 8 hours and can be modified using the jwt.expiration property.

When using the execution API endpoints, the token must be sent in the Authorization header.

Authorization: Bearer {token}

Start new execution

POST /api/executor/{name}

Creates and starts a new executor for a workflow, which is specified by a name. Optionally, omit the name if the Hub instance has only one workflow.

Optionally, the body can contain a JSON payload that can be then accessed in a workflow script using it.

Response

Example:

HTTP/1.1 201 Created
Content-Length: 80
Content-Type: application/json;charset=UTF-8
Location: http://localhost:8080/api/execution/511b0441-d6d0-4a8a-8003-22ef52740847

{
    "uuid": "511b0441-d6d0-4a8a-8003-22ef52740847",
    "uri": "/api/execution/511b0441-d6d0-4a8a-8003-22ef52740847",
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co"
}

Query the current route

GET /api/execution/{uuid}

Query the current route using an executor with uuid. Since it must wait until the current route is ready, the response may take a few moments.

Response

Example:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "uri": "/product",
  "terminal": false,
  "backEnabled": false,
  "export": {"product1": "Product1", "product2": "Product2"},
  "payload": {"product": "product1"}
)

Submit data and resume execution

POST /api/execution/{uuid}

Submit data gathered at the current route and resume a workflow execution using an executor with uuid.

Request

Parameters:

Example:

POST /api/executor/c973a8e7-eb24-4e55-980f-f2ea0fff680e HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "uuid": "3a0d231f-12b8-47b3-a495-b9418db294b3",
    "payload": {
        "firstname": "Joe",
        "lastname": "Bloke",
        "id_card": {
            "uuid": "a3c6c1ec-ae02-47a6-8cac-8760521f2ed8",
            "fileName": "my_cool_id_card_photo.png",
            "mimeType": "application/png",
            "size": 123123,
            "expiredOn": "2020-01-01T12:00:00Z"
        }
    }
}
Response

Example response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "uri": "/product",
  "terminal": false,
  "backEnabled": false,
  "export": {"product1": "Product1", "product2": "Product2"}
)

This is an example of a route validation error response:
HTTP/1.1 422 UNPROCESSABLE ENTITY
Content-Type: application/json;charset=UTF-8

{
    "errors": [
        {
            "field": "mobile",
            "message": "Required"
        }
    ]
}

Reverse execution to the previous route

POST /api/execution/{uuid}/back

Execution move back to the previous route which has an executor with the uuid.

Parameters:

Request

Form of the request:

POST /api/executor/c973a8e7-eb24-4e55-980f-f2ea0fff680e/back HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "uuid": "3a0d231f-12b8-47b3-a495-b9418db294b3",
    "payload": {
        "firstname": "Joe"
    }
}
Response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{
    "uuid": "fc05e808-9521-4268-af2d-dcc68a6eb4c9",
    "uri": "/basic",
    "terminal": false,
    "backEnabled": false,
    "payload": {
        "firstname": "Joe",
        "lastname": "Bloke"
    }
}

Execute function

POST /api/execution/{uuid}/function

Creates and starts a new executor for a function specified by a name with payload as input data.

Parameters:

Request
POST /api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/ HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
 "name": "exchange"
}
Response

Example response:

HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8
Location: http://localhost:8080//api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/9899999f-0845-4027-9cfd-09d1f109a420

{
    "uuid": "9899999f-0845-4027-9cfd-09d1f109a420",
    "uri": "/api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/9899999f-0845-4027-9cfd-09d1f109a420"
}

Query function result

GET /api/execution/{uuid}/function/{functionUUID}

Query the result of executed function with functionUUID within execution with uuid. Since it responds when the result is ready, it may take a few moments.

Response

Form of the response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "hello": "dummy"
}

Hub File Cache REST API

The Hub provides a REST API for caching client-uploaded files. This gives you the ability to use only cached file descriptors for form processing and avoid using multipart data. Also, this feature improves user experience since the user can upload files separately—and it is faster than a batch upload.

POST /api/files/cache

Uploads a new file to cache. The file is a multipart representation of uploaded file.

Request

Here's an example request:

POST /api/files/cache HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data

Response

HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "fileName": "my_file.pdf",
  "mimeType": "application/pdf",
  "size": 123123,
  "expiredOn": "2020-01-01T12:00:00Z"
)

DELETE /api/files/cache/{uuid}

Removes a file containing a uuid from the cache and deletes it from the server.

Request

Here's an example request:

POST /api/files/cache/9828e1e-c834-42a2-86f1-893209f63ab5 HTTP/1.1
Host: localhost:8080
Content-Type: application/json;charset=UTF-8

Response

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

HUB Client

Architecture

HUB client consists of several architectural components:

Target is a folder with appropriate source files (YAML, LESS, Assets etc.) and core dependecy: HUB Client application

HUB client

YAML

The YAML used in Target Builder is a standard YAML that has been extended with some specific YAML tags.

For initial acquaintance with the YAML read this article: Learn X in Y minutes (Where X=yaml)

Reserved fields

In the YAML files, there are two "reserved" fields in the root:

List of supported tags

Compile-time tags

Run-time tags

Deprecated tags


!include compile-time tag

Syntax

Short version (without properties):

!include ./path/file.ext

Long version (with properties):

!include file: ./path/file.ext
property1: 'some'
property2: 123
Examples
list:
  - !include ./info.md
  - !include file: ./more_info.yml
    title: 'Hello'
    withoutHeader: true
something: !include ./something.yml

!ref compile-time tag

Syntax
!ref components: component_name
Examples
components:
  header: !include file: ./components/header.yml
    title: "Default title"
  bodyItem: !include ./components/body-item.yml
  footer: !include ./components/footer.yml

...

items:
  - !ref components: header
    title: 'Welcome'
  - !ref components: bodyItem
    name: 'First one'
  - !ref components: bodyItem
    name: 'Second one'
  - !ref components: footer

!property compile-time tag

Syntax

Short version (without default):

!property some_prop

Long version (with default or required):

!property name: some_prop
default: 'Some default value'
required: true
Examples
items:
  - !property prop1
  - !property name: prop2
    default: 'Prop2 is not here :-)'
  - !property name: prop3
    required: true

!condition compile-time tag

Syntax
!condition include: typeof some_prop === "boolean" && some_prop === true
!condition omit: typeof some_prop === "string" && some_prop !== "foo"
Examples
items:
  - name: 'this item is not here :-('
    !condition include: typeof some_falsy_prop === "boolean" && some_falsy_prop === true
  - name: 'this item is here :-)'
    !condition omit: typeof some_falsy_prop === "boolean" && some_falsy_prop === true

!component run time tag

Syntax
!component as: some_component
property: 'foo'
Examples
items:
  - !component as: div
    items:
      - !component as: h1
        items: 'Hello'
      - !component as: HubClientMagicComponent
        doMagic: true

!repeat run time tag

Syntax
!repeat some_item:
  - name: 'Some 1'
  - name: 'Some 2'
  - name: 'Some 3'
component:
  !component as: span
  items: !expression some_item.name
Examples
items:
  - !repeat car: !expression export.cars
    component:
      !component as: span
      items: !expression car.name
  - !repeat pair:
      - name: 'Some 1'
        value: 'Val 1'
      - name: 'Some 2'
        value: 'Val 2'
      - name: 'Some 3'
        value: 'Val 3'
    component:
      !component as: div
      items:
        - !component as: span
          items: !expression pair.name
        - !component as: span
          items: "!expression '(' + pair.value + ')'"

The !repeat expression provides a way to iterate on some collections and map components to it. You simply specify an array as the input collection (you can use an expression as well). Also specify a component that will be rendered n times (n is length of array).

Inside the component, you can access an item by expression that is stored in the key. In the expression, you can directly access an item of the array ([key].item) or its index ([key].index).

This is the format:

!repeat [key]: [array]
component: [some component]

Examples:

Here's an array from the api:

!repeat person: !expression flow.export.persons
component:
  !component as: span
  items: !expression person.item.name

Here's an array in yaml:

!repeat item:
  - Item 1
  - Item 2
component:
  !component as: span
  items: !expression item.item

!expression run time tag

Syntax

Short version (without parameters):

!expression 'some_expression'

Long version (with parameters):

!expression eval: 'some_expression'
parameter_one: 'Some parameter value'
Examples
items:
  - !component as: span
    items:
      - !expression 'exports.someExportField'
  - !component as: span
    max:
      !expression eval: 'parseInt(param1) - param2'
      param1: !expression 'exports.someExportField'
      param2: !property test1

!function run time tag

Syntax

Short version (without parameters):

!function 'some_expression'

Long version (with parameters):

!function eval: 'some_expression'
parameter_one: 'Some parameter value'
Examples
items:
  - !component as: div
    onClick: !function 'setSomething(arg1 + "A")'
  - !component as: div
    onClick:
      !function eval: 'doSomething(parseInt(param1) - param2)'
      param1: !expression 'exports.someExportField'
      param2: !property test1
  - !component as: div
    onClick: !function |
      var a = "A";
      var b = "B";
      return a + b + " = done";

!t run time tag

Syntax

Short version (without params):

!t Some text

Long version (with params):

!t text: Some text with {paramOne} and {paramTwo}
paramOne: 'Zenoo'
paramTwo: !expression 'exports.someExportField'
Examples
items:
  - !component as: span
    items:
      - !expression 'exports.someExportField'

!cx run time tag

Syntax
!cx args:
  - 'classOne'
  - 'classTwo'
  - !expression 'exports.someClassName'
Examples
items:
  - !component as: span
    className:
      !cx args:
        - 'classOne'
        - 'classTwo'
        - !expression 'exports.someClassName'

Target configuration

Target structure

HUB Client Target should have the following structure, which can vary depending on complexity.

/src — folder with target source code
  /assets — static assets (fonts, images etc)
  /components — YAML reusable components
  /layouts — visual layouts
  /pages — configuration files for specific pages. By convention, names of these files should be the same as route names in flow
    index.yml
    ...
  /styles — LESS styles
    index.less — list of imports used in target and global styles
    fonts.less — font styles (or import from CDN)
    overstyle.less — style overrides for UI components
    variables.less — CSS variables for UI components theming
    studio-variables.less — CSS variables for Design Studio
  /translates — translations for target
    {LANG}.yml — list of translations for {LANG} locale
    ...
  index.yml — project configuration file
package.json — metadata information about the target and its dependencies
package-lock.json — dependencies tree with locked versions

Project settings

Project settings can be set in the root index.yml file. The values for these settings can be individually set for any particular environment. (Environment-specific entry points)

List of available parameters

Parameter Required Default Value Description
analytics false Analytics configuration
apiVersion false 'v0' API version ('v0' or 'v1')
authorizationTimeout false 10 Authorization cookie expiration timeout (in minutes)
backDisabledAlert false Message to be displayed in case of disabled back action
coreLocale false List of translates for Core messages (more in localization)
defaultLocale false Default locale code (more in localization)
devTools false true Toggles developer tools (open with ctrl + shift + D hotkey)
errorPage false Error page configuration
expressionContextVersion false 'v0' Expression context version ('v0' or 'v1')
favicon false Path to favicon
flowName true Flow name in Backend instance
flowStartParameters false List of parameter to be passed to flowStart action
indexPageInit false true Specifies if application initialization should start from flowStart action
loadingComponent false Component to be didsplayed during application initialization
mockData false Mocked input data for development tools
og false List of meta og tags
pages true Page Configuration
serverUrl true URL of Backend instance server
studioSettings false Studio settings
styles false LESS files includes
title true Meta title of an application, shown in browser tab header
translates false List of translates for specific languages (more in localization)

Example configuration

Here's an example of various settings in index.yml:

title: "Zenoo Demo Project"
serverUrl: "https://zenoo.onboardapp.io/api"
flowName: "zenoo"
favicon: "/assets/favicon.ico"
indexPageInit: true
mockData: !include ./mockdata.json
styles:
  - !include ./styles/index.less
analytics:
  gtm: "GTM-ID001"
authorizationTimeout: 60
translates:
  en: !include ./translates/en.yml
  cz: !include ./translates/cz.yml
defaultLocale: "en"
studioSettings:
  name: ZenooBank
  logo: /assets/logo.png
  country: Mexico
  previewUrl: https://onboarding.zenoo.com/
pages:
  index: !include ./pages/index.yml
  otp: !include ./pages/otp.yml
  loan-overview: !include ./pages/loan-overview.yml
  thanks: !include ./pages/thanks.yml
  rejected: !include ./pages/rejected.yml

Page settings

The entire application consists of pages. Each view that is presentable to a user must be implemented as page. There are two predefined user positions, the index page and the error page. The index must be inside index property in pages. In root of your yaml (typically index.yml), you can specify the value of the property errorPage. This property is name of page to which the user will be redirected when an error occurs (such as a network failure).

List of available parameters

Parameter Required Description
analytics false Analytics configuration for specific page
defaultAction false Default form submit action name
defaultActionParams false Default form submit action params
defaults false Default values for form fields
fadeAnimationBack false Use "fade" animation on back action
fadeAnimationSubmit false Use "fade" animation on submit action
items false Elements tree of specific page
og true List of meta og tags (will be merged with the ones coming from project configuration)
schema false Validation rules as a JSON schema
title false Page meta title

Example configuration

components:
  formLayout: !include @common/layouts/form-layout.yml
  formGroup: !include @common/components/form-group.yml
  header: !include @common/components/header.yml
  pinInput: !include @common/components/pin-input.yml
fadeAnimationBack: true
schema:
  required:
    - code
  properties:
    code:
      type: string
      minLength: 4
      maxLength: 4
  errorMessage:
    _: "{field} - Required field"
defaults:
  mobile: !expression "flow.export.mobile"
items:
  - !ref components: formLayout
    items:
      - !ref components: header
        progress: <\%-((3 / 8) * 100)\%>
      - !component as: div
        className: "content main-content"
        items:
          - !component as: h1
            items: "Enter your phone number"
          - !component as: p
            items: "Please enter a valid mobile phone number to where we can text a confirmation code to."
          - !ref components: formGroup
            items:
              - !ref components: pinInput
                field: code
                label: "Enter your confirmation code"
                length: 4

Error page

The error page can be specified as an errorPage parameter in application configuration.

errorPage: "error-page"
---
pages:
  error-page: !include ./pages/error.yml # Include error page to a list of pages

If an error page is not specified, the auth cookie will be deleted and application will be reloaded.

You can create more dynamic error page that provides useful features, such as a button to continue or reattempt the previous action (flowContinue). This button will automaticaly fetch the last stored data from the server and redirect user to correct screen.

Another useful error management feature is to provide a button that reloads the flow. If the problem is not easily resolved, have the user click a button to redirect to start of the flow in case with the form action flowReload.

If you are on error page, there are also available page parameters that contain the reason for the error. For example, query the value of page.params.error to get the raw output from the error catch.

Analytics

You are able to initialize different analytics providers application starts and call specific action when certain event occurs.

List of currently available providers:

User indentification

To identify current user for different analytics providers analytics.authorizationToken configuration key can be used, e.g.:

# index.yml
analytics:
  authorizationToken: !expression "url.query.do_authorization"

In order to identify user not on initial page load, but by some event, analytics.authorization action from Expression context can be used:

- !component as: div
  onClick: !function "analytics.authorization(flow.export.identityId)"

Events

To fire custom analytics event, analytics.event action can be used:

- !component as: div
  onClick: !function "analytics.event(eventName, eventParams)"

Global application state and methods

Expressions

Expressions are a simple way to access data from the app runtime, or the response from server. Data is accessed through an object that is internally known as Core context or Expression context. If expression fails, it will return undefined. If you specify the default parameter, it will be returned when expression fails.

Examples of expressions:

property: !expression flow.export.value
property:
  !expression eval: flow.export.value
  default: Nothing

Functions

A function is another type of expression. It's useful to add some callbacks, such as a button onClick event.

Extending Expression context

To extend expression context with custom values or methods, globals configuration key can be used:

# index.yml
globals:
  test: "I am a global variable"
  sum: !expression "function (a, b) { return a + b; }"

Then in page configuration:

- !component as: Heading
  items: !expression "globals.test"
- !component as: Heading
  items: !expression "globals.sum(1, 2)"

Expression context

analytics: {
  authorization: (token) => void, // - trigger mixpanel.identify(token), GA.set({ userId: token }) and GTM dataLayer event "authorization" with parameter token (string)
  event: (eventName, eventParams) => void, // - trigger GTM dataLayer event with given eventName (string) and eventParams (object)
}
api: {
  authToken: string,
  progress: {
    [field-name]: number // - percentage of progress in file uploading
  }
}
app: {
  waiting: {
    [waiting-tag]: boolean, // - app waiting for tags
  }
}
device: {
  ... // https://github.com/duskload/react-device-detect#selectors
}
flow: {
  export: ...any-data-from-server, // - this is exported data for page in flow from server
  backEnabled: true/false, // - value of backEnabled from server for current page
  function: {
    [function-name]: (payload?: any, resultKey?: string) => void, // - call (in !fuction) any flow/route function by call function name (like `flow.function.search('something')`), you can also set output resultKey (default function-name)
    results: {
      [function-name or result-key]: ...any-data-from-server-function, // - here will be data from server under function-name or result-key property name (like `flow.function.results.search`)
    }
  }
}
form: {
  data: {
    [field-name]: ...data-inside-field, // - data can be string, file, etc.
  },
  field: {
    [field-name]: {
      isValid: boolean,           // - is field valid
      validationError: string,    // - only validation erros generated by page schema
      error: string,              // - all field errors including validation errors and server errors
      isFilled: boolean,          // - is there any data
    }
  },
  valid: boolean,           // - is form valid
  disabled: boolean,        // - is form disabled
  tag: {
    [tag-name]: boolean,  // - form visual tags
  },
  // checks if all tags are present
  hasTags: (tags: string | string[]) => boolean
  // add tags to form
  addTags: (tags: string[]) => void,
  // regexp as string can be also used to identify more tags
  removeTags: (tags: string[]) => void,
  // change value of some field, you can use callback that will be called after data set, for example if you need to submit form
  changeValue: (fieldName: string, value: any, callback?: () => void) => void,
  // submit form
  submit: (actionName: string, params: string[]) => void,
}
formatter: {
  thousandsSeparator: (value, separator) => string
}
financial: {
  rate: (periods, payment, present, future, type, guess) => any,
  ptm: (ir, np, pv, fv, type) => number,
  round: (num, places?) => number,
}
url: {
  ... // - https://github.com/unshiftio/url-parse
}
changeLocale: (locale: string) => void
t: (string, params) => string
page: {
  params: any // - for example page.params.error contains informations, why you are on error page
}

Localization

HUB Client has built-in support for multiple locales and an easy way to manage translations.

All translation keys are being stored in src/translates folder under appropriate YAML files: {LANG}.yml and should be described in index.yml project configuration file:

defaultLocale: "en"
translates:
  en: !include ./translates/en.yml

Translations can be stored under nested keys, e.g.

# translates/en.yml
welcome:
  text: "Automated real-time identity authentication & decisioning."
  button: "Lets get started"

otp:
  title: "Enter your confirmation code"
  text: "We've sent a confirmation code to your phone number"

...

# Page configuraion
- !component as: Heading
  items: !t "welcome.text"
- !component as: SubmitButton
  text: !t "welcome.button"

In order to use translation key with some parameter, the following notation can be used:

# translates/en.yml
welcome:
  text: "Some text with {param}"

...

# Page configuraion
!t text: "welcome.text"
param: 'Zenoo'

There are two ways to use translations in YAML:

To change locale, use the action changeLocale, in which the first parameter is target locale name.

Examples:

# Evaluate translation for given translation key
- !t translation_key

# Evaluate translation for dymanic translation key (e.g. error coming from Backend)
- !expression t(flow.export.translation_key)

Built in components

Using components in YAML page configuration

Each component must have an "as" parameter that specifies the component element name. You can use the provided component name, or the standard HTML DOM element.

Each component has also $reference property, which can create a named reference to DOM element. This reference is accessible through a $reference object inside appDataContext.

Examples of $reference:

# Referenceable div
!component as: div
$reference: myDiv

# Some component that uses this reference
property: !expression #reference.myDiv

UI components

List of UI components can be viewed in Zenoo Storybook.

Logical components

Fragment

Fragment wrapper - this is a dummy component without any effect on the rendering. You can wrap a set of components into it to create a single component with children.


ClassNameWrapper

Wrapper for adding a class (or removing) to/from an element. You can use expressions to determine whether to show the class or not. The default wrapping element is div, but you can change it by setting the property element - tag name.

Class names are specified as name of properties, and enable it or disable by setting this property to true or false, respectively. It's the same as CX (classNames package). You can also specify animatedClassName and this class name will be added, when some class changes, and will be removed in the animation end event.

Properties:

element?: string;
classNames: {[name: string]: boolean};
animatedClassName?: string;


VisibilityWrapper

Wrapper for hiding and showing elements. Elements (items) inside it will be shown only if property visible is set to true, or property invisible is set to false.

Properties:

visible?: boolean;
invisible?: boolean;


Trigger

This component permits you to track changes on some value and retrieve that value with an expression. On loading (mounting) of this component, the load callback will be called with current value. If this value was changed, the change callback will be called with old and new values. Value variables are accessible in scope. For load function there is value, that referes to current value of the trigger. In a change callback, there is an oldValue and newValue-which contain the values before and after the change, respectively.

Example

- !component as: Trigger
   value: !expression form.data.mobile
   load: !function form.changeValue('otherFieldName', '08123456789')
   change: !function console.log("Data was changed", oldValue, newValue)

Properties

value: any;
load?: (value: any) => void;
change?: (oldValue: any, newValue: any) => void;
hysterezis?: number; // timeout for on change callback


Script

A script component that runs a script, which is defined by the onEnter and onLeave property (use a !function expression). The onEnter script will be run immediately if the trigger property is not defined—or the value of the trigger property changes from false to true. The onLeave script will be run when a component unmounts (in case trigger has not been set), or when trigger changes from true to false.

You can also specify runOnUpdate and the script will run whenever trigger changes. If runOnUpdate is false or not set, both the onEnter and onLeave scripts will be run only once. You can also provide a URL to an external script.

Properties

attributes?: {[name: string]: string} // - html attributes, can by used only in combination with url
onCreate?: () => void
onEnter?: () => void
onError?: () => void
onLeave?: () => void
url?: string;


Target compilation

For target compilation HUB Client contains CLI tool, which uses Target Builder module internally.

The Target Builder is a tool to process Target files and combine the files with the contents of compiled @zenoo/hub-client-core into releaseable package.

Target Builder uses the index.yml file as entry point, then combines and compiles all files that are included inside this file and also process files in assets folder. These files are supported: YAML, JSON, HTML, MD, LESS, and CSS.

Target Builder passes these steps:

  1. Process assets folders and put it to output folder.
  2. Process the entry point YAML file, and recursively process all includes (for more details, see !include).
  3. Process/compile all other files (such as less, md, etc.).
  4. Combine all output into one large configuration.json file and one styles.css file and place into the output folder.

CLI commands

# Run from specific target directory
hub-client <command> [<environment>] [-p <port>]
<command>

Command to run over specified target: build, dev or deploy

<environment>

Environment name on the basis of which target entry point will be taken

${target}/src/index.${environment}.yml
Examples
# In targets/<target_name> folder
hub-client dev
hub-client deploy stage
hub-client build production

Options

Parameter Explanation Type Default
--port, -p Port for webpack dev server number 8888
--branch, -b Branch to deploy string master

Assets processing

While building a target, two folders of assets are being processed:

Target Builder collects all of the content in these two folders, adds a random hash postfix to the filenames (to prevent caching issues), and places it into the /assets output folder. All references to assets will be replaced with new hashed names, both in YML and LESS/CSS files.

In case of collisions between <target>/src/assets and @zenoo/hub-client-common/lib/assets, the file from <target>/src/assets will have a higher priority. Use this prioritization to replace some "default asset" with an asset specific only for this target.

IMPORTANT

The format to reference an asset should be: /assets/some_file.ext.

Other reference formats such as ./../assets/some_file.ext will not be resolved properly.

Styles processing

In root of each YAML which is included in any depth of target scruture you can define styles field which can contain array of CSS or LESS files to include it into target build.

In finally steps of target build is all styles filtered by unique, that means you can import one style file in multiple components as many times you want, and on output styles.css will be each file only once.

Example:

styles:
  - !include ./style.less
  - !include ./other-style.css

Environment-specific entry points

To build a target with an environment-specific configuration in Target Builder, you can specify a different entrypoint by creating a different index.yml file.

The format of this environment-specific index.yml file must be as follows:

<target>/index.{ENVIRONMENT}.yml

Example: <target>/index.production.yml

Within this file, simply include the main index.yml entry file:

<<<: !include index.yml
serverUrl: 'https://production.onboardapp.io/api'
analytics:
  ga: '123456'
# More configuration keys for selected environment

Development tools

HUB Client has built-in devtools for easer development, support and QA process.

# index.yml application configuration file
devTools: true

IMPORTANT:

If you enable devtools for development environment, it will be inherited by other environment-specific configs. In this case devTools parameter should be explicitly set to false in appropriate environment config. Read more in Environment-specific entry points

And then development tools panel can be toggled with ctrl + shift + D hotkey.

Available features

Mocked input data

In order to test DO application faster, there is a way to set mocked user input data and go through the whole flow by submitting dataset specific to selected scenario.

Create JSON file with the similar content like:

{
  "default": { // Scenario name
    "welcome": { // Route name
      "firstName": "John" // User input data
    }
  },
  "anotherScenario": {
    "welcome": {
      "firstName": "Edgar"
    }
  }
}

And inlcude it in YAML application configuration file

# index.yml application configuration file
mockData: !include ./mockdata.json

Design Studio

Design Studio allows you to quickly design, test and deploy DO application. It gives you a lot of capabilities without the need to code.

Requirements to DO target

body {
  --base-border-color: #dde0ec;
  --base-brand-color: #2c3c9a;
  --base-color: #465a6a;
  --base-disabled-color: #d6e5f8;
  --base-error-color: #e44343;
  --base-focused-color: #017aff;
  --base-label-color: #8893aa;
  --base-success-color: #1ea03f;

  --base-font-family: "Inter UI", sans-serif;
  --base-font-size: 16px;
  --base-h1-font-size: 29px;
  --base-h2-font-size: 23px;
  --base-h3-font-size: 20px;
  --base-h4-font-size: 20px;
  --base-small-font-size: 14px;

  --base-logo: url("/assets/download.jpeg");
  --base-header-logo: url("/assets/download.jpeg");
}

src/layouts/main.yml

components:
  footer: !include ../components/footer.yml
!component as: LayoutWithSidebar
isLayout: true # Required for DS to understand how to change properties in nested layout items
items:
  !property name: items
footer:
  - !ref components: footer
{
  "calculator": {
    "loanPurposeList": {
      "Business": "Business",
      "Personal": "Personal"
    }
  },
  "employment-info": {
    "employmentTypeList": {
      "Employed": "Employed",
      "Self-Employed": "Self-Employed"
    }
  }
}

Design Studio settings available in application configuration

Parameter Description
country Used to define global formats (currency, date format, phone number mask etc.)
flowExport Mocked flow export to display pages in DesignStudio correctly
logo Logo to be displayed in projects list
name Project name to be displayed in DesignStudio, title will be used as a default value
previewUrl Link to preview environment

Example configuration

title: "Zenoo Demo Project"
...
studioSettings:
  country: "Mexico"
  flowExport: !include ./flowExport.json
  logo: "/assets/logo.png"
  name: "ZenooBank"
  previewUrl: "https://onboarding.zenoo.com/"

Technology Stack

Code languages Frameworks
HUB Backend Groovy, Java 8 Spring 5, Project Reactor
HUB Client TypeScript 3 React, Node.js

Environment

Server requirements

The following requirements and assumptions are based on average production values (seen in typical installation).

Assumptions

Transaction: Client starts the flow and reaches the first decision point.

Transaction hours: 7:00 - 24:00 (17 hours in total)

Peak times: 3 times per day (7:00, 12:00, 20:00)

Transaction numbers:

Period Count
Per Month 150K - 210K
Per Day 5K - 7K
Per Peak 1667 - 2333
Per Hour 417 - 583

Average network in/out: 50-100 MB

HUB Backend

Number of CPUs min 2
RAM 4 GiB
Storage min 8 GiB
OS Linux base (AWS Linux, RedHat, CentOS)
Dependency OpenJDK 8, Mongo DB

HUB Client

Number of CPUs min 2
RAM 2 GiB
Storage min 8 GiB
OS Linux base (AWS Linux, RedHat, CentOS)
Dependency Nginx

Operations

Deployment Options

By pipeline

The HUB platform can be easily integrated to any CI & CD environment such as Jenkins, Circle CI and GitLab.

In typical scenarios, at the end of a green pipeline step, compiled artifacts are uploaded and started automatically.

Typical steps of a pipeline may consist of:

Build

Example backend commands:

./gradlew clean build # Build and run all tests
./gradlew bootJar # Create runnable HUB instance artifact with all dependencies

Example frontend commands:

npm i # Install NPM packages
npm run build:production #Build HUB client application

This step compiles the artifacts and runs unit/integration tests in an isolated environment (e.g.: Docker image).

Deploy to Stage

After a successful build step, all artifacts are uploaded to the staging environment together with their configuration.

Deploy to Production

After end-to-end testing and UAT, when the necessary approval is in place, the stable stage version in question is uploaded to the production environment.

Manually

In case the client side has security concerns or internal process related limitations, artifacts are delivered (through the Zenoo artifact repository or external file system) and deployed manually.

Deployment can be done on-premise, where the client has its own dedicated servers—or on-cloud such as AWS or Azure.

As long as server requirements are met, the HUB platform can be deployed to any cloud provider.

Deliverables

Operational Commands

Here are examples of deployment commands:

HUB Backend

java -jar hub-instance.jar --spring.config.location=application.yaml

HUB Client

Client application files are compiled as static files to be served by a Nginx web server.

Here's a common deployment scenario:

Step 1 - Locate static files on server:

/var/www/hub-client

Step 2 - Set the location under Nginx configuration:

server {
    listen 8080;
    server_name hub-client.onboardapp.io;
    location / {
      root /var/www/hub-client;
      index index.html;
    }
}

Step 3 - Restart the server

sudo service nginx restart

Scalability, Fault tolerance and High Availability

Logging & Monitoring

Integration to reporting & logging tools:

Security

Encryption

APIs use secure encrypted connections (HTTPS)

Authentication & Authorization

hub-client API secured using JWT tokens (For bearer authentication, see API security for more details)

Static Code Analysis (Code Vulnerability)

In each release, the HUB backend & client code is analyzed by a static code analyzer to indicate vulnerabilities and potential security breaches.

Examples

Simple Loan Application

Here we present an illustration of a simple workflow. The client selects which type of application (Loan or Credit Card) to complete and will see a "Success" page at the end of the workflow.

Version 1 — All commands are in one workflow script

simple-app.wf

values.applicationTypes << [
        "card": "Credit Card",
        "loan": "Loan",
]

route('Application Type') {
    namespace application
    uri '/application-type'
    validate {
        selectedType
    }
    export types: values.applicationTypes
}

match(application.selectedType == 'card') {
    route('Credit Card Application') {
        namespace application
        uri '/credit-card-application'
        validate {
            email
            personalId
            amount
        }
    }
}

match(application.selectedType == 'loan') {
    route('Loan Application') {
        namespace application
        uri '/loan-application'
        validate {
            email
            personalId
            amount
            term
            purpose
        }
    }
}

route('Successful') {
    uri '/successful'
    terminal()
}

Version 2 - DSL commands are separated to .init file to keep .wf easy to read.

simple-app.wf

values.applicationTypes << [
        "card": "Credit Card",
        "loan": "Loan",
]

route('Application Type')

match(application.selectedType == 'card') {
    route('Credit Card Application')
}

match(application.selectedType == 'loan') {
    route('Loan Application')
}

route('Successful')

simple-app.init

route('Application Type') {
    namespace application
    uri '/application-type'
    validate {
        selectedType
    }
    export types: values.applicationTypes
}

route('Credit Card Application') {
    namespace application
    uri '/credit-card-application'
    validate {
        email
        personalId
        amount
    }
}

route('Loan Application') {
    namespace application
    uri '/loan-application'
    validate {
        email
        personalId
        amount
        term
        purpose
    }
}

route('Successful') {
    uri '/successful'
    terminal()
}

Route DSL Examples

Terminal route

route("Finish") {
    uri "/finish"
    terminal()
}

Terminal route with a checkpoint

route("Rejected") {
    uri "/rejected"
    checkpoint()
    terminal()
}

Route with result validation

route("Basic Info") {
    uri "/basic"
    namespace client
    validate {
        firstname
        lastname
        address {
            street
            city { values "Prague", "Paris" }
            zip { regex ~/^[0-9]{5}(?:-[0-9]{4})?$/ }
        }
    }
}

Route exporting a static map

route("Select Product") {
    uri "/product"
    namespace product
    export  product1: "Product 1",
            product2: "Product 2",
            product3: "Product 3"
    validate {
        values "product1", "product2", "product3"
    }
}

Route exporting an attribute

delivery.address << [street: "Dejvicka 18", city: "Prague", zip: 12345]

route("Delivery address") {
    uri "/delivery"
    export address: delivery.address
}

Route registration

register {
    route("Delivery address") {
        uri "/delivery"
        export delivery.address
    }
}

route("Delivery address") {
    export client.address
}

Payload Validation Examples

Single mandatory field

validate {
    mobile
}

Mandatory field with regex validator

validate {
     mobile { regex ~/[0-9]{5}/ }
}

Mandatory field with value validator

validate {
     product { values "product1", "product2", "product3"}
}

Mandatory nested fields

validate {
    firstname
    lastname
    address {
        street
        city
        state
        zip
    }
}