Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 107 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

v4.2

Loading...

Loading...

Overview

Loading...

Loading...

Loading...

Loading...

Component Definition

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Customizing UI

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Appmixer Self-Managed

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

API

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Appmixer SDK

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Appmixer Backoffice

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Appmixer CLI

Loading...

App Registration

Loading...

Loading...

Loading...

Loading...

Loading...

Introduction

Appmixer is a workflow engine together with a web user interface that allows end-users to create business processes in an easy-to-use drag&drop UI without writing a single line of code.

For companies that seek to embed the Appmixer workflow technology into their own products (OEM), it is important to know that thanks to its modular architecture, customizations are possible both on the process level but also in the UI layer.

The Appmixer workflow engine provides features that are critical to workflow applications:

  • Error handling. Errors are handled gracefully, logged and actions repeated when possible.

  • Message passing. Data messages are delivered once and only once between connected components in a workflow.

  • Custom components. Components in a workflow are seen as black-boxes by the engine and their behaviour can be completely customized (they can call either external APIs, internal APIs, do some processing or scheduling).

  • Quotas and Limits. Appmixer engine can throttle the processing of components to make sure API usage limits are respected.

  • Authentication. Appmixer supports industry standards for authentication out-of-the box (API keys, OAuth 1, OAuth 2). All tokens are automatically refreshed when necessary.

  • Monitoring. All transactions and data passing through Appmixer are logged. Get real-time insight to all activities.

The Appmixer system can be installed on a number of different systems such as private clouds (OpenShift), cloud computing platforms (AWS) or plain Linux machines.

Migration from 4.1

Designer Toolbar

/* Create "Designer". */
var designer = appmixer.ui.Designer({
    el: '#your-designer',
    options: {
        menu: [
            { label: 'Rename', event: 'flow:rename' },
            { label: 'Insights Logs', event: 'flow:insights-logs' },
            { label: 'Clone', event: 'flow:clone' },
            { label: 'Share', event: 'flow:share' },
            { label: 'Export to SVG', event: 'flow:export-svg' },
            { label: 'Export to PNG', event: 'flow:export-png' },
            { label: 'Print', event: 'flow:print' }
        ],
        // buttons in the toolbar
        toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['logs']
        ]
    }
});

Custom Inspector Fields

The props structure for Custom Inspector Fields was changed. Although the same data is available inside the fields, it is accessed a bit differently. Until the 4.0 version the props definition looked like this:

props: {
   value: { type: null, required: false },
   variables: { type: Object, default: () => ({}) },
   inputManifest: { type: Object, default: () => ({}) },
   descriptorPath: { type: String, default: '' },
   errors: { type: Array, default: () => ([]) },
   disabled: { type: Boolean, default: false }
}

This is how it looks in 4.1:

props: {
    context: { type: Object },
    value: { type: null, required: false },
    errors: { type: Array, default: () => ([]) },
    disabled: { type: Boolean, default: false },
}

The context object contains the following properties:

  • componentId

  • inputManifest

  • componentManifest

  • descriptorPath

  • variables

As you can see, the context object now contains the contextual data - i.e. the data from the component to which the input belongs. The same data as before remains available, and access to the componentId and componentManifest is now available as well. So if you have references to inputManifest, descriptorPath or variables, change them and refer to these properties inside the context object. For example, if we had this code:

computed: {
    config() {
        return this.inputManifest.config;
    },
},

You have to update it to:

computed: {
    config() {
        return this.context.inputManifest.config;
    },
},

Slack app

The toolbar is configurable. You have to specify them in the appmixer.ui.Designer constructor, otherwise, it will be empty. More information .

Migrate the legacy Slack applications. The Slack Appmixer module in 4.1 does not work with newly registered Slack applications. We upgraded the module to the newest Slack API. Unfortunately, it is no longer compatible with legacy Slack apps. More information can be found in the section.

Drag&Drop Workflow & Integration Designer

End User Guide

Knowledge base

Please visit the for end-user tutorials.

Appmixer Knowledge base

Introduction

Appmixer is a workflow engine together with a web user interface that allows end-users to create business processes in an easy-to-use drag&drop UI without writing a single line of code.

For companies that seek to embed the Appmixer workflow technology into their own products (OEM), it is important to know that thanks to its modular architecture, customizations are possible both on the process level but also in the UI layer.

The Appmixer workflow engine provides features that are critical to workflow applications:

  • Error handling. Errors are handled gracefully, logged and actions repeated when possible.

  • Message passing. Data messages are delivered once and only once between connected components in a workflow.

  • Custom components. Components in a workflow are seen as black-boxes by the engine and their behaviour can be completely customized (they can call either external APIs, internal APIs, do some processing or scheduling).

  • Quotas and Limits. Appmixer engine can throttle the processing of components to make sure API usage limits are respected.

  • Authentication. Appmixer supports industry standards for authentication out-of-the box (API keys, OAuth 1, OAuth 2). All tokens are automatically refreshed when necessary.

  • Monitoring. All transactions and data passing through Appmixer are logged. Get real-time insight to all activities.

The Appmixer system can be installed on a number of different systems such as private clouds (OpenShift), cloud computing platforms (AWS) or plain Linux machines.

Drag&Drop Workflow & Integration Designer

label

(optional)

The label of your component. If not label is specified, then last part of name will be used when component is dropped into Designer. If your component name is appmixer.twitter.statuses.CreateTweet then CreateTweet will be name of the component unless you specify label property. This allows you to use spaces as opposed to the name property.

{ "label": "Create Tweet" }

Component

Components are the building blocks of Appmixer. Each component in a flow reacts on incoming messages, processes them and produces outgoing messages. User can wire components together to define complex behaviour and workflows. Usually, components call external APIs but they can also do some internal processing, logic or scheduling.

To make our example flow complete, it is important to note that any component can be configured using data generated on output ports of any component back in the chain of connected components. In our example, our SendChannelMessage component sends a message on Slack channel #qa-messages with text containing the city, humidity, pressure and temperature as it was received from the weather API. The user configures the flow in the designer UI simply by selecting placeholders (variables in the Appmixer engine jargon) that will eventually be replaced when the flow goes to the running state and the actual data is available.

Components have some important properties that we should mention before diving into the details:

  • Components don't know about each other. All components are totally independent and loosely coupled. They only react on incoming messages and produce outgoing messages. The linkage between components is not internal to the components themselves but rather a mechanism of the Appmixer engine and its protocol.

  • Components are black-boxes to the Appmixer engine. The engine does not know and also does not need to know what components internally do and how they are implemented. It only wires them together through ports and makes sure messages are always delivered and in the right order.

Take the example above. There are three components in the flow. The first one (Timer) we call a trigger because it does not have any input ports and so the component generates outgoing messages based on its internal logic. In our case, the Timer component sends messages to its output port out in regular intervals that the user can specify in the UI. As soon as a message leaves an output port, it travels through all the connected links to input ports of other connected components. In our scenario, when a message leaves the out port of our Timer, it goes to the location input port of the GetCurrentWeather component. As soon as the GetCurrentWeather component receives a message on its input port, it starts processing it. In this case, it requests current weather information from the API. Once a response is received from the API, the component continues to send the result to its output port weather. Note that the location for which we're requesting the current weather can be specified by the user in the UI. The process then repeats for all the other connected components until no message is generated on an output port or there is no other component connected.

https://openweathermap.org

Manifest

The component manifest provides information about a component (such as name, icon, author, description and properties) in a JSON text file. The manifest file must be named component.json.

Example manifest file:

{
    "name": "appmixer.utils.controls.OnStart",
    "author": "Martin Krčmář <martin@client.io>",
    "label": "On Flow Start",
    "description": "This trigger fires once and only once the flow starts.",
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV...",
    "outPorts": [
        {
            "name": "out",
            "schema": {
                "properties": {
                    "started": {
                        "type": "string",
                        "format": "date-time"
                    }
                },
                "required": [ "started" ]
            },
            "options": [
                { "label": "Start time", "value": "started" }
            ]
        }
    ]
}

Members

  • Name of the component.

  • label

    Label that will be used instead of name.

  • Component icon.

  • Author.

  • Description.

  • Authentication mechanism if the component requires authentication.

  • Parameters for the quota module used in the component (to conform with API usage limits).

  • Properties of the component.

  • Definition of the input of the component and how data transforms before it is processed by the component.

  • Definition of the output of the component and variables that other connected components can use in their input.

  • Requirements for the component input messages to decide whether the component is ready to fire.

  • Enable polling mechanism on the component.

Basic Structure

Introduction

Components in Appmixer are structured into "services", "modules" and "components" hierarchy. Each service can have multiple modules and each module can have multiple components. For example, "Google" service can have "gmail", "calendar" or "spreadsheets" modules and "gmail" module can have "SendEmail", "NewEmail" and other components:

This hierarchy is reflected in the directory structure of component definitions. Typically, services and modules are structured in two ways. Either the service itself appears as an "app" in Appmixer or modules are separate apps. If a module has its own manifest file (module.json), it is considered a separate app in Appmixer.

For example, in case of Google, we want to have separate apps for each module (GMail, Calendar, Analytics, ...):

But in case of Twilio, we may just want to have one app and all the actions/triggers as different components of the Twilio app:

Directory Structure

As mentioned in the previous section, services, modules and components must follow the service/module/component directory structure. The following images show the two different ways you can structure your services (i.e. modules as separate apps or a service as one single app).

Service Manifest File

Service manifest is defined in the service.json file. The file has the following structure:

Available fields are:

Module Manifest File

Module manifest is defined in the module.json file. The file has the following structure (similar to the service.json file):

Available fields are:

icon

Component badge icon giving users extra context.

Make component private to hide it from the user.

Make component "webhook"-type meaning it can receive HTTP requests.

Controls whether component's internal state is preserved across flow restarts.

The icon representing the component in the UI. It must be in the Data URI image format as described here: . image/png or image/svg+xml image types are recommended. Example:

name
icon
marker
author
description
auth
quota
properties
inPorts
outPorts
firePatterns
tick
private
webhook
state
{
    "name": "[vendor].[service]",
    "label": "My App Label",
    "category": "applications",
    "categoryIndex": 2,
    "index": 1,
    "description": "My App Description",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
}    
{
    "name": "[vendor].[service].[module]",
    "label": "My App Label",
    "category": "applications",
    "categoryIndex": 2,
    "index": 3,
    "description": "My App Description",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
}    
{
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV..."
}
https://en.wikipedia.org/wiki/Data_URI_scheme

Field

Description

name

label

The label of your app.

category

App category. By default, components shipped with Appmixer are divided into two categories "applications" and "utilities" but you can have your own custom categories too. Just use any custom category name in the service manifest file to create a new category and add your service to it. This category will become automatically visible e.g. in the Appmixer Designer UI.

categoryIndex

App category index. By default, categories are sorted alphabetically, you can change that using this index property. Optional.

index

The app index within the category. This allows sorting the apps within the same category.

description

Description of your app.

icon

App icon in the Data URI format.

Field

Description

name

label

The label of your app.

category

App category. By default, components shipped with Appmixer are divided into two categories "applications" and "utilities" but you can have your own custom categories too. Just use any custom category name in the module manifest file to create a new category and add your app to it. This category will become automatically visible e.g. in the Appmixer Designer UI.

categoryIndex

App category index. By default, categories are sorted alphabetically, you can change that using this index property. Optional.

index

The app index within the category. This allows sorting the apps within the same category.

description

Description of your app.

icon

App icon in the Data URI format.

Flow

A flow consists of an orchestrated pattern of business activity enabled by interconnecting components together that transform input data (coming from trigger-type of components), perform actions, store data and/or load data to external systems.

Appmixer provides an interpreter for running flows and UI to manage flows.

Flow Descriptor

Flows are represented as JSON objects in the Appmixer engine. The JSON object is called "flow descriptor" in the Appmixer jargon and for the example image above, it may look like this:

The flow descriptor contains information about the components in the flow and their types, how they are interconnected (source), their properties (config.properties) and data transformation for all input ports (config.transform).

If any component position property (x, y) is not a number, Appmixer SDK will apply a tree layout on the entire diagram after the flow is loaded.

The name of the service. The name must have the [vendor].[service] format where [vendor] is the Vendor name (See e.g. for more details). Normally you'll have just one vendor or use the default 'appmixer' vendor. [service] is the name of your service. Example: "appmixer.google", "appmixer.twilio", ... .

The name of the module. The name must have the [vendor].[service].[module] format where [vendor] is the Vendor name (See e.g. for more details). Normally you'll have just one vendor or use the default 'appmixer' vendor. [service] is the name of your service and [module] is the name of your module. Examples: "appmixer.google.gmail", "appmixer.google.calendar", .... . Note that the directory structure of your module must follow this name. In other words, if you have a module named "appmixer.myservice.mymodule", your directory structure will look like this: myservice/mymodule.

{
    "76a77abf-d5ec-4b1c-b6e8-031359a6a640": {
        "type": "appmixer.utils.timers.Timer",
        "label": "Timer",
        "x": 265,
        "y": 115,
        "config": {
            "properties": {
                "interval": 15
            }
        }
    },
    "a0828f32-34b8-4c8d-b6b3-1d82ca305921": {
        "type": "appmixer.utils.weather.GetCurrentWeather",
        "label": "GetCurrentWeather",
        "source": {
            "location": {
                "76a77abf-d5ec-4b1c-b6e8-031359a6a640": [
                    "out"
                ]
            }
        },
        "x": 515,
        "y": 115,
        "config": {
            "transform": {
                "location": {
                    "76a77abf-d5ec-4b1c-b6e8-031359a6a640": {
                        "out": {
                            "type": "json2new",
                            "lambda": {
                                "city": "Prague",
                                "units": "metric"
                            }
                        }
                    }
                }
            }
        }
    },
    "11f02d1e-106e-4cf5-aae2-5514e531ea4d": {
        "type": "appmixer.slack.list.SendChannelMessage",
        "label": "SendChannelMessage",
        "source": {
            "message": {
                "a0828f32-34b8-4c8d-b6b3-1d82ca305921": [
                    "weather"
                ]
            }
        },
        "x": 735,
        "y": 115,
        "config": {
            "properties": {
                "channelId": "C3FNGP5K5"
            },
            "transform": {
                "message": {
                    "a0828f32-34b8-4c8d-b6b3-1d82ca305921": {
                        "weather": {
                            "type": "json2new",
                            "lambda": {
                                "text": "City: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[name]}}}\nHumidity: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.humidity]}}}\nPressure: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.pressure]}}}\nTemperature: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.temp]}}}"
                            }
                        }
                    }
                }
            }
        }
    }
}

description

Description of your component. The description is displayed in the Designer UI inspector panel like this:

The description should not be longer than a sentence or two. Example:

{
    "description": "This action gets the current weather conditions for a location."
}

name

(required)

The name of your component. The name must have the following format: [vendor].[service].[module].[component]. Note that all the parts of the name must contain alphanumeric characters only. For example:

{ "name": "appmixer.twitter.statuses.CreateTweet" }

The vendor part of the component name is the ID of the author of the component set. service and module allows you to organize your components into categories. These categories not only help you keep your components in a tidy hierarchical structure but it also has a meaning in that you can share your authentication and quota definitions between modules and components (more on that later). component describes the actual component activity.

auth

The authentication service and parameters. For example:

{
    "auth": {
        "service": "appmixer:google",
        "scope": [
            "https://mail.google.com/",
            "https://www.googleapis.com/auth/gmail.compose",
            "https://www.googleapis.com/auth/gmail.send"
        ]
    }
}

When auth is defined, the component will have a section in the Designer UI inspector requiring the user to select from existing accounts or connect a new account. Only after an account is selected the user can continue configuring other properties of the component.

The auth.service identifies the that will be used to authenticate the user to the service that the component uses. It must have the following format: [vendor]:[service]. The Appmixer engine looks up the auth.js file under that vendor and service category. auth.scope provides additional parameters to the authentication module. See the Authentication section for more details.

authentication module
Component Description
Connected Accounts
Flow
Inspector panel
Services, modules and components hierarchy
Google modules
Twilio service
Modules as separate apps.
A single app type of service.
Service manifest fields meaning.
Flow

authConfig

This section specifies which service should be used to search for additional configuration values.

{
	"authConfig": {
		"service": "appmixer:deepai"
	}
}

To set additional global values for a service, use the authConfig API. See the section for more information. The purpose of this property is to offer authentication settings for components/services that do not require user authentication. Consider that service. It is a service that requires authentication (API key), but you don't want your users to provide their API keys for it. You want to use your API key for all your users. You cannot use define the property in component.json, because that would tell Appmixer to show Connect Account button to users in the frontend.

This is not needed since version 4.2. Appmixer will look for service configuration for each component without auth section. More details in .

Service Configuration
deepai
auth
here

author

The author of the component. Example:

{
    "author": "David Durman <david@client.io>"
}

marker

The marker icon that can be added to the component in the UI to give some extra context. The most common use case is to display e.g. a "Beta" badge to tell the user that this component is in beta. The marker must be in the Data URI image format as described here: . image/png or image/svg+xml image types are recommended. The marker icon is displayed in the top right corner of the component shape. Example:

{
    "marker": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL..."
}
https://en.wikipedia.org/wiki/Data_URI_scheme
Beta badge

inPorts

The definition of the input ports of the component. It's an array of objects.

Each component can have zero or more input ports. If a component does not have any input ports, we call it a trigger. Input ports allow a component to be connected to other components. Input ports receive data from output ports of other connected components when the flow is running and the data is available. Each input port has a name and configuration that has the exact same structure as the configuration of properties, i.e. it has schema , inspector or source objects. The difference is that the user can use placeholders (variables) in the data fields that will be eventually replaced once the actual data is available. The placeholders (variables) can be entered by the user using the "variables picker" in the Designer UI inspector (see below). Example:

{
    "inPorts": [
        {
            "name": "message",
            "schema": {
                "type": "object",
                "properties": {
                    "body": { "type": "string" },
                    "phoneNumber": { "type": "string" }
                },
                "required": [ "phoneNumber" ]
            },
            "inspector": {
                "inputs": {
                    "body": {
                        "type": "text",
                        "group": "transformation",
                        "label": "Text message",
                        "index": 1
                    },
                    "phoneNumber": {
                        "type": "text",
                        "group": "transformation",
                        "label": "Phone number",
                        "index": 2
                    }
                },
                "groups": {
                    "transformation": {
                        "label": "Transformation",
                        "index": 1
                    }
                }
            }
        }
    ]
}

The message from the example looks like this in the raw form:

City: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[name]}}}
Humidity: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.humidity]}}}
Pressure: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.pressure]}}}
Temperature: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.temp]}}}

As you can see, the placeholders for variables use a special format that the Appmixer engine eventually replaces with real values that come from the GetCurrentWeather component once the data is available.

inPort.schema

inPort.inspector

inPort.source

Definition of the source of the variables or dynamic inspector that will be available in the designer UI for this input port.

inPort.variablesPipeline

This object allows you to control what variables will be available to this component in the UI and in the component receive() method. By default, variables are collected from all the components back in the chain of connected component. This might not be desirable in some cases. One can set scopeDepth to a number that represents the depth (levels back the the graph of connected components) used to collect variables. rawValue can be used to tell the engine not to resolve variable placeholders to their actual values but to treat variable names as values themselves. Example:

{
    "variablesPipeline": {
        "scopeDepth": 1,
        "rawValue": true
    }
}

inPort.maxConnections

Set the maximum number of links that can be connected to the input port. Maximum number of connections is infinite by default but in some applications, it might be desirable to set a limit on this, usually 1. The Appmixer Designer UI will not allow the user to connect more than maxConnections links to the input port.

Input Port Configuration using Variables

Definition of the schema of the data that the input port expects. Please see the section for more details.

Definition of the inspector UI for this input port. Please see the section for more details.

Properties
Properties

firePatterns

Fire patterns is an advanced configuration of a component that allows you to define when your component is ready to fire (ready to process input messages). Fire patterns can make the engine to hold input messages on components input ports until the pattern matches and then send the messages to the component in bulk. Fire patterns are defined as an array or a matrix. An example of fire patterns may look like this:

{
    "firePatterns": ['*', 1]
}

The fire pattern above is interpreted as follows: The component processes messages only if the first input port has zero or more messages waiting in the queue and at least one message waiting in the second input port queue. Another example can be a fire pattern:

{
    "firePatterns": [1, 1]
}

In this case, the component only processes messages if there is at least one message on each of its two input ports. A good example for this pattern is the Sum component:

The Sum component expects messages on both of its input ports before it can produce a sum of its inputs.

The following table lists all the possible fire pattern symbols:

Symbol

Description

*

(Any) The input port must have zero or more messages in the queue.

1

(Exists) The input port must have at least one message in the queue.

0

(Empty) The input port must have no message in the queue.

A

(All) The input port must have at least one message from all the connected components in the queue. This is a synchronization pattern that lets you specify that the component must wait for all the connected components to send a message before it can start processing. A typical example is a "Multiple-to-Single" join component. This component must wait for all the LoadCSV components to send a message before it can produce an SQL-like join schema.

Note that you can also define a set of fire patterns for a component, for example:

{
    "firePatterns": [
        ['*', 1],
        [1, 0]
    ]
}

When more fire patterns are used, there must be at least one fire pattern that matches before the component fires.

tick

When set to true, the component will receive signals in regular intervals from the engine. The tick() Component Virtual method will be called in those intervals (see Component Behaviour). This is especially useful for trigger-type of components that need to poll a certain API for changes. The polling interval can be set by the COMPONENT_POLLING_INTERVAL environment variable (for custom on-prem installations only). The default is 60000 (ms), i.e. 1 minute.

private

When set to true, the component will not be visible to the user.

properties

The configuration properties of the component. Note that unlike properties specified on input ports (described later on in the documentation), these properties cannot be configured by the user to use data coming from the components back in the chain of connected components. In other words, these properties can only use data that is known before the flow runs.

Configuration properties are defined using two objects schema and inspector.

properties.schema

{
    "properties": {
        "schema": {
            "properties": {
                "interval": {
                    "type": "integer",
                    "minimum": 5,
                    "maximum": 35000
                }
            },
            "required": [
                "interval"
            ]
        }
}

The JSON Schema gives you enough flexibility to describe your property types and the required format, possibly using regular expressions or other mechanisms. When the user fills in the forms in the Designer UI inspector to configure their components, the Designer automatically validates all inputs using the schema. If any of the properties are invalid, the Designer UI gives an immediate feedback to the user that they should correct their configuration:

properties.inspector

{
    "properties: {
        "inspector": {
            "inputs": {
                "interval": {
                    "type": "number",
                    "group": "config",
                    "label": "Interval (in minutes, min 5, max 35000)"
                }
            },
            "groups": {
                "config": {
                    "label": "Configuration",
                    "index": 1
                }
            }
        }
    }
}

As you can see, fields (e.g. interval in this case) are nested inside the inputs object and have the following properties:

  • type can be any of the built-in types. See below for more details. (Custom inspector fields are also possible for on-prem installations. See the Custom Inspector Fields page for more details.)

  • group is an identifier of an Inspector group this field belongs to. As you can see in the example above, you can have one or more custom groups (like config in this case) that you can define in the groups object. Groups will render in the Inspector UI in an accordion-like fashion. This is handy to organize your fields.

  • label is a short text that appears above your input field. This is a great place to tell your users what your field is.

Inspector built-in types:

text

A single line input field.

{
    "type": "text",
    "label": "Text message."
}

textarea

A multi-line text input field.

{
    "type": "textarea",
    "label": "A multi-line text message."
}

number

A numerical input field. Additional configuration includes min, max and step numbers.

{
    "type": "number",
    "label": "A numerical input.",
    "min": 1,
    "max": 10,
    "step": 1
}

select

A menu of options. Options are defined in the options array each item having content and value properties. Note that content can be HTML. You can optionally provide placeholder that is displayed if no option is selected. Default values can be defined with defaultValue. If you need one of the items to clear the value of the select input field, use { "clearItem": true, "content": "Clear" } as one of the objects in the options array.

multiselect

Similar to select type, multiselect defines options the user can choose from. The difference is that with multiselect, the user can select multiple options, not only one. The value stored in the flow descriptor is an array of values the user selected. Supported options are options and placeholder.

{
    "type": "multiselect",
    "options": [
        { "content": "one", "value": 1 },
        { "content": "two", "value": 2 },
        { "content": "three", "value": 3 }
    ],
    "placeholder": "-- Select something --",
    "label": "Multi Select box"
}

date-time

A date-time input field allows the user to select a date/time using a special date/time picker interface. The date-time input field can be configured to support different type of formats or modes (only date or date-time combination). The configuration is stored in the "config" object. The following table shows list of all the available options:

Option

Description

format

enableTime

Boolean. Enables time picker.

enableSeconds

Boolean. Enables seconds in the time picker.

maxDate

String representing the maximum date that a user can pick to (inclusive).

minDate

String representing the minimum date that a user can pick to (inclusive).

mode

Mode of the date/time picker. Possible values are "single", "multiple", or "range".

time_24hr

Boolean. Displays time picker in 24 hour mode without AM/PM selection when enabled.

weekNumbers

Boolean. Enables display of week numbers in calendar.

{
    "type": "date-time",
    "label": "Date",
    "config": {
        "enableTime": true
    }
}

toggle

A toggle input field allows the user to switch between true/false values.

{
    "type": "toggle",
    "label": "Toggle field"
}

color-palette

{
    "type": "color-palette",
    "label": "Color palette",
    "options": [
        { "value": "green", "content": "Green" },
        { "value": "yellow", "content": "Yellow" },
        { "value": "orange", "content": "Orange" },
        { "value": "red", "content": "Red" },
        { "value": "purple", "content": "Purple" }
    ]
}

select-button-group

A group of toggle buttons. Both single and multiple selection is allowed (can be controlled with the multi flag). Buttons are defined in the options array each item having value, content and icon properties.

{
    "type": "select-button-group",
    "label": "Select button group",
    "options": [
        { "value": "line-through", "content": "<span style=\"text-decoration: line-through\">S</span>" },
        { "value": "underline", "content": "<span style=\"text-decoration: underline\">U</span>" },
        { "value": "italic", "content": "<span style=\"font-style: italic\">I</span>" },
        { "value": "bold", "content": "<span style=\"font-weight: bold\">B</span>" }
    ]
}
{
    "type": "select-button-group",
    "label": "Select button group",
    "multi": true,
    "options": [
        { "value": "line-through", "content": "<span style=\"text-decoration: line-through\">S</span>" },
        { "value": "underline", "content": "<span style=\"text-decoration: underline\">U</span>" },
        { "value": "italic", "content": "<span style=\"font-style: italic\">I</span>" },
        { "value": "bold", "content": "<span style=\"font-weight: bold\">B</span>" }
    ]
}
{
    "type": "select-button-group",
    "label": "Select button group with icons",
    "multi": true,
    "options": [
        { "value": "cloud", "icon": "data:image/png;base64,iVBORw0KGgoAA..." },
        { "value": "diamond", "icon": "data:image/png;base64,iVBORw0KGgoAAAA..." },
        { "value": "oval", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUh..." },
        { "value": "line", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." },
        { "value": "ellipse", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEU..." }
    ]
}

expression

A multi-field type field that allows for definition of logical expressions (OR/AND) or dynamic field definitions. This field accepts a list of other inspector field types (text, textarea, number, toggle, ....) and renders a "field builder" UI that enables the user to dynamically create nested fields.

{
      "type": "expression",
      "label": "Filter expression",
      "levels": ["OR", "AND"],
      "exclusiveFields": ["myText"]
      "fields": {
          "myText": {
              "type": "text",
              "label": "Column",
              "required": true,
              "index": 1
          },
          "mySelect": {
              "type": "select",
              "label": "Filter action",
              "variables": false,
              "required": true,
              "options": [
                  { "content": "Equals", "value": "equals" },
                  { "content": "Not Equals", "value": "notEquals" }
              ],
              "index": 2
          },
          "myAnotherText": {
              "label": "Filter value",
              "type": "text",
              "defaultValue": "My Filter",
              "index": 3
          }
      ]
}

The value of this field has the following structure:

{
    "OR": [
        {
            "AND": [
                { "myText": "My column name", "mySelect": "My filter action", "myAnotherText": "My filter value" },
                { "myText": "Another column name", "mySelect": "Another filter action", "myAnotherText": "Another filter value" }
            ]
        },
        {
            "AND": [
                { "myText": "Alternative column", "mySelect": "Alternative action", "myAnotherText": "Alternative value" }
            ]
        }
    ]
}

Note that by specifying the levels option, you can define the nesting. Currently, maximum of 2 levels of nesting is supported. The common use case is to use just one level. In that case, set e.g. "levels": ["ADD"].

The exclusiveFields is an optional property which defines the fields that will use variables in an exclusive way. For example let's say that the component has variableA and variableB available for use in its fields. Now if the myText field is in exclusiveFields array that means that you can use each variable once across all the fields inside the expression groups. To clarify this further, imagine the following scenario configuring an expression type:

  1. Click the ADD button to create a second group

  2. Select variableA on the myText field inside the first group using the variables picker

  3. When opening the variables picker in myText field inside the second group, only variableB will be available, because variableA is already been used

filepicker

An input that allows selecting and uploading files from the user's computer. When clicked, it will open the browser's file selector, and the file selected will be uploaded to Appmixer and referenced on the input.

"inputs": {
    "fileId": {
        "type": "filepicker",
        "label": "Select file",
        "index": 1,
        "tooltip": "Pick a CSV file to import into the flow"
    }
}

googlepicker

Similar to the filepicker input, this one allows users to select files or folders on their Google Drive accounts. When clicked a Google Drive file picker is opened, showing the user's Google Drive content. When selecting a folder/file, the input value becomes an object which includes the Id of the folder/file which should be used on Google API calls to reference that asset.

"inputs": {
    "file": {
        "type": "googlepicker",
        "index": 1,
        "label": "File",
        "placeholder": "Choose a file...",
        "tooltip": "Choose a file to export."
    }
}

You can use googlepicker to pick folders instead of files:

"inputs": {
    "file": {
        "type": "googlepicker",
        "index": 1,
        "label": "Folder",
        "placeholder": "Choose a folder...",
        "tooltip": "Choose a folder.",
        "view": "FOLDERS"
    }
}

This input type needsappmixer.google.drive.GooglePicker component to be installed.

onedrivepicker

Similar to the googlepicker, this one allows users to select files or folders from their OneDrive accounts. When clicked, an OneDrive file picker is opened, showing the user's OneDrive content. When selecting a folder/file, the input value becomes an object which includes the id of the folder/file which should be used on OneDrive API calls to reference that asset.

"input": {
    "folder": {
        "type": "onedrivepicker",
        "index": 1,
        "label": "Folder",
        "placeholder": "Choose a folder...",
        "tooltip": "Choose a folder to upload the file to.",
        "view": "folders"
    }
}

The view property works similar to the same property on googlepicker. It can be used to determine what is shown on the picker. You can use 3 values: files, folder, all. As their names indicate, if select files, only files will be shown, if you select folder it will show only your folders and if you select all it will show both. This input type needs appmixer.microsoft.onedrive.OneDrivePicker component to be installed.

Conditional fields

There are some cases when you want to show input fields depending on other values in the inspector. This allows to a better UX for component configuration. For this we use the whenproperty in the field we want to be conditional:

"inputs": {
    "field1": {
        "type": "toggle",
        "label": "This input controls rendering of field2",
        "index": 1
    },
    "field2": {
        "when": { "eq": { "field1": true }}
        "type": "text",
        "label": "This field will be only rendered if field1 is set to true",
        "index": 2
    }
}

The when field has the following structure: { op: { field: comparisonValue }}.

  • op: Is the operand that will be used to determine if the condition holds true or false. The following operands are supported:

    • eq: Equality between the values.

    • equal: Equality between the values by deep comparison. Used for objects and arrays.

    • ne: Values are not equal.

    • regex: Check if the value in given field matches the regex in the comparisonValue.

    • text: Check if the value in the given field contains the string in the comparisonValue.

    • lt: Check if the value in the given field is less than the comparisonValue.

    • lte: Check if the value in the given field is less or equal than the comparisonValue.

    • gt: Check if the value in the given field is greater than the comparisonValue.

    • gte: Check if the value in the given field is greater or equal than the comparisonValue.

    • in: Check if the value in the given field is included on the given comparisonValue array.

    • nin: Check if the value in the given path is not included in the given comparisonValue.

  • field: The field that is used for comparison, there are several ways to reference the field:

    • field: The same form presented in the example. It will search the given fields in current input port fields.

    • properties/someProperty: Refer to a property inside component properties.

    • ./field: It will refer to sibling fields of the current field. Specially useful when working with expression types.

  • comparisonValue: The value used to compare the field against.

As it was mentioned, conditional fields also work with expression types, allowing to control the field rendering inside those expressions:

{
      "type": "expression",
      "label": "Filter expression",
      "levels": ["OR", "AND"],
      "fields": {
          "myText": {
              "type": "text",
              "label": "Column",
              "required": true,
              "index": 1
          },
          "conditionalField": {
              "when": { "eq": { "./myText": "Render" }}
              "type": "select",
              "label": "Filter action",
              "variables": false,
              "required": true,
              "options": [
                  { "content": "Equals", "value": "equals" },
                  { "content": "Not Equals", "value": "notEquals" }
              ],
              "index": 2
          }
      ]
}

properties.source

Sometimes the structure of the inspector is not known in advance and it cannot be hardcoded in the manifest. Instead, the inspector fields are composed dynamically based on the data received from an API. A good example is the google.spreadsheets.CreateRow component where the inspector renders fields representing columns fetched from the actual worksheet. For this to work, we can define the source property in the manifest that calls a component of our choosing in a so called "static" mode. For example:

{
       "source": {
           "url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
           "data": {
               "messages": {
                   "in": 1
               },
               "properties": {
                   "sheetId": "properties/sheetId",
                   "worksheet": "properties/worksheet"
               },
               "transform": "./transformers#columnsToInspector"
           }
       }
}

In the example above, we call the ListColumns component and we're interested in the output coming from the output port out.Since this is just a normal component, we need to transform the result into the inspector-like object, i.e.:

{
    inputs: { ... },
    groups: { ... }
}

We need to tell Appmixer where it can find the transformation function. For this we use the transform property which tells Appmixer to look for the transformers.js file inside the ListColumns/ directory. The transformer must return an object with a function named columnsToInspector that can look like this:

module.exports.columnsToInspector = (columns) => {

    let inspector = {
        inputs: {},
        groups: {
            columns: { label: 'Columns', index: 1 }
        }
    };

    if (Array.isArray(columns) && columns.length > 0) {
        columns.forEach((column, index) => {
            inspector.inputs[column[0]] = {
                type: 'text',
                group: 'columns',
                index: index + 1
            };
        });
    }
    return inspector;
};

properties.source.url

A special URL that identifies a component that should be called in a "static" mode. It has to be of the form /component/[vendor]/[service]/[module]/[component]. It should also contain outPort in the query string that point to the output port in which we're interested to receive data from. Example:

"/component/appmixer/google/spreadsheets/ListColumns?outPort=out"

properties.source.data.messages

Messages that will be sent to the input port of the component referenced by the properties.source.url. Keys in the object represent input port names and values are any objects that will be passed to the input port as messages.

properties.source.data.properties

Properties that will be used in the target component referenced by the properties.source.url. The target component must have these properties defined in its manifest file. The values in the object are references to the properties of the component that calls the target component in the static mode. For example:

{
    "properties": {
        "targetComponentProperty": "properties/myProperty"
    }
}

properties.source.data.transform

The transformation function used to transform the output of the target component. It should return an inspector-like object, i.e.:

{
    inputs: { ... },
    groups: { ... }
}

Example:

{
    "transform": "./transformers#columnsToInspector"
}

The transform function is pointed to be a special format [module_path]#[function], where the transformation module path is relative to the target component directory.

quota

{
     "quota": {
        "manager": "pipedrive",
        "resources": "requests",
        "scope": {
            "userId": "{{userId}}"
        }
    }
}

quota.manager

The name of the quota module where usage limit rules are defined.

quota.resources

One or more resources that identify rules from the quota module that apply to this component. Each rule in the quota module can have the resource property. quota.resources allow you to cherry-pick rules from the list of rules in the quota module that apply to this component. quota.resources can either be a string or an array of strings.

quota.scope

This scope instructs the quota manager to count calls either for the whole application (service) or per-user. Currently, it can either be omitted in which case the quota limits for this component apply for the whole application or it can be { "userId": "{{userId}}" } in which case the quota limits are counted per Appmixer user.

outPorts

The definition of the output ports of the component. It's an array of objects.

Components can have zero or more output ports. Each output port has a name and optionally an array options that defines the structure of the message that this output port emits. Without the options object, the user won't be able to see the possible variables they can use in the other connected components. For example, a component connected to the weather output port of our GetCurrentWeather component, can see the following variables in the variables picker:

An example of outPorts definition can look like this:

{
    "outPorts": [
        {
            "name": "weather",
            "options": [
                { "label": "Temperature", "value": "main.temp" },
                { "label": "Pressure", "value": "main.pressure" },
                { "label": "Humidity", "value": "main.humidity" },
                { "label": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
                { "label": "Sunset time (unix, UTC)", "value": "sys.sunset" },
                { "label": "City name", "value": "name" },
                { "label": "Weather description", "value": "weather[0].description" },
                { "label": "Weather icon code", "value": "weather[0].icon" },
                { "label": "Weather icon URL", "value": "weather[0].iconUrl" }
            ]
        }
    ]
}

outPort.maxConnections

Set the maximum number of outgoing links that can exist from the output port. Maximum number of connections is infinite by default but in some applications, it might be desirable to set a limit on this, usually 1. The Appmixer Designer UI will not allow the user to connect more than maxConnections links from the output port.

webhook

Component Configuration

schema is a JSON Schema definition () of the properties, their types and whether they are required or not. An example looks like this:

Invalid Inspector field
Configuration Overview

inspector tells the Designer UI how the input fields should be rendered. The format of this definition uses the . Example:

String representing the format of the date/time. Please see the moment.js library documentation for all the available tokens: .

A menu of colors. Colors are defined in the options array each item having content and value properties, where values must be a color in any of the (named-color, hex-color, rgb() or hsl()).

Configuration of the used for this component. Quotas allow you to throttle the firing of your component. This is especially useful and many times even necessary to make sure you don't go over limits of the usage of the API that you call in your components. Quota managers are defined in the quota.js file of your service/module. Example:

Variables Picker

Set webhook property to true if you want your component to be a "webhook" type. That means that context.getWebhookUrl() method becomes available to you inside your component virtual methods (such as receive()). You can use this URL to send HTTP POST requests to. See the section, especially the for details and example.

http://json-schema.org
Rappid Inspector definition format
CSS color formats
quota manager
https://momentjs.com/docs/#/parsing/string-format/
Behaviour

state

Set state property to { persistent: true } to tell the engine not to delete component state when flow is stopped. See for more information.

Dependencies

Component NodeJS modules can use 3rd party libraries which are defined in the standard package.json file. An example:

{
    "name": "appmixer.twilio.sms.SendSMS",
    "version": "1.0.0",
    "private": true,
    "main": "SendSMS.js",
    "author": "David Durman <david@client.io>",
    "dependencies": {
        "twilio": "^2.11.0"
    }
}

The package.json file from the example above tells the Appmixer engine to load the twilio library that the appmixer.twilio.sms.SendSMS component requires for its operation.

More information on the package.json file can be found at .

https://docs.npmjs.com/files/package.json

localization

An optional object containing localization strings. For example:

{
    "name": "appmixer.twilio.sms.SendSMS",
    "author": "David Durman <david@client.io>",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVp...",
    "description": "Send SMS text message through Twilio.",
    "private": false,
    "auth": {
        "service": "appmixer:twilio"
    },
    "outPorts": [
        {
            "name": "sent",
            "options": [
                { "label": "Message Sid", "value": "sid" }
            ]
        }
    ],
    "inPorts": [
        {
            "name": "message",
            "schema": {
                "type": "object",
                "properties": {
                    "body": { "type": "string" },
                    "to": { "type": "string" },
                    "from": { "type": "string" }
                },
                "required": [
                    "from", "to"
                ]
            },
            "inspector": {
                "inputs": {
                    "body": {
                        "type": "text",
                        "label": "Text message",
                        "tooltip": "Text message that should be sent.",
                        "index": 1
                    },
                    "from": {
                        "type": "select",
                        "label": "From number",
                        "placeholder": "Type number",
                        "tooltip": "Select Twilio phone number.",
                        "index": 2,
                        "source": {
                            "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                            "data": {
                                "transform": "./transformers#fromNumbersToSelectArray"
                            }
                        }
                    },
                    "to": {
                        "type": "text",
                        "label": "To number",
                        "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                        "index": 3
                    }
                }
            }
        }
   ],
   "localization": {
       "cs": {
           "label": "Pošli SMS",
           "description": "Pošli SMS pomocí Twilia",
           "inPorts[0].name": "Zpráva",
           "inPorts[0].inspector.inputs.body.label": "Textová zpráva",
           "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
           "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo",
           "outPorts[0].name": "Odesláno",
           "outPorts[0].options[sid].label": "Sid zprávy"
       },
       "sk": {
           "label": "Pošli SMS",
           "description": "Pošli SMS pomocou Twilia",
           "inPorts[0].name": "Správa",
           "inPorts[0].inspector.inputs.body.label": "Textová správa",
           "inPorts[0].inspector.inputs.from.label": "číslo volajúceho",
           "outPorts[0].name": "Odoslané",
           "outPorts[0].options[sid].label": "Sid správy"
       }
   }
}

For more information about component localization, refer to the section.

Authentication

Appmixer provides an easy way to configure authentication modules. Most of the time, it's just about configuring a 3rd party service provider URLs for authentication, requesting access tokens and token validation.

Authentication Module Structure

Each authentication module is a NodeJS module that returns an object with type and definition properties. type can be either apiKey, oauth (for OAuth 1) and oauth2 (for OAuth 2). definition is either an object or a function (useful in cases where there's a code that you need to run dynamically).

type

The type of the authentication mechanism. Any of apiKey, oauth and oauth2.

definition

The definition of the authentication mechanism specific to the API service provider. An object or a function. This differs significantly between authentication mechanisms.

If the definition property is specified as a function, it that case it has one argument context. It is an object that contains either consumerKey, consumerSecret (OAuth 1) or clientId, clientSecret (OAuth 2) and it always contains callbackUrl property. This will be shown later in the examples.

Authentication mechanisms

As it was mentioned in the beginning, Appmixer authentication supports three mechanisms: API Key, OAuth 1 and OAuth 2. Each of them has a different definition.

function, object or a URL string

In the following examples we will show how a particular property of the definition object can be specified as a function, object or string. Let's demonstrate that on a requestProfileInfo property, that one is common for all authentication mechanisms.

// or any library you want to perform API requests
const request = require('request-promise');

module.exports = {

    // there will be some other properties based on authenication mechanism
    // defined prior requestProfileInfo
    
    definition: {
    
        // requestProfileInfo can be defined as a function. In this case, you
        // can do whatever you need in here and return object with user's
        // profile information (in promise)
        requestProfileInfo: async context => {
    
            // curl https://mydomain.freshdesk.com/api/v2/agents/me \
            //  -u myApiKey:X'
            return request({
                method: 'GET',
                url: `https://${context.domain}.acme.com/api/v2/agents/me`,
                auth: {
                    user: context.apiKey  // 'context' will be explained later
                },
                json: true
            });
        },
        
        // or you can specify it as an object. In this case, the object follows
        // the 'request' javascript library options.
        requestProfileInfo: {
            method: 'GET',
            url: 'https://acme.com/get-some-records/app_id={{appId}}',
            headers: {
                'Authorization': 'Basic {{apiKey}}'  // {{apiKey}} explained later
            }
        },
        
        // the last way is to specify just the URI. In this case Appmixer
        // will perform GET request to that URI.
        requestProfileInfo: 'https://acme.com/get-profile-info?apiKey={{apiKey}}'    
    }
}

API key

This is the most basic type of third party authentication, since you only need to provide a sort of ID and a key provided by the app in concern. In order to use this mechanism, type property must be set to apiKey. Here is an example from Freshdesk components:

module.exports = {

    type: 'apiKey',

    definition: {

        tokenType: 'authentication-token',

        auth: {
            domain: {
                type: 'text',
                name: 'Domain',
                tooltip: 'Your Freshdesk subdomain - e.g. if the domain is <i>https://example.freshdesk.com</i> just type <b>example</b> inside this field'
            },
            apiKey: {
                type: 'text',
                name: 'API Key',
                tooltip: 'Log into your Freshdesk account and find <i>Your API Key</i> in Profile settings page.'
            }
        },

        accountNameFromProfileInfo: 'contact.email',

        requestProfileInfo: async context => {

            // curl https://mydomain.freshdesk.com/api/v2/agents/me \
            //  -u myApiKey:X'
            return request({
                method: 'GET',
                url: `https://${context.domain}.freshdesk.com/api/v2/agents/me`,
                auth: {
                    user: context.apiKey,
                    password: 'X'
                },
                json: true
            });
        },

        validate: async context => {

            // curl https://mydomain.freshdesk.com/api/v2/agents/me \
            //  -u myApiKey:X'
            const credentials = `${context.apiKey}:X`;
            const encoded = (new Buffer(credentials)).toString('base64');
            await request({
                method: 'GET',
                url: `https://${context.domain}.freshdesk.com/api/v2/agents/me`,
                headers: {
                    'Authorization': `Basic ${encoded}`
                }
            });
            // if the request doesn't fail, return true (exception will be captured in caller)
            return true;
        }
    }
};

Now we explain the fields inside the definition object:

auth (object)

This is basically the definition for the form that will be displayed to the user to collect the data required by the third party application. Freshdesk requires the domain name and the API key in order to authenticate Appmixer. So we define two fields representing those items and we define the label and tooltip that will appear in the form for each field. In this case, the auth definition will make Appmixer to render a form like this:

While this field is optional, is recommended for a better UX. This field can be a function which returns a promise, object or a string. It is about calling an endpoint from the third party app which returns the current user info. This will be used to display a name for this account in Appmixer.

This field is the path in the object returned by requestProfileInfo that points to the value that will be used as account name. Following the example, the object returned by requestProfileInfo would have an structure like this:

{
    contact: {
        email: 'appmixer@example.com',
        name: 'Appmixer example',
        // More properties here...
    }
    // There can be more properties here as well...
}

We want to use the email to identify the Freshdesk accounts in Apmmixer, so we set accountNameFromProfileInfo as contact.email.

If requestProfileInfo is not defined, the auth object will be used instead. The account name will be the resolved value for the property specified by accountNameFromProfileInfo.

Similar to requestProfileInfo. This is used to validate if the authentication data entered by the user is correct. For this purpose you can call any endpoint that requires authentication, you can even use the same endpoint as requestProfileInfo. If the data is correct, this function should resolve to any non-false value. Otherwise throw an error or resolve to false. You can define validate as an object. In that case, that object has the same structure as object passed into request library. In that case it will look like this:

'use strict';

module.exports = {

    type: 'apiKey',

    definition: {

        tokenType: 'apiKey',

        accountNameFromProfileInfo: 'appId',

        auth: {
            appId: {
                type: 'text',
                name: ' APP ID',
                tooltip: 'Log into your account and find Api key.'
            },
            apiKey: {
                type: 'text',
                name: 'REST API Key',
                tooltip: 'Found directly next to your App ID.'
            }
        },

        // In the validate request we neet the appId and apiKey specified by user.
        // All properties defined in the previous 'auth' object will be
        // available in the validate call. Just use {{whatever-key-from-auth-object}}
        // anywhere in the next object. Appmixer will replace these with the
        // correct values.
        validate: {
            method: 'GET',
            url: 'https://acme.com/get-some-records/app_id={{appId}}',
            headers: {
                'Authorization': 'Basic {{apiKey}}'
            }
        }
    }
};

OAuth 1

In order to use this mechanism, type property must be set to oauth. Here is an example from Trello components:

'use strict';
const OAuth = require('oauth').OAuth;
const Promise = require('bluebird');

module.exports = {

    type: 'oauth',

    // In this example, 'definition' property is defined as a function. 
    definition: context => {

        let trelloOauth = Promise.promisifyAll(new OAuth(
            'https://trello.com/1/OAuthGetRequestToken',
            'https://trello.com/1/OAuthGetAccessToken',
            context.consumerKey,
            context.consumerSecret,
            '1.0',
            context.callbackUrl,
            'HMAC-SHA1'
        ), { multiArgs: true });

        return {

            accountNameFromProfileInfo: 'id',

            authUrl: context => {

                return 'https://trello.com/1/OAuthAuthorizeToken' +
                    '?oauth_token=' + context.requestToken +
                    '&name=AppMixer' +
                    '&scope=read,write,account' +
                    '&expiration=never';
            },

            requestRequestToken: () => {

                return trelloOauth.getOAuthRequestTokenAsync()
                    .then(result => {
                        return {
                            requestToken: result[0],
                            requestTokenSecret: result[1]
                        };
                    });
            },

            requestAccessToken: context => {

                return trelloOauth.getOAuthAccessTokenAsync(
                    context.requestToken,
                    context.requestTokenSecret,
                    context.oauthVerifier
                ).then(result => {
                    return {
                        accessToken: result[0],
                        accessTokenSecret: result[1]
                    };
                });
            },

            requestProfileInfo: context => {

                return trelloOauth.getProtectedResourceAsync(
                    'https://api.trello.com/1/members/me',
                    'GET',
                    context.accessToken,
                    context.accessTokenSecret
                ).then(result => {
                    if (result[1].statusCode !== 200) {
                        throw new Error(result[1].statusMessage);
                    }
                    result = JSON.parse(result[0]);
                    // get rid of limits for now
                    // may and will contain keys with dots - mongo doesn't like it
                    delete result.limits;
                    return result;
                });
            },

            validateAccessToken: context => {

                return trelloOauth.getProtectedResourceAsync(
                    'https://api.trello.com/1/tokens/' + context.accessToken,
                    'GET',
                    context.accessToken,
                    context.accessTokenSecret
                ).then(result => {
                    if (result[1].statusCode === 401) {
                        throw new context.InvalidTokenError(result[1].statusMessage);
                    }
                    if (result[1].statusCode !== 200) {
                        throw new Error(result[1].statusMessage);
                    }

                    result = JSON.parse(result[0]);
                    if (result['dateExpires'] === null) {
                        return;
                    }
                    throw new context.InvalidTokenError('Invalid token.');
                });
            }
        };
    }
};

Note that in this case the definition is a function instead of an object, but it still returns an object with the items needed for OAuth 1 authentication, similar to API key authentication. Now we explain the fields from definition object:

OAuth 2

The latest OAuth protocol and industry-standard, OAuth 2.0 improved many things from the first version in terms on usability and implementation, while maintaining a high degree of security. It is easier to implement in Appmixer as well. In order to use this mechanism, type property must be set to oauth2. Here is an example from Asana auth.js:

'use strict';
const request = require('request-promise');

module.exports = {

    type: 'oauth2',

    // function definition is used in this case because of the 'profileInfo'
    // property. It is set in the 'requestAccessToken' function and then
    // later returned in 'requestProfileInfo' function.
    definition: () => {

        let profileInfo;

        return {

            accountNameFromProfileInfo: context => {

                return context.profileInfo['email'] || context.profileInfo['id'].toString();
            },

            authUrl: 'https://app.asana.com/-/oauth_authorize',

            requestAccessToken: context => {

                // don't put params into post body, won't work, have to be in query
                let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
                    'grant_type=authorization_code&code=' + context.authorizationCode +
                    '&redirect_uri=' + context.callbackUrl +
                    '&client_id=' + context.clientId +
                    '&client_secret=' + context.clientSecret;

                return request({
                    method: 'POST',
                    url: tokenUrl,
                    json: true
                }).then(result => {
                    profileInfo = result['data'];
                    let newDate = new Date();
                    newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
                    return {
                        accessToken: result['access_token'],
                        refreshToken: result['refresh_token'],
                        accessTokenExpDate: newDate
                    };
                });
            },

            requestProfileInfo: () => {

                return profileInfo;
            },

            refreshAccessToken: context => {

                // don't put params into post body, won't work, have to be in query
                let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
                    'grant_type=refresh_token&refresh_token=' + context.refreshToken +
                    '&redirect_uri=' + context.callbackUrl +
                    '&client_id=' + context.clientId +
                    '&client_secret=' + context.clientSecret;

                return request({
                    method: 'POST',
                    url: tokenUrl,
                    json: true
                }).then(result => {
                    profileInfo = result['data'];
                    let newDate = new Date();
                    newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
                    return {
                        accessToken: result['access_token'],
                        accessTokenExpDate: newDate
                    };
                });
            },

            validateAccessToken: {
                method: 'GET',
                url: 'https://app.asana.com/api/1.0/users/me',
                auth: {
                    bearer: '{{accessToken}}'
                }
            }
        };
    }
};

Notice that there is no requestRequestToken method in OAuth 2, but we there is the requestAccessToken used to get the token and refreshAccessToken method, which is used by Appmixer to refresh the access tokens. Now we explain the definition object properties:

Similar to OAuth 1, we should provide the authentication url for the third party app. However, due to the different authentication flows supported by OAuth 2, the way this is defined may vary according to the third party implementation. If the OAuth 2 implementation is standard, you can define the authUrl with just a string like:

authUrl: 'https://www.dropbox.com/oauth2/authorize'

Standard means, there is a response_type parameter set to code, then there is the client_id, redirect_uri, state and scope parameter. If the OAuth 2 implementation requires any other parameters (or the standard ones use different names), then you have to define this property as a function and provide all the additional parameters. The same logic applies to the following property requestAccessToken.

This function should return a promise with an object which contains accessToken, refreshToken (optional, some OAuth 2 implementations do not have refresh tokens) and accessTokenExpDate or expires_in (also options if the implementation does not have tokens that expire). Inside this function we call the endpoint which handles out the access tokens for the application. Inside this function you have access to context properties you need: clientId, clientSecret, callbackUrl and authorizationCode.

Part of the OAuth 2 specification is the ability to refresh short lived tokens via a refresh token that is issued along with the access token. This function should call the refresh token endpoint on the third party app and resolve to an object with accessToken and accessTokenExpDate properties, as shown in the example. You have access to context properties clientId, clientSecret, callbackUrl and refreshToken.

There are more ways the OAuth 2 can be implemented. It always depends on the third party server and its OAuth 2 implementation.

This is an example of Dropbox auth.js module:

'use strict';

module.exports = {

    type: 'oauth2',

    definition: {

        accountNameFromProfileInfo: 'email',

        authUrl: 'https://www.dropbox.com/oauth2/authorize',

        requestAccessToken: 'https://api.dropbox.com/oauth2/token',

        requestProfileInfo: {
            'method': 'POST',
            'uri': 'https://api.dropboxapi.com/2/users/get_current_account',
            'headers': {
                'authorization': 'Bearer {{accessToken}}'
            }
        }
    }
};

It can be that simple, if the third party API implements OAuth 2 according to standards. And in this Dropbox case, their OAuth 2 implementation does not contain refresh tokens.

Sometimes the OAuth 2 needs a scope or a different scope delimiter. Here is a full example of Microsoft authentication module with such features:

'use strict';
const TENANT = 'common';

module.exports = {

    type: 'oauth2',

    definition: {

        scope: ['offline_access', 'user.read'],

        scopeDelimiter: ' ',

        authUrl: `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/authorize`,

        requestAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',

        refreshAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',

        accountNameFromProfileInfo: 'displayName',

        emailFromProfileInfo: 'mail',

        requestProfileInfo: 'https://graph.microsoft.com/v1.0/me',

        validateAccessToken: {
            method: 'GET',
            url: 'https://graph.microsoft.com/v1.0/me',
            auth: {
                bearer: '{{accessToken}}'
            }
        }
    }
};

scope

String or an array of strings.

scopeDelimiter

String. The default one is , and you can change it to ' ' for example.

Setting OAuth 1,2 secrets

The OAuth applications need some secrets: consumerKey, consumerSecret (OAuth 1), or clientId, clientSecret (OAuth 2).

Another way is the credentials.json file which has to be located where your auth.js file is.

Then when you pack and publish your component archive, Appmixer will read the file and store the values into Mongo DB. Later it will use them to create the context object for your auth.js module.

Components that require authentication from the user must implement an authentication module for the service/module they belong to. The authentication module must be named auth.js and must be stored under either the service or module directory (i.e. [vendor]/[service]/auth.js or [vendor/[service]/[module]/auth.js. Appmixer currently supports three types of authentication mechanisms that are common for today's APIs: , and .

Authentication form for Freshdesk, defined by auth object

The values introduced by the user will be exposed in the object with the same keys as in the auth object. In this case we will be able to access the values as context.domain and context.apiKey.

requestProfileInfo () (optional)

accountNameFromProfileInfo ()

validate ()

accountNameFromProfileInfo ()

Works exactly the same way as described on section.

requestRequestToken ()

This must be a function (or an object, or just a string URL as explained ) that returns a promise which must resolve to an object containing requestToken and requestTokenSecret the same way that is showed on the example. Those are needed to get the access token and become exposed by the context - context.requestToken and context.requestTokenSecret.

requestAccessToken ()

This must be a function (or an object, or just a string URL as explained ) that returns a promise which must resolve to an object containing accessToken and accessTokenSecret the same way that is showed on the example. Usually you will be using the requestToken and requestTokenSecret inside this function as they are required by the OAuth 1 flow in this step. Similarly to requestRequestToken function, accessToken and accessTokenSecret will become exposed by the context - context.accessToken and context.accessTokenSecret.

authUrl ()

URL returning auth URL. Appmixer will then use this URL to redirect the user to proper login page. The requestToken is available in the context. The example shows the authUrl declaration using the token provided by the context.

requestProfileInfo () (optional)

Works exactly the same way as described in the section.

validateAccessToken ()

This property serves the same purpose as property in the API Key mechanism. This is used by Appmixer to test if the access token is valid and accepted by the third party app. You have access to context.accessToken and context.accessTokenSecret to make authenticated requests. If the token is valid, this function should resolve to any non-false value. Otherwise throw an error or resolve to false.

accountNameFromProfileInfo ()

Works exactly the same way as described in the section.

authUrl ()

requestAccessToken ()

requestProfileInfo () (optional)

Works exactly the same way as described in the section.

refreshAccessToken ()

validateAccessToken ()

Has the exact same purpose as the same method int the .

You can use the to do that. This is the preferable way.

credentials.json
API key
OAuth 1
OAuth 2
Backoffice
function, object or string
function or string
function, object or string
function or string
API Key
function, object or string
here
function, object or string
here
function, object or string
Function, object or a string
function, object or string
API Key
function, object or string
validate
function or string
API Key
function, object or string
function, object or string
function, object or string
API Key
function, object or string
function, object or string
OAuth 1

Custom Inspector Fields

Defining Custom Inspector Fields

Option

Default value

Description

readOnly

false

Set read only mode on the inline editor. Available values are true, false, "nocursor" (similar to true but no cursor is blinking).

singleVariable

false

Allow only a single variable to be selected in the variable picker, i.e. selecting another variable will replace the current inline editor content.

clearButton

true

Show clear button at the right edge of the inline editor allowing the user to switch back to your custom field.

Note that the definition is an Appmixer specific wrapper around a Vue JS component. The basic functions and properties you can use are:

  • this.invalid computed property is a boolean value that tells the field whether its value is valid or not. An example is a required field that is empty. In that case this.invalid will be true.

  • Properties propagated by Appmixer are (properties that can be accessed via this keyword, e.g. this.value):

props: { context: { type: Object }, value: { type: null, required: false }, errors: { type: Array, default: () => ([]) }, disabled: { type: Boolean, default: false } }

The context object contains contextual data from the component and the current input. It has the following structure: context: { componentId: { type: String }, inputManifest: { type: Object }, componentManifest: { type: Object }, descriptorPath: { type: String }, variables: { type: Object } }

Basic example

Here's a very basic example of a custom inspector field with just one HTML input element. Note how we use the Vue JS template definition. Also note that we need to call the change() method to propagate the value back to the flow descriptor (i.e. to save the flow with the new value of the field when the user makes a change):

appmixer.registerInspectorField('your-custom-text-input', {
    template: `
        <div class="your-custom-text-input">
            <label>Name</label>
            <input :value="value" @change="change($event.target.value)">
        </div>
    `
});

Advanced example

Here's another example of a custom inspector field that accepts a value that represents first and last name of a person in one string. The inspector field renders two HTML inputs, one for first name and one for last name. The returned value (the one stored back to the flow descriptor) is again one string. Note how we call this.change(value) to propagate value of the inspector field to the flow descriptor (i.e. to save the flow with a new value for this field):

appmixer.registerInspectorField('your-custom-input-type', {
    template: `
        <div class="your-custom-input-field">
            <label>Firstname</label>
            <input v-model="firstname" @change="onChange">
            <label>Lastname</label>
            <input v-model="lastname" @change="onChange">
        </div>
    `,
    watch: {
        value: {
            immediate: true,
            handler(value = '') {
                this.firstname = value.split(' ')[0] || '';
                this.lastname = value.split(' ')[1] || '';
            },
        },
    },
    data() {
        return {
            firstname: '',
            lastname: '',
        };
    },
    methods: {
        onChange() {
            const name = [
                this.firstname.trim(),
                this.lastname.trim(),
            ].join(' ');
            this.change(name);
        },
    },
});

To style your inspector field, just use regular CSS in your application, e.g:

.your-custom-input-field {
    font: 14px 'Segoe UI', Verdana, sans-serif;
}
.your-custom-input-field label {
    display: block;
    margin: 10px 0 5px;
    color: #888;
}
.your-custom-input-field input {
    display: block;
    padding: 0.5em;
    border: solid 1px #e8e8e8;
    border-radius: 5px;
}

Behaviour

Components receive incoming messages, process them, and generate outgoing messages. The way messages are processed is called component behaviour. It defines what components do internally and how they react to inputs.

Components are implemented as NodeJS modules that return an object with a set of methods (Component Virtual Methods) that the Appmixer engine understands. Let's start with a simple example, a SendSMS component that has one input port (message), no output ports and its purpose is to send an SMS using the Twilio API.

Component Virtual Methods

As was mentioned in the previous paragraph, components are simple NodeJS modules that can implement a certain set of methods the Appmixer engine understands. The one most important method is the receive() method. This method is called by the engine every time messages are available on the input ports and the component is ready to fire (fire patterns match). The method must return a promise that when resolved, acknowledges the processing of the input messages. If the promise is rejected, the Appmixer engine automatically re-tries to send the messages again in the next turn (with some delay).

Messages that have been rejected 30-times are put in a special internal "dead-letter" queue and never returned to the flow for processing again. They can only be recovered manually by the Administrator.

For trigger-type of components, the most important virtual methods to remember is tick() and start().

Context

All virtual methods have one argument, the context. The context contains all the information you need to process your messages and send new messages to the output ports.

Input/Output message(s)

context.messages

(applies to receive())

Incoming messages. An object with keys pointing to the input ports. Each message has a content property that contains the actual data of the message after all variables have been replaced. For example:

Remember, if before running the flow, the input port message was defined in the Inspector using variables:

where the flow descriptor would contain something like this:

the context.messages object contains the result of replacing variables with actual data that was sent through the output port of the connected component, i.e.

Each message also contains the correlation ID in the context.messages.myInputPort.correlationId property.

correlationId is a "session ID" that associates all the messages in one pass through the flow. Every time a trigger component sends a message to the flow (e.g. webhook, timer, ...) and the message does not have a correlation ID yet, the Appmixer engine assigns a new correlation ID to the message. This correlation ID is then copied to all the messages that were generated as a reaction to the original trigger message.

async context.sendJson(messageContent, outPort)

A method on the context object that you should call when you want to emit a message on one of the components output ports. The first argument can be any JSON object and the second argument is the name of an output port. The function returns a promise that has to be either returned from the receive() , tick() or start() methods or awaited.

Authentication

context.auth

The authentication object. It contains all the tokens you need to call your APIs. The authentication object contains properties that you defined in the auth object in your Authentication module (auth.js) for your service. For example, if our authentication module for our service (auth.js) looks like this:

we can use the context.auth.accountSID and context.auth.authenticationToken in the component virtual methods:

Backoffice configuration

context.config

When you configure your service/module in the Backoffice, you can access those values in the context.auth or context.config objects. context.config is just an alias to the original context.auth.

Properties

context.properties

The configuration properties of the component. This corresponds to the properties object from the component manifest file. For example, if our component defines the following properties in the manifest file:

context.properties.fromNumber will contain the value the user entered in the Designer UI Inspector:

Component State

context.state

A persistent state of the component. Sometimes you need to store data for the component that must be available across multiple receive() calls for the same component instance. If you also need the data to be persistent when the flow is stopped and restarted again, set the state: { peristent: true } property in your component manifest. context.state is a simple object with keys mapped to values that are persistently stored in DB. This object is loaded on-demand in each receive() call. It is not recommended to store large amounts of data here. Example:

The context.state is especially useful for trigger-type of components when polling an API for changes to store the ID of the latest processed item from the API.

The context.state object should not be used to store large amounts of data. The state is loaded with each received message on a component input port. The maximum limit is 16MB but storing such large objects will slow down the processing of the component input messages.

async context.loadState()

Load the component's state from DB. The Component's state is loaded just before the component is triggered and the state is available it context.state, but there are cases when a component needs to reload the state from the DB.

async context.saveState(object)

Save an updated state object. See context.state for details. The function returns a promise that resolves if storing of the state was successful and rejects otherwise.

async context.stateSet(key, value)

Set a state key to hold the value. key must be a string. value can be any JSON object.

async context.stateGet(key)

Get a state value stored under key.

async context.stateUnset(key)

Remove a value under key.

async context.stateClear()

Clears the entire state.

Service State

This is similar to the component state, but this state is available to all components in the module.

async context.service.loadState()

Load the state from the DB.

async context.service.stateSet(key, value)

Set a state key to hold the value. key must be a string. value can be anything that can be stored in Mongo DB.

async context.service.stateGet(key)

Get a state value stored under key.

async context.service.stateUnset(key)

Remove a value under key.

async context.service.stateClear()

Clears the entire state.

async context.service.stateAddToSet(key, value)

Add value into a Set stored under key.

async context.service.stateRemoveFromSet(key, value)

Remove value from Set stored under key.

Files

async context.saveFile(fileName, mimeType, buffer)

Save a file to the Appmixer file storage. This function returns a promise that when resolved gives you an UUID that identifies the stored file (this is different from the file Mongo ID). You can pass this ID through your flow (send it to an output port of your component) so that later components can load the file from the Appmixer storage using the file ID.

async context.saveFileStream(fileName, stream)

Save a file to the Appmixer file storage. The function returns a Promise that resolves with the ID of the stored file. This is a more efficient and recommended version of context.saveFile(name, mimeType, buffer).

async context.getFileInfo(fileId)

Returns a promise, which when resolved returns the file information (name, length, content type...). For backward compatibility, fileId can be either Mongo ID or UUID.

Example:

async context.loadFile(fileId)

Load a file from the Appmixer file storage. For backward compatibility, fileId can be either Mongo ID or UUID. The function returns a promise that when resolved, gives you the file data as a Buffer.

context.readFileStream(fileId)

This method has been deprecated. Use context.getFileReadStream instead. Read a file stream from the Appmixer file storage. The function returns a NodeJS read stream that you can e.g. pipe to other, write streams (usually to a request object when uploading a file to a 3rd party API). This is a more efficient and recommended version of context.loadFile(fileId). fileId must be Mongo ID.

async context.getFileReadStream(fileId)

Read a file stream from the Appmixer file storage. The function returns a Promise, which when resolved, returns a NodeJS read stream that you can e.g. pipe to other, write streams (usually to a request object when uploading a file to a 3rd party API). This is a more efficient and recommended version of context.loadFile(fileId). This method is backward compatible, sofileId can be either be Mongo ID or UUID.

async context.removeFile(fileId)

Remove a file from the Appmixer file storage. The function returns a promise. fileId can be either Mongo ID or UUID.

Webhook

context.getWebhookUrl()

Get a URL that you can send data to with HTTP POST or GET. When the webhook URL is called, the receive() method of your component is called by the engine with context.messages.webhook object set and context.messages.webhook.content.data contains the actual data sent to the webhook URL:

Note: The context.getWebhookUrl() is only available if you set webhook: true in your component manifest file (component.json). This tells the engine that this is a "webhook"-type of component.

The full context.messages.webhook object contains the following properties:

async context.response(body, statusCode, headers)

Send a response to the webhook HTTP call. When you set your component to be a webhook-type of component (webhook: true in your component.json file), context.getWebhookURL() becomes available to you inside your component virtual methods. You can use this URL to send HTTP POST requests.

When a request is received by the component, the context.messages.webhook.content.data contains the body of your HTTP POST call. In order to send a response to this HTTP call, you can use the context.response() method. See context.getWebhookUrl() for details and examples.

Miscellaneous

async context.setTimeout(messageContent, delay)

Set a timer that causes the component to receive messageContent in the receive() method in the special context.messages.timeout.content object. delay is the time, in milliseconds, the timer should wait before sending the messageContent to the component itself. Note that this is especially useful for any kind of scheduling components. Note that the context.setTimeout() function works in a cluster environment as opposed to using the global setTimeout() JavaScript function. For example, a component that just "slows down" incoming messages before sending them to its output port, waiting e.g. 5 minutes, can look like this:

Note that you can also access the correlation ID of the timeout message which can be useful in some scenarios. The correlation ID is available in the context.messages.timeout.correlationId property.

async context.callAppmixer(request)

async context.stopFlow()

Stop the running flow. Example:

context.componentId

The ID of the component.

context.flowId

The ID of the flow the component lives in.

context.flowDescriptor

The flow descriptor of the flow in which the component instance lives. This allows you to access configuration of the entire flow within your component virtual methods. To get the configuration of the component itself, you can use context.flowDescriptor[context.componentId]. Note that this is normally not necessary since you can access the properties of the components by context.properties and the current input message by context.messages.myInPort.content but can be useful in some advanced scenarios.

context.customFields

async context.loadVariables()

Allows you to load variables in your component definition. Variables are data available from components connected back in the chain. loadVariables() returns a promise that resolves to an array that looks like this:

The return array has as many items as there are other components connected to this component.

Example:

async context.log(object)

Log message into InsightsLogs. The argument has to be an object.

Example:

And the object can be seen in logs (and InsightsLogs as well):

async context.lock(lockName, options)

This method allows components to create a lock. This is useful when creating a mutually exclusive section inside the component's code. Such a thing can be achieved in Appmixer using either quota (you can define quota the way that only one receive call can be executed at a time) or using this lock. This method returns the lock instance. Don't forget to call lock.unlock() when you're done. Otherwise, the lock will be released after TTL.

lockName string will be prefixed with vendor.service:. If a component type is appmixer.google.gmail.NewEmail, then the lockName will be prefixed with appmixer.google:. This allows you to create a lock that is shared among all components within a service and prevents possible collisions between components from different vendors or services.

The first parameter is required, the second (options) is optional with the following optional properties:

  • ttl, number, 20000 by default (ms)

  • retryDelay, number, 200 by default (ms)

  • maxRetryCount, number, 30 by default

Example:

Error Handling

Every function a component implements may throw an exception (or return rejected promise).

receive(context)

Sometimes you, as a developer of a component, know that there is no point in retrying a message. It would fail, again and again, 30 times. In such a case, you can tell Appmixer to cancel the message.

tick(context)

If a tick function throws an exception, such exception is logged (and visible in Insights) and that is it. Appmixer will trigger this function again in the future.

start(context)

Appmixer won't start a flow if any component in the flow throws an exception in the start function. Such error will be logged and visible in Insights.

stop(context)

Appmixer will stop the flow even when a component in the flow throws an exception in the stop function. Such errors will be logged and visible in Insights.

Configuration

Sometimes you need to configure your module and have a different configuration for different environments. QA vs Production for example.

You can use the Backoffice to set the configuration key/values:

The key to the Backoffice Service Configuration depends on the component.json. If it is a component with auth section, then the key is the service. Let's take a Slack component.json for example:

In this case, the appmixer:slack will be used as a key into the Backoffice. Then you can store various key/value pairs in the Service Configuration:

A good example is the clientID and clientSecret. Slack is an Oauth2 application and it needs a different Oauth application for each environment (due to the redirect URL). All of these values will be available in the component's context object (and in the context object in the auth.js file as well).

If your component/module does not use user authentication (no auth section in the component.json file), then the key to the Backoffice Service Configuration depends on the name of the component. Here's an example.

For such component/module you can store configuration under appmixer:utils or under appmixer:utils:tasks. Such key/value configuration pairs will be then available under context.config.

Example Component

Here's a how a full example of a component that sends SMS via the Twilio service can look like:

Directory Structure

Component Behaviour (sms/SendSMS/SendSMS.js)

Defines how the component reacts on incoming messages.

Component Manifest (sms/SendSMS/component.js)

Defines the component properties and metadata.

Component Dependencies (sms/SendSMS/package.json)

Our component uses the twilio NodeJS library. Therefore, we need to list it as a dependency.

Service Manifest (service.json)

Metadata about the Twilio service. The Appmixer UI uses this information to display the Twilio app in the Apps panel of the Designer UI.

Service Authentication Module (auth.js)

Defines the authentication for the Twilio service. This information is used both for rendering the Authentication dialog (displayed when the user clicks on "Connect New Account" button in the Designer UI inspector) and also for the actual authentication to the Twilio API and validation of the tokens.

Service Authentication Module Dependencies (package.json)

Our auth.js module uses the twilio NodeJS library. Therefore, we need to list it as a dependency.

Helper Component (sms/ListFromNumbers/ListFromNumbers.js)

This is a helper component that is used to list the phone numbers registered in the user's Twilio account. You can see that this component is called in the "static" mode in the SendSMS component manifest. Note that this component is not used out of necessity but more as a convenience for the user. Thanks to this component, the user can just select a phone number from a list of all their registered phone numbers in the Designer UI inspector. An alternative would be to let the user enter the phone number in a text field. However, that might result in errors to the API calls to Twilio if the phone number does not exist in the user's list of registered phone numbers in their Twilio account.

Helper Component Manifest (sms/ListFromNumbers/component.json)

Note the component is marked as "private" meaning that it will not be available in the Designer UI as a standalone component.

Helper Component Dependencies (sms/ListFromNumbers/package.json)

Helper Component Transfomer (sms/ListFromNumbers/transformers.js)

The numbers output port of our helper component returns a list of numbers. However, when the component is called in a static mode from our SendSMS component Inspector UI, the expected value is a list of objects with label and value properties so that it can be rendered by the UI. Alternatively, we could have just skip the transformer altogether and use this array structure right in our context.sendJson() call in our ListFromNumbers.js file. The advantage of using transformers is that we can use the ListFromNumbers component as a regular component (i.e. not private) and so allow the user to put the component in their flows. In other words, the same component can be used for static calls (to show a list of available phone numbers in the Inspector UI of the SendSMS component) as well as in a normal mode (as a regular component that can be put in a flow).

Quotas & Limits

The majority of APIs define limits on the API usage. Components that call APIs need to make sure that these limits are respected, otherwise their API calls would start failing quickly. The quota.js module allows you to specify what those limits are on a per-service, per-module or even per-component basis. The Appmixer engine uses this module to make sure API calls are throttled so that the usage limits are respected.

The quota module must be named quota.js and must be stored under either the service, module or component directory (i.e. [vendor]/[service]/auth.js , [vendor/[service]/[module]/auth.jsor [vendor/[service]/[module]/[component]/auth.js.

An example of a quota module:

The quota definition above tells the engine to throttle the receive() call of the component to a max of 2000-times per day and 3-times per second.

Quota Module Structure

Quota modules are NodeJS modules that return an object with one property rules.

rules

An array of rules that define usage limits. Each rule can have the following properties:

limit

Maximum number of calls in the time window specified by window.

window

The time window in milliseconds.

throttling

The throttling mechanism. Can be either a string 'window-sliding' or an object with type and getStartOfNextWindow function. Example of a quota module for LinkedIn:

resource

An identifier of the resource to which the rule applies. The resource is a way for a component to pick rules that apply to that specific component. This can be done in the component manifest file in the quota.resources section.

Custom Strings

Appmixer SDK allows you to change all the strings of all the UI widgets it provides (Designer, FlowManager, Insights, ...). This is especially useful to localize the entire Appmixer UI.

Setting Custom Strings

A strings object is represented as a JSON object that you set on your Appmixer SDK instance using the set('strings', myStrings) method:

You can set the strings object anywhere in your application but usually, you'll do that right after initializing your appmixer instance. Note that you can even set the strings multiple times with different configurations in which case the Appmixer SDK will automatically re-render all the UI widgets using the configuration with new strings.

If you don't set strings, the default strings will be applied.

Structure of the Strings Object

The strings object is a JSON object (with one exception, see below) that contains references to various UI elements within the Appmixer UI. The final values of the JSON objects are the actual strings used in the UI.

Example of setting the strings object:

Complete Strings Object

For reference, we prepared a complete strings object for you to download and inspect to see all the possibilities for strings customization/localization.

Time Localization

For localization of time-related strings, a special time root scope of the strings object can be modified:

Please download the default strings object above to see all the possibilities for time localization. Notice in the code above that there is one specialty to the time localization which (if used) makes the strings object non-JSON compliant. That's the ordinal(number) function. Given a number, this function returns a string representing the number in ordinal form (i.e. 1 becomes "1st", 2 becomes "2nd", ...). Since this is hard to describe declaratively in JSON, the strings object may contain the oridnal(number) function for you to be able to localize ordinal numbers. The default implementation looks like this:

Pluralization and Strings with Variables

Some text can contain both singular and plural versions based on whether the number variable used inside the text equals 1 or not. For example, the pagination widget in the Flows Manager:

The "of 8 flows" string used above can vary based on whether the total number of flows is more than one or if it equals one. The two versions can be expressed using the | character in the strings object like so:

Also, notice the use of variables ({{total}} in the example above). Variables are always enclosed by two curly brackets and are replaced by the SDK with the actual numbers when used. See the Appmixer default strings object for all occurrences of variables.

Slots

Sometimes words inside longer text have some functionality bound to them, typically a clickable link. For example, the Flow Manager displays the following text when there are no flows created yet:

The word "here", when clicked, opens up the Designer for the user to create a new flow. So how to localize the text and keep the functionality? Enter slots. Slots allow us to keep the functionality (and sometimes styling) bound to the words inside our localized strings. The strings object for this scenario looks like this:

The tokens (@createFlow) and (/@createFlow) are quite similar to HTML tags, meaning that what is between them will be bound to the event described by the token names (i.e. createFlow event).

Custom Theme

Appmixer SDK allows you to completely change the look&feel of all the UI widgets it provides (Designer, FlowManager, Insights, ...).

Setting a Custom Theme

A theme is represented as a JSON object that you set on your Appmixer SDK instance using the set('theme', myTheme)method:

You can set the theme anywhere in your application but usually you'll do that right after initializing your appmixer instance. Note that you can even set the theme multiple times with different configuration in which case the Appmixer SDK will automatically re-render all the UI widgets using the new theme configuration.

If you don't set a theme, the default theme will be applied.

Structure of the Theme Object

The theme object is a JSON object that contains references to various UI elements and their CSS-like properties. Each UI element is named and prefixed with the $ character. UI elements can have different states each having a different styling (e.g. disabled, hovered, ...). States are prefixed with the @ character. All other keys in the theme object are CSS-like properties that you can assign CSS values. Properties are inherited in the hierarchical structure of the UI elements. For example, setting a blue text color on the entire FlowManager ($flowManager) will make all the text within the nested UI elements of FlowManager (e.g. pagination) blue as well unless the text color is overwritten in that nested UI element.

Example of a theme object:

The theme object above produces the following result in the FlowManager widget:

And here's how the default styling looked like before we applied our custom theme:

Complete Theme Object

For reference, we prepared a dark theme for all the Appmixer UI widgets that you can use as a quick overview of all the UI elements that you can set your custom theme for:

Screenshots of the dark theme for some of the UI widgets:

The Inspector widget in the Designer UI provides built-in types such as text, number, toggle, select and others (See the for details). However, thanks to the modular design of the Designer UI, customized installations of the Appmixer system allows to extend the set of Inspector fields by custom types.

Custom Inspector Field for Polygon Selection in Google Maps
Custom Inspector Field with Rich Text Editor
Custom Inspector Field with Price Calculator

To define your custom inspector field, use the Appmixer JavaScript SDK appmixer.registerInspectorField(type, definition, options) method. To style your inspector field, just use regular CSS. definition is a . The component must call the change(value) method to tell the SDK to change the value of the inspector field and save the flow. The componet additionally adds this.value which stores the current value of the field in the flow descriptor. Therefore, calling change(newValue) will also change this.value. options are additional options that control the behaviour of the inline editor that replaces your inspector field when the user selects a variable from the variable picker. Available options are:

Template and Render function: ,

Data objects: , , ,

Livecycle hooks:

There are components that do not require user authentication, but they use API keys to authenticate to other third-party services. For example the Weather components. They use an API key to . In order to configure this API key (and not have it hardcoded), you can use access the context.auth.apiKey in the component and insert the apiKey into Backoffice:

Call an Appmixer REST API endpoint. You can call any of the Appmixer endpoints defined in the . The main advantage of this method (as opposed to calling the API endpoint manually) is that the method automatically populates the "Authorization" header of the request to the access token of the user who owns the flow this component lives in. For example:

Flow property is available here.

If this function throws an exception, then the Appmixer engine will try to process the message that triggered this receive call again in a minute. There is an exponential backoff strategy, so the next attempt will happen in a minute and a half and so on. In total, Appmixer will try to process that message 30 times before it is saved into collection. Every unsuccessful attempt will be logged and visible in Insights.

Properties section of the Component Manifest
Vue JS component
https://vuejs.org/v2/api/#template
https://vuejs.org/v2/api/#render
https://vuejs.org/v2/api/#data
https://vuejs.org/v2/api/#computed
https://vuejs.org/v2/api/#methods
https://vuejs.org/v2/api/#watch
https://vuejs.org/v2/api/#Options-Lifecycle-Hooks
const twilio = require('twilio');

module.exports = {

    receive(context) {
        let { fromNumber } = context.properties;
        let { accountSID, authenticationToken } = context.auth;
        let message = context.messages.message.content;
        let client = new twilio(accountSID, authenticationToken);
        return client.message.create({
            body: message.body,
            to: message.to,
            from: fromNumber
        });
    }
};

Virtual Method

Description

receive(context)

Called whenever there are new messages on the input ports that the component is ready to consume. This method must return a promise that when resolved, tells the engine that the messages were successfully processed. When rejected, the engine re-tries to send the messages to the component again in the next turn (or with an arbitrary delay).

tick(context)

Called whenever the polling timer sends a tick. This method is usually used by trigger Components.

start(context)

Called when the engine signals the component to start (when the flow starts). This method is usually used by some trigger components that might schedule an internal timer to generate outgoing messages in regular intervals.

stop(context)

Called when the engine signals the component to stop (when the flow stops). This is the right place to do a graceful shutdown if necessary.

{
    receive(context) {
        const smsContent = context.messages.message.content;
    }
}
Humidity: {{{$.ec8cd99f-0ad3-4bca-9efc-ebea5be6b596.weather.main.humidity}}}
context.messages.message.content === 'Humidity: 75'
const twilio = require('twilio');
module.exports = {
    type: 'apiKey',
    definition() {
        return {
            tokenType: 'authentication-token',
            accountNameFromProfileInfo: 'accountSID',
            auth: {
                accountSID: {
                    type: 'text',
                    name: 'Account SID',
                    tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
                },
                authenticationToken: {
                    type: 'text',
                    name: 'Authentication Token',
                    tooltip: 'Found directly next to your Account SID.'
                }
            },
            validate: context => {
                let client = new twilio(context.accountSID, context.authenticationToken);
                return client.api.accounts.list();
            }
        };
    }
};
{
    receive(context) {
        let { accountSID, authenticationToken } = context.auth;
    }
}
module.exports = {

    receive(context) {

        // prepare the qs

        // the 'apiKey' value set in the Backoffice will be available
        // at context.auth.apiKey (and context.config.apiKey, context.config
        // is an alias to context.auth)
        return weather.get('/weather', qs, context.auth.apiKey)
            .then(body => {
                // process results
            });
    }
};
{
    "properties": {
        "schema": {
            "properties": {
                "fromNumber": { "type": "string" }
            }
        },
        "inspector": {
            ...
        }
    }
}
{
    receive(context) {
        const fromNumber = context.properties.fromNumber;
    }
}
{
    async receive(context) {
        // Emit a message only once per day for this component instance.
        const day = (new Date).getDay();
        const state = context.state;
        if (!state[day]) {
            state[day] = true;
            await context.saveState(state);
            return context.sendJson({ tick: true }, 'out');
        }
    }
}
module.exports = {

    async start(context) {

        // register webhook in the slack plugin
        return context.service.stateAddToSet(
            context.properties.channelId,
            {
                componentId: context.componentId,
                flowId: context.flowId
            }
        );
    },

    async stop(context) {

        return context.service.stateRemoveFromSet(
            context.properties.channelId,
            {
                componentId: context.componentId,
                flowId: context.flowId
            }
        );
    }
}
{
    receive(context) {
        // getAttachment() is some function that retrieves a file from an API
        return getAttachment(context.auth, context.messages.attachment.content.id)
            .then((file) => {
                return context.saveFile(file.name, file.mimeType, Buffer.from(file.data, 'base64'));
            })
            .then((result) => {
                return context.sendJson({ fileId: result.fileId }, 'file');
            });
    }
}
{
  "filename": "testFile",
  "contentType": "text",
  "length": 7,
  "chunkSize": 261120,
  "uploadDate": "2021-01-22T12:20:29.227Z",
  "metadata": {
    "userId": "5f804b96ea48ec47a8c444a7",
    "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
  },
  "md5": "9a0364b9e99bb480dd25e1f0284c8555",
  "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
}
{
    receive(context) {
        return context.loadFile(context.messages.file.content.fileId)
            .then((fileContent) => {
                // uploadFileToAPI() is some function that uploads a file to an API
                return uploadFileToAPI(context.auth, content.messages.file.content.fileName, fileContent);
            });
    }
}
module.exports = {
    async receive(context) {
        if (context.messages.webhook) {
            // Webhook URL received data.
            await context.sendJson(context.messages.webhook.content.data, 'myOutPort');
            // Send response to the webhook HTTP call.
            // Note: you can also skip sending response immediately and send it
            // in other connected components in the flow.
            // If context.response() is not called, the engine waits for the first component
            // that sends the response (in the same "session", i.e. the same "message flow").
            return context.response('<myresponse></myresponse>', 200, { 'Content-Type': 'text/xml' });
        }
        // Otherwise, normal input port received data.
        const input = context.messages.myInPort.content;

        // The webhook URL. Do something with it (send to your API, send to other connected,
        // components, send to your backend, ...)
        const url = context.getWebhookUrl();
    }
};

Property

Description

content.method

HTTP method of the request.

content.hostname

Hostname of the Appmixer API.

content.headers

HTTP headers of the request.

content.query

Object with query parameters, i.e. query string parsed into a JSON object.

content.data

Object with the body parameters of the request.

correlationId

A special ID generated by the Appmixer engine uniquely identifies the input message which resulted in generating the webhook URL. In other words, if you call context.getWebhookUrl() in the receive() method in a reaction to an input message that arrived on an input port of the webhook component, the correlationId will be part of the returned webhook URL. This allows you to later associate the input message with the HTTP call to the webhook. A common pattern is to store the input message in the context.state object and later use the context.messages.webhook.correlationId to retrieve it back. For example, if you have an input port named myInPort, you can get the correlationId of the input message that just arrived by accessing the context.messages.myInPort.correlationId.

module.exports = {
    receive(context) {
        if (context.messages.timeout) {
            // Timeout message.
            return context.sendJson(context.messages.timeout.content, 'out');
        } else {
            // Normal input message.
            return context.setTimeout(context.messages.in.content, 5 * 60 * 1000);
        }
    }
};
const task = await context.callAppmixer({
    endPoint: '/people-task/tasks',
    method: 'POST',
    body: {
        title: 'My Task',
        description: 'My Example Task',
        requester: 'john@example.com',
        approver: 'alice@example.com',
        decisionBy: (function() {
            const tomorrow = new Date;
            tomorrow.setDate(tomorrow.getDate() + 1);
            return tomorrow.toISOString();
        })()
    }
});
module.exports = {
    async receive(context) {
        return context.stopFlow();
    }
};
[
  {
    "variables": {
      "dynamic": [
        {
          "label": "Column A",
          "value": "columnA",
          "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
          "port": "out"
        },
        {
          "label": "Column B",
          "value": "columnB",
          "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
          "port": "out"
        }
      ],
      "static": {}
    },
    "sourceComponentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
    "outPort": "out",
    "inPort": "in"
  },
  {
    "variables": {
      "dynamic": [
        {
          "label": "Column C",
          "value": "columnC",
          "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
          "port": "out"
        },
        {
          "label": "Column D",
          "value": "columnD",
          "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
          "port": "out"
        }
      ],
      "static": {}
    },
    "sourceComponentId": "71eb1f40-ce29-4963-8922-102739bafee4",
    "outPort": "out",
    "inPort": "in"
  }
]
{
    receive(context) {
        return context.loadVariables()
            .then(data => {
                const newSchema = data.reduce((acc, item) => {
                    return acc.concat(item.variables.dynamic.map(o => ({ label: o.label, value: o. value })));
                }, []);
                context.sendJson(newSchema, 'leftJoin');
                context.sendJson(newSchema, 'innerJoin');
                context.sendJson(newSchema, 'rightJoin');
            });
    }
}
{
    async start(context) {
        
        await context.log({ test: 'my test log' });
        return context.sendJson({ started: (new Date()).toISOString() }, 'out');
    }
}
    async receive(context) {

        let lock = null;
        try {
            lock = await context.lock(context.flowId, {
                ttl: 30000,
                maxRetryCount: 1
            });
            let { callCount = 0, messages = [] } = await context.loadState();
            messages.push(context.messages.in.originalContent);

            if (++callCount === context.messages.in.content.callCount) {
                await context.sendJson(messages || [], 'out');
                await context.saveState({ callCount });
                return;
            }

            await context.saveState({ callCount, messages });
        } finally {
            if (lock) {
                await lock.unlock();
            }
        }
    }
/**
 * Example of context.CancelError
 */
module.exports = {

    async receive(context) {

        let data = context.messages.in.content;

        try {
            // In this component is trying to create a record in a 3rd party
            // system.
            const resp = await someAPI.createSomething(data);
            await context.sendJson(resp, 'out');
        } catch (err) {
            // And there might be a unique constraint, let's say an email
            // address. And the 3rd party API will return an error with a
            // message saying that this record cannot be created.
            if (err.message === 'duplicate record') {
                // In this case, we can tell Appmixer to cancel the message.
                // Because next attempt would fail again with the same result.
                throw new context.CancelError(err.message);
            }
            // In case of any other error, rethrow the exception. Appmixer will
            // then try to process it again.
            throw err;
        }
    }
};
context.getWebhookUrl()
context.state
context
{
    "name": "appmixer.slack.list.NewChannelMessageRT",
    "description": "As opposed to NewChannelMessage and NewPrivateChannelMessage this trigger fires immediately every time a message is posted to a channel. Works both for public and private channels.",
    "webhook": true,
    "auth": {
        "service": "appmixer:slack",  // appmixer:slack will be used as a 'key'
        "scope": [
            "channels:read",
            "channels:history"
        ]
    },
    "outPorts": [
    ...
}
module.exports = {

    async tick(context) {
    
        context.auth.clientId;
        context.auth.clientSecret;
        context.auth.orAnythingElse;
        
        // all of those will be available at context.config as well
        context.config.clientId;
        context.config.clientSecret;
        context.config.orAnythingElse;        
    }
}
// component.json file without "auth" section
{
    // the property "name" will be used to look for configuration stored through
    // the Backoffice. Appmixer will try to look for a key [vendor]:[service] and
    // [vendor]:[service]:[module]
    "name": "appmixer.utils.tasks.RequestApprovalEmail",
    "description": "People Task is a feature of Appmixer that allows human interaction inside workflows.",
    "webhook": true,
    "inPorts": [
        {
   ...
}
twilio/
├── auth.js
├── package.json
├── service.json
└── sms
    ├── ListFromNumbers
    │   ├── ListFromNumbers.js
    │   ├── component.json
    │   ├── package.json
    │   └── transformers.js
    └── SendSMS
        ├── SendSMS.js
        ├── component.json
        └── package.json
const twilio = require('twilio');

module.exports = {

    receive(context) {

        let { accountSID, authenticationToken } = context.auth;
        let client = twilio(accountSID, authenticationToken);
        let message = context.messages.message.content;

        return client.messages.create({
            body: message.body,
            to: message.to,
            from: message.from
        }).then(message => {
            return context.sendJson(message, 'sent');
        });
    }
};
{
    "name": "appmixer.twilio.sms.SendSMS",
    "author": "David Durman <david@client.io>",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PGcgZmlsbD0iI0NGMjcyRCI+PHBhdGggZD0iTTEyNy44NiAyMjIuMzA0Yy01Mi4wMDUgMC05NC4xNjQtNDIuMTU5LTk0LjE2NC05NC4xNjMgMC01Mi4wMDUgNDIuMTU5LTk0LjE2MyA5NC4xNjQtOTQuMTYzIDUyLjAwNCAwIDk0LjE2MiA0Mi4xNTggOTQuMTYyIDk0LjE2MyAwIDUyLjAwNC00Mi4xNTggOTQuMTYzLTk0LjE2MiA5NC4xNjN6bTAtMjIyLjAyM0M1Ny4yNDUuMjgxIDAgNTcuNTI3IDAgMTI4LjE0MSAwIDE5OC43NTYgNTcuMjQ1IDI1NiAxMjcuODYgMjU2YzcwLjYxNCAwIDEyNy44NTktNTcuMjQ0IDEyNy44NTktMTI3Ljg1OSAwLTcwLjYxNC01Ny4yNDUtMTI3Ljg2LTEyNy44Ni0xMjcuODZ6Ii8+PHBhdGggZD0iTTEzMy4xMTYgOTYuMjk3YzAtMTQuNjgyIDExLjkwMy0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODUgMTEuOTAzIDI2LjU4NSAyNi41ODUgMCAxNC42ODQtMTEuOTAyIDI2LjU4Ni0yNi41ODUgMjYuNTg2LTE0LjY4MyAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4Nk0xMzMuMTE2IDE1OS45ODNjMC0xNC42ODIgMTEuOTAzLTI2LjU4NiAyNi41ODYtMjYuNTg2IDE0LjY4MyAwIDI2LjU4NSAxMS45MDQgMjYuNTg1IDI2LjU4NiAwIDE0LjY4My0xMS45MDIgMjYuNTg2LTI2LjU4NSAyNi41ODYtMTQuNjgzIDAtMjYuNTg2LTExLjkwMy0yNi41ODYtMjYuNTg2TTY5LjQzMSAxNTkuOTgzYzAtMTQuNjgyIDExLjkwNC0yNi41ODYgMjYuNTg2LTI2LjU4NiAxNC42ODMgMCAyNi41ODYgMTEuOTA0IDI2LjU4NiAyNi41ODYgMCAxNC42ODMtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDMtMjYuNTg2LTI2LjU4Nk02OS40MzEgOTYuMjk4YzAtMTQuNjgzIDExLjkwNC0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODYgMTEuOTAyIDI2LjU4NiAyNi41ODUgMCAxNC42ODQtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4NiIvPjwvZz48L3N2Zz4=",
    "description": "Send SMS text message through Twilio.",
    "auth": {
        "service": "appmixer:twilio"
    },
    "outPorts": [
        {
            "name": "sent",
            "options": [
                { "label": "Message Sid", "value": "sid" }
            ]
        }
    ],
    "inPorts": [
        {
            "name": "message",
            "schema": {
                "type": "object",
                "properties": {
                    "body": { "type": "string" },
                    "to": { "type": "string" },
                    "from": { "type": "string" }
                },
                "required": [
                    "from", "to"
                ]
            },
            "inspector": {
                "inputs": {
                    "body": {
                        "type": "text",
                        "label": "Text message",
                        "tooltip": "Text message that should be sent.",
                        "index": 1
                    },
                    "from": {
                        "type": "select",
                        "label": "From number",
                        "tooltip": "Select Twilio phone number.",
                        "index": 2,
                        "source": {
                            "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                            "data": {
                                "transform": "./transformers#fromNumbersToSelectArray"
                            }
                        }
                    },
                    "to": {
                        "type": "text",
                        "label": "To number",
                        "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                        "index": 3
                    }
                }
            }
        }
    ]
}
{
    "name": "appmixer.twilio.sms.SendSMS",
    "version": "1.0.0",
    "main": "SendSMS.js",
    "author": "David Durman <david@client.io>",
    "dependencies": {
        "twilio": "^3.14.0"
    }
}
{
    "name": "appmixer.twilio",
    "label": "Twilio",
    "category": "applications",
    "description": "Twilio is a cloud communications platform as a service.",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAPEElEQVR42u2df5RVVRXHP+85MyA/hBBLkHO1pGsqCqL9wsRqlSmWUVFqGEiR/VyZQVlp1kL7g5X9tlZqRZpkJrmszFr2a6EtTE1+iGAdMexcbMxAMkdkBpjpj7Mv3m5v3rvn3PNm3iB7rbfWc+Tdc87+3rN/nb33qdAClEQxyuj83zqA4UAHcChwAhDLZxLwQmA8cCAwTH7WDTwLbAWeALYAWj5rgMeBHmCnMrqn0RwGgyqtBEQSxQcDRwNTgOnyOU5ACUE9wHpgtXweBB5SRm9rFWAqLQLE6cBcYCpwGDBugKbyJPAYsA5Yroz+9WADUxksIJIoHg18GLgYeAGtQduBpcC3ldFPDwYwAw3IMGAysAC4EGijNWk38HVgGbBJGd09UANXm70jMt9nAtcC9wKLWhgMZG6LZK7Xytz/b01DZoekk1ZGk0TxGOC7wCxgBEOTdgC3AwuV0U9l19fSgOSAaAfOA65p8d3gKsouAG5QRu9qBjCVkGBkFPZ04HPAbPZNuhW4XBm9OrTiDy6ykiheDHwCmMC+TZ3AV5TRV7aUyMoouQ4RT/MG2+EcQOoDrhcx1hNCfFXLAiETmAj8AZj/PAIjfaHny9onZkT2oO6QU4HrgMN5ftPfgfnK6JUDLrJSJZZE8euBm7BBvmbTLuA+YC3wF+BR4J9Al4iOdD2jgBcBRwAvA6YBLwfaB2COW4GzldG/91X0FR8g5PvLgT81ybnsBfYI028AViijNzZyPuvJ8CSKjwHmiCl+BHBAE+f+KmX0fT4WmO8OeT3wmyYs6D/AA8AdwI3K6E1NclwnA+cCpwHHAwc1AZQ3KqN/37QdkhFTpwIrAoupXcDV8tz7ldFdNQyHoI6r/Pco4ETZOR8ILNa2AnOU0StddknFcTETgVWBFfhNwGKgUxm9pxlAFADmAPGbrgTODqzoZwD/KLqWatHJi59xcyAw+rAHRScro89RRm8RnbGXUc0Medd4/h5l9BZl9DnAyTK3vgBDHS486yhqDleLKnFx+mYEmGQX8AXg1croVc3eDUUByqx3FfBqmWNXgMfPEN6lIr+8YyjhkHkBJrcOOEUZvUQZ/UyrnGNngZE5PaOMXgKcInMuS/OEh/46JKPEpwO3BYhN/Qo4Nw1ftxIQDdY/BrgROCNA7OvNyujV9dbfSGS1Y6O2ZcG4DjhzqICR2y1PAWfKGsrQBOBzwlM3kZVh2nmUD6Ffqow+XxndN1TAqAFKnzL6fODSko+cDZxXT5fUE1ljxJYuc7h0KbBUGb17qIHRj/hqwyZlXFHicbuB8bLznAC5WRwmbzElb9U+R0kU/wAb5fWlFcrod7qIrJnYM/AyCnxBUdt7KO0UoQWyRl+alU2c6BeQJIrTVJ2F+CckrBNrqpDOKAJYCFBDjJPVKRIL8zWJRwALkygelh+zUmNSx2LTX3wA6RI/Y21RMDJO2YnAW7FppCOAf8k8blFGP1bDUXWS//L9UODtwCuxucHPAhuBXwD3ZBheVKdMA+7ChvxdaQfwCmX0hpo7JLcdfcDoA75UBIzMWAclUXx2EsUPA38WE/ttwJvEwvsGsCWJ4l8mUfyKdL6ub3sSxSclUXyr+ALfEif3dBnrEuwxwuYkis8DxjYaIwPcWuBLnmGWEbXEeiW3iNHYfFcfy2q9hEOeKfh2xcDXHByu3cBXgcuU0TsL7o52CYEs4rkM+Ub0W+DjyugNBXfLSOBubFK4j8U1Lk1bpcYb9+ESZu4H03BIAUZNBH7q6P22AZ8EvplEcUe9cUQXtguAn3UAA+ANwC1JFKuC4vAZ4IOePGsTnu/FoJoL7F3s+eCb0kBhARlfwaaUTvEcayEwtz/nKjOHOcBHPMeIsdkk1YJKfhX2GMGHLs5iUE0XJSUBPlnou7DnGUX1xrySJjXAd5IoPqTWeMKksdgDrzL02vzb2x8oQouFF670AuE9SRRTzTxwrufErwY6i5iMQl8O4BJ0AEtqKO/062eA0QHGuSKJ4o6CpntniZdgbsqjVIccjC2WcaX/iNe5p4g5mkTxWYQrxlmQ35WZ7x8JNMZBwDuKxLzktHOF8MSVpgoGe2XkMdjKJVd6ALjfUQyEovYkil9VA/TjCJdlXwFOdXBO7xeeuNJhgsFeQI71eHN7gTuU0V0OnvRLCJfZWJHn5enIwBGTFzfSjxljogubMdPrOMY4wYCqVLtO95joHuzBjQsND8ysYQMwhmvB6Y1k8gMcaHoSxR1VWYAPIJuV0ZscwxnbAzPr3zX+9iRhEhSyetIlzrUJ2OwDCDC8Km+Aj5e53FPnhGJWpZ/g3gMBxWKaHeMaQ/PhzXFARxVblO9TB77CY6I/C8iszcrov9V4Ux+XgGEo0H9e9B9neLHCUzQeWsV2SHB2BvvLtW0w4Y3APYGYVc8PuSzQGA8qo+/1XKePk3hCVcIErnRfWf+hJG1QRv8gr78ycvyWknNM6XwHkzcEj2JfQNb6zFCY9RDwqRJM2k6x49OF2DMVX/q8Mvr+ErkAa30BmeTxw7/4zDATEPwa8BWPR+zGFsX0y6jMLnkAe6biU/T/bWBpycQMHx5NasOenLnSo76zlAXuAhYlUbwZewhVRNH/A5viv7ERozKg3CEnoL+jeE7yJwMVcvrw6IVt+JUV/LPMTDP5vFclUXyLKOGT5eUYjS2m6QaewrZY+mnKpKJvbQaUR4Ajkii+EJvZPgl7KjhMHLguEW13A0uU0SZQvrEPj8ZXkijeidsBThpq2Vg2zyp33n0wtg/KeGydxg5sf6u/pr1GApypdwBHYbMIR4gI3Ao8ooz+l+8Y/bxwxwAbHH/a3eYBBqGcu2xSgfSs2tZgR5XRW0jTsvWps1cPvEBOpXMoqKnNZ1qFhlLGZJvIatddEsTbrtE/KxVZbdgUnU7gYWV0b1mRldkpcU5kbROR9VTRNKAm8qi7kkTxdlFyLvRKHw82J4Iqkkw3Fpv+8zpsOfNo7LFAjyh1gy0A/Y4rKDnA3wu8R6ytsRKq6BWl/gRwpyj1rSGUuqQtuUYl/l2RnKjJjj88Sxn9iwA7ZB62dVORYstHgDcoox91TGabiE3tObrAGL3Ah5TR1wRY21tc4mBCm9rk7XAF5IiSu6MNmy91icNPjwQeSqL4LGX0b+qBkgFjJrbP1ciCY1SBq5Mofik2fWhXiV3iw6MnqmLnu9JRJXXGhxzBSGk4sDyJ4uPr1VjI/zsa+LEDGFlaDFxUpCYwMI+2VLE9bV3J50ArZdSR4p370iHA9xs5ndhCyzKVX0uTKJ5SYof48Ej7AnJSiYV+P4AFc2ISxefk3+CMqDoTeE2AccqUsZ3kC8gajx+2S+8QV5EVAzMDmexL8pZQ5vsXA40xPYniEzzWeQx+XSHWVHmu/bYrzcmJiCI0m3BHuC9NovjwGsw4BNu/JJS3PdsBiP/hjSP1AI+n9v56jwf4ZDpOJex597Qaf59G2HP7qR4vng9v1gM9VWAntg+6K704ieLJjpZI6Bbi4/r5W8iudmOLOIkZ/TUZyeVypNXAzqoE3HwAOQBb1uW6LUNSzyCNUY/OFd44A6KM7kmDixuw+UwuVAVOS6J4lINp+LeA4qSP2vlPIccAOWgqmJc1CtuDyzVo+6RgsPeHG7G3BLjS8dh+U0XpzoCM2iN1GXnGrMPv2LY/0O9yiGud6GlQPCYY7C3Y2YZfRelBwJwkig8oqEd+Ru1sQx/6Yf7NzXy/OtAYXdhKr4b6Q3puzcGvO9269A6TamYRyz0n/QFgQkGl14ut3ShLvUibi378kCvEWClLlyujdxSsfZkgvPCh5SmP9hbsyGUmPrm37dhObEUrja7FJh2UoY8qozvrlLRtw8bLytA9FCguyszhSk9ncHt6kUy2YGdv/MZz8mcnUTyjoAncC7wXeNhzrBuAZY3SgIAfAd8rocjfLXMtElWegX9rwKVZDAazLHoKNsj4OocxrgI+3WiMzFgHApcDFzlYPn8EPqaMXjNoZdEZRj2NvVnGh6Zg68GLFt0/iC0Xex8256oe3SUxsIscSq9RRj+LzZI8BWjUsvUJbHnzbGX0mkaWVWYOi/CvKP66MvrpfhsHyEAD3lojieK0dOxt2BSjkcKge4GbldE6++8dA33ZY9wjgXdhW2u8CJtqtFGsvz8oo/cMdmuNSg3Uh4nifY8n6uuAU4t2j3NpUON7NlH0945gjAFW4lcsm5rt7we6s2NW+hl0Jrb9kG/x5O3Y/oJ9Q7lxWYMdfRv+9fY7gDOU0XfWCn/UkvN3ClN9aRb2hrN9BozcWpZRrvnB7bXAoIHlsVCsAF+an0TxJdIWb0g3Mst0u2hLovgSynWT2y28pTAgmW6cF5RcyxVkenkMRVByIrdsv0WAC7I3veWp0uCtaAd+QvnOpNcBC4aaTsnpjGUldwbYy8TeRZ30okbdbnaJY9VZciLzgduSKB4zVHZKzpq6LQAYnRIbq1t7WC3gwK3GVjuVPWOYBaxMonhaiLuamq0vMn7GSsp3L+rD3ui2upGEKBROkGKZ6wOsdypwVxLFlyVRPLLVdktmV4xMovgycfqmBnj09UWrsgo16EqVEfbukLI0CptGencakBzs3ZLbFTMkNvUFTw88T6tS46iI/tx/oQutdaHL/iuPhuqVR7mF7b8UrD41/1KwvAwcoGvzNmOPN0NemzcXmze1T12bN1gXS/4Zm0P2V4pdLHkUNgv9JPbFiyX7AWf/1avPKfDSV6+GuJx4pVgSq57HYKwCZqRgDPblxOnX/dd3B7AI919wXy42FfyC+2pAILJhljdjI5v7Kt2KPRFtmI82qDskF4Jox7ZHuoZy91i1Eu0W8XSDMnpXMyIKTZH1OWDGYGvRZxGuwfFA0w7skfbC7OFSM0I7TVW+OUdyJvbo8h1DCJgd2GTr76Zn4M0+YBtQa0jut5qM7bt4YQuLst3YhMFlwKa0PdRA0EB3A+pWRm9QRi/Glp59hvDNlcvQdpnTOGX0Ykli6x7ICQyKv5Df9nJ/xlzsYdBhhK9F7I+exBbLrAOWp1noAyGaWgqQOsAcjO3Ediw2BjUd6fgcaMi04ni1fDZgO+NtG2wgWgKQekyQdnzDBYwJ2HLnWD6TsP0ZxwMH8ly/r25sn62t2NzgLdhOFRrbtrVTQNkpxa60ChAp/RdLBDnJ9t9abQAAAABJRU5ErkJggg=="
}
const twilio = require('twilio');
module.exports = {
    type: 'apiKey',
    definition: () => {
        return {
            tokenType: 'authentication-token',
            accountNameFromProfileInfo: 'accountSID',
            auth: {
                accountSID: {
                    type: 'text',
                    name: 'Account SID',
                    tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
                },
                authenticationToken: {
                    type: 'text',
                    name: 'Authentication Token',
                    tooltip: 'Found directly next to your Account SID.'
                }
            },
            validate: context => {
                let client = new twilio(context.accountSID, context.authenticationToken);
                return client.api.accounts.list();
            }
        };
    }
};
{
    "name": "appmixer.twilio",
    "version": "1.0.0",
    "dependencies": {
        "twilio": "^3.14.0"
    }
}
const twilio = require('twilio');

module.exports = {

    receive(context) {

        let { accountSID, authenticationToken } = context.auth;
        let client = twilio(accountSID, authenticationToken);
        return client.incomingPhoneNumbers.list()
            .then(res => {
                return context.sendJson(res, 'numbers');
            });
    }
};
{
    "name": "appmixer.twilio.sms.ListFromNumbers",
    "author": "David Durman <david@client.io>",
    "description": "When triggered, returns a list of numbers from user.",
    "private": true,
    "auth": {
        "service": "appmixer:twilio"
    },
    "inPorts": [ "in" ],
    "outPorts": [ "numbers" ]
}
{
    "name": "appmixer.twilio.sms.ListFromNumbers",
    "version": "1.0.0",
    "description": "Appmixer component for twilio to get a list of phone numbers of a user.",
    "main": "ListFromNumbers.js",
    "author": "David Durman <david@client.io>",
    "dependencies": {
        "twilio": "^3.14.0"
    }
}
module.exports.fromNumbersToSelectArray = (numbers) => {
    let transformed = [];
    if (Array.isArray(numbers)) {
        numbers.forEach(number => {
            transformed.push({
                label: number['phoneNumber'],
                value: number['phoneNumber']
            });
        });
    }
    return transformed;
};
module.exports = {
    rules: [
        {
            limit: 2000,
            throttling: 'window-sliding',
            window: 1000 * 60 * 60 * 24,
            scope: 'userId',
            resource: 'messages.send'
        },
        {
            limit: 3,
            window: 1000,
            throttling: 'window-sliding',
            queueing: 'fifo',
            resource: 'messages.send',
            scope: 'userId'
        }
    ]
};
const moment = require('moment');

module.exports = {
    rules: [{
        // The limit is 25 per day per one user.
        limit: 25,
        window: 1000 * 60 * 60 * 24,
        throttling: 'window-sliding',
        queueing: 'fifo',
        resource: 'shares',
        scope: 'userId'
    }, {
        // The limit is 125000 per day per application.
        limit: 125000,
        throttling: {
            type: 'window-fixed',
            getStartOfNextWindow: () => {
                // Daily quotas refresh at midnight PST.
                return moment.utc().startOf('day').add(1, 'day').add(8, 'hours').valueOf();
            }
        },
        resource: 'shares'
    }]
};
var appmixer = new Appmixer({ baseUrl: BASE_URL });
appmixer.set('strings', STRINGS);
appmixer.set('strings', {
  flowManager: {
    header: {
      btnCreate: 'Create new Flow',
      search: 'Search flows'
    }
  }
});
appmixer.set('strings', {
  time: {
    months: [...],
    monthsShort: [...],
    weekDaysShort: [...],
    ordinal(number){ ... },
    relativeTime: {...}
  }
});
ordinal(number) {
   const b = number % 10;
   const output = (~~(number % 100 / 10) === 1) ? 'th'
       : (b === 1) ? 'st'
           : (b === 2) ? 'nd'
               : (b === 3) ? 'rd' : 'th';
   return number + output;
}
appmixer.set('strings', {
  flowManager: {
    header: {
      pagination: 'of {{total}} flow|of {{total}} flows'
    }
  }
});
appmixer.set('strings', {
  flowManager: {
    messageNoFlows: {
      text: 'Click (@createFlow)here(/@createFlow) to create a new flow'
    }
  }
});
var appmixer = new Appmixer({ baseUrl: BASE_URL });
appmixer.set('theme', THEME);
appmixer.set('theme', {
    $flowManager: {
        backgroundColor: 'lightgray',
        color: 'blue',
        $header: {
            $btnCreateFlow: {
                '@hover': {
                    backgroundColor: 'black',
                    color: 'white'
                }
            }
        },
        $thumbnails: {
            spacing: '15px',
            $flow: {
                color: 'white'
            }
        }
    }
});
60KB
appmixer-strings-en.json
appmixer-strings-en.json
554KB
theme-dark.json
Appmixer Dark Theme
https://openweathermap.org/api
API section
unprocessedMessages
Enabling Users to Publish Custom Components
Enabling Users to Publish Custom Components

Installation

Appmixer Self-Managed package is shipped as a zip archive and allows you to install the Appmixer platform on your own infrastructure or in a cloud-computing platform.

Prerequisites

Installation

First, unzip the appmixer.zip archive and change to the appmixer/ directory.

Install and Start Appmixer

docker-compose --project-name appmixer up

Stop Appmixer

docker-compose --project-name appmixer stop

Stop and Clean Up

Stop Appmixer and remove all containers and images:

docker-compose --project-name appmixer down --volumes --remove-orphans --rmi all

Using Webhook Components

$ ngrok http 2200
ngrok by @inconshreveable                                                                                                                                                                                                                     (Ctrl+C to quit)

Session Status                online
Account                        (Plan: Basic)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://568284c4.ngrok.io -> http://localhost:2200
Forwarding                    https://568284c4.ngrok.io -> http://localhost:2200

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Copy the URL it gives you, in our case https://568284c4.ngrok.ioand replace the following line in the docker-compose.yml file in the root directory of the Appmixer package:

  engine:
    ...
    environment:
      - APPMIXER_API_URL=${APPMIXER_API_URL:-http://localhost:2200}
    ...

with the URL from ngrok:

engine:
    ...
    environment:
      - APPMIXER_API_URL=https://568284c4.ngrok.io
    ...

Now restart Appmixer:

docker-compose --project-name appmixer down # or kill existing with Ctrl-c
docker-compose --project-name appmixer up

Or you can keep the docker-compose.yml file as it is and run it with:

APPMIXER_API_URL=https://568284c4.ngrok.io docker-compose --project-name appmixer up

Creating an Admin User

Users in Appmixer can have a special "admin" scope defined which gives them access to the entire system. Such users have access to all flows and all users and also access to the Backoffice UI application that gives these admin users an overview of users and flows in the system. Due to security reasons, the only way to give a user the "admin" scope is to modify a user record right inside the MongoDB database:

docker-compose -p appmixer exec mongodb mongo appmixer --quiet --eval 'db.users.update({"email": "admin@example.com"}, { $set: {"scope": ["user", "admin"]}})'

Replace the "admin@example.com" email address with an email address of an existing user which you want to grant admin rights. If you don't have a user created yet, please continue to the next "Getting Started" section to see how you can create one using the Appmixer Front-end UI interface. The command above should have the following output:

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

Enabling Users to Publish Custom Components

Once you have your admin user created. You can use the Backoffice UI to enable users to upload custom components. This can be done by setting the "Vendor" property on your users. Only users with the "Vendor" property set can upload components and only those components that have a matching [vendor] in their names. For example, component "appmixer.utils.forms.CreateForm" can only be uploaded by a user who has "Vendor" set to "appmixer".

Note that the Appmixer Trial package automatically sets the "appmixer" vendor on ALL newly created users so any user of the Trial package can publish custom components with no extra action required.

It is a good practice to set the "appmixer" vendor so that you can upload all of the connectors we provide without modifying all the component/service/module names:

From now on, my "david@client.io" user will be able to publish new custom components using the appmixer CLI tool.

Custom API

Appmixer SDK allows you to override any API method used by the SDK instance.

Setting a custom API option

Custom API is represented as an object composed of asynchronous methods that you set on your Appmixer SDK instance using the api option:

The list of API methods can be found here.

Using OAuth applications

How to install pre-packed OAuth applications.

Appmixer comes with a set of prepared components (applications). Some of them use OAuth 1 (Twitter) authentication mechanism and some of them (Slack, Google) use OAuth 2 authentication mechanism.

If you try to use the Slack component right after fresh Appmixer installation you will get an error Missing client ID.

Therefore you need to create these OAuth applications on your own. At the end of the process, you will always get a client ID and client Secret (OAuth 2).

Before installing an OAuth application, please visit the APP registration section. It contains specific settings for each application.

GRIDD_URL

There is another ENV variable that is used for OAuth redirects. It is called GRIDD_URL.

By default, the GRIDD_URL will be set to http://localhost:2200. This is fine for local development. For most of the services, you can register http://localhost:2200 as a callback URL. In production, this has to point to your Appmixer's public API URL.

If you run the following command:

Another way would be replacing the variables directly in docker-compose.yml file.

Or you can set just the GRIDD_URL like this:

If APPMIXER_API_URL is not set, Appmixer will use the value from GRIDD_URL.

Appmixer engine will print out values of these variables when starting so you can check they are set correctly:

Variables
Variables dynamically populated at design time with available Twilio phone numbers.
FlowManager Dark Theme
Designer Dark Theme
Insights Logs Dark Theme
Insights Chart Editor

v12.13.0 (only for )

Now you can open the Appmixer Frontend in your browser at . Before you start creating flows with applications that require user authentication (OAuth), read this .

Some components require that the Appmixer engine URL is accessible on the Internet. This is especially true for any component that uses webhooks. In order to get these components working on your local machine, you need to set the APPMIXER_API_URL environment variable on the Appmixer engine to point to a public URL. When using the Appmixer trial on your local machine, we recommend using the utility:

But this command will set the GRIDD_URL to https://568284c4.ngrok.io as well. See more information about GRIDD_URL in .

As you can see, we have set the "scope" property on our user record to ['user', 'admin']. From now on, our user will have admin rights. Now you can sign-in to the Backoffice UI at using the email address and password of your admin user. If you visit the Users page, you should see something like this:

In the case of the OAuth 1 application, the error will be Missing consumer key. As described the OAuth applications need these secrets. The reason we do not provide these secrets with Appmixer is simple. Part of the OAuth application registered in the third party service is the redirect URL which points to your server URL where the Appmixer backend API is running.

The OAuth variables can be set through the .

It will set both the APPMIXER_API_URL and the GRIDD_URL to the .

Docker
Docker Compose
http://localhost:8080
section
ngrok
http://localhost:8081
var myCustomApi = {
  /* the key must match an existing API method */
  myCustomApiMethod(/* arguments of the original method */) {
    return new Promise((resolve) => {
      resolve(myCustomResponse);
    });
  }
}

/* Use a custom API on the entire SDK instance */
var appmixer = new Appmixer({ api: myCustomApi });

/* Use a custom API on a particular SDK UI widget */
var designer = new appmixer.ui.Designer({ api: myCustomApi });
engine:
    ...
    environment:
      - APPMIXER_API_URL=${APPMIXER_API_URL:-http://localhost:2200}
      - GRIDD_URL=${APPMIXER_API_URL:-http://localhost:2200}
    ...
APPMIXER_API_URL=https://247bb870a6f4.ngrok.io docker-compose --project-name appmixer up
engine:
    ...
    environment:
      - APPMIXER_API_URL=https://247bb870a6f4.ngrok.io
      - GRIDD_URL=https://247bb870a6f4.ngrok.io
    ...
engine:
    ...
    environment:
      - GRIDD_URL=https://247bb870a6f4.ngrok.io
    ...
here
API
Backoffice
https://247bb870a6f4.ngrok.io
here

Custom Component: HelloAppmixer

At this point, you have your Appmixer system up and running and you know how to create and start a flow. Now it's time to guide you through the process of implementing your own, custom component. In our tutorial, we implement a component that has one input and one output port, consumes messages on its input port, sends an HTTP request to an external API for each incoming message and outputs the response to the output port for other connected components to ingest.

Directory structure and files

Our component will live in a namespace "appmixer.myservice.mymodule.HelloAppmixer". All components in Appmixer are organized in the hierarchy [vendor].[service].[module].[component].

We prepared the HelloAppmixer component from this tutorial for you. You can download it from here:

2KB
appmixer.myservice.zip
archive

You can just download the component and publish it with:

# Install and initialize the Appmixer CLI tool if you haven't done so already:
$ npm install -g appmixer
$ appmixer url http://localhost:2200
# Use e.g. the same user as you signed-up with in the Getting Started guide.
$ appmixer login your@user.com
$ appmixer publish appmixer.myservice.zip

If you prefer to go step by step instead (which we recommend), create the following files and the required directories:

  • myservice/mymodule/HelloAppmixer/HelloAppmixer.js, component source code file

  • myservice/mymodule/HelloAppmixer/component.json, component manifest file

  • myservice/service.json, service manifest file

The resulting directory structure should look like this:

$ tree myservice/
myservice/
├── mymodule
│   └── HelloAppmixer
│       ├── component.json
│       └── HelloAppmixer.js
└── service.json

2 directories, 3 files

Component Definition

Now let's fill up the files with the minimum content necessary for the engine to consider this as a valid component.

myservice/mymodule/HelloAppmixer/HelloAppmixer.js

module.exports = {};

myservice/mymodule/HelloAppmixer/component.json

The component manifest file defines properties of our component (such as name, icon, input/output ports, ....). For now, we just fill in the only required field, the name and the optional but useful icon. It's important to note that the name must be a fully qualified namespace or our component:

{
    "name": "appmixer.myservice.mymodule.HelloAppmixer",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg=="
}

myservice/service.json

The service manifest defines properties of our app as it appears in the left palette in the Designer UI.

{
    "name": "appmixer.myservice",
    "label": "My Service",
    "category": "applications",
    "description": "My Custom App",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg=="
}
# Initialize your Appmixer CLI client if you haven't done that already:
$ npm install -g appmixer
$ appmixer url http://localhost:2200
# Use e.g. the same user as you signed-up with in the Getting Started guide.
$ appmixer login your@user.com

# Now we can pack and publish our component:
$ appmixer pack myservice
$ appmixer publish appmixer.myservice.zip

Extending our Component Definition

Now let's make our component actually do something. First, we start with extending the component manifest file (component.json) by adding an input port so that we can connect our component to other components:

{
    "name": "appmixer.myservice.mymodule.HelloAppmixer",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg==",
    "inPorts": [
        {
            "name": "in"
        }
    ]
}

This is enough to define an input port with no restrictions. However, we would like to add two properties (text and count) that we can count on in our component behaviour and that we can assume are always defined (i.e. marked as required in the Designer UI). Moreover, we will also define the Inspector UI for both our properties so that the user can fill them in in the Designer UI.

{
    "name": "appmixer.myservice.mymodule.HelloAppmixer",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg==",
    "inPorts": [
        {
            "name": "in",
            "schema": {
                "type": "object",
                "properties": {
                    "text": { "type": "string" },
                    "count": { "type": "number" }
                },
                "required": ["text"]
            },
            "inspector": {
                "inputs": {
                    "text": {
                        "type": "text",
                        "group": "transformation",
                        "label": "Text",
                        "index": 1
                    },
                    "count": {
                        "type": "number",
                        "group": "transformation",
                        "label": "Count",
                        "index": 2
                    }
                },
                "groups": {
                    "transformation": {
                        "label": "Transformation"
                    }
                }
            }
        }
    ]
}

When you now republish your component, refresh the Designer page and connect our component to another component, you should see both our properties in the Inspector:

$ appmixer pack myservice
$ appmixer publish myservice.zip

Note that we're using the Controls/OnStart component in our flow below. This utility component is useful especially during development of your custom components as the only thing it does is that it triggers/fires as soon as we start our flow.

Note that at this point, our component will still not work. If you try to run this flow, you will notice errors coming from the engine in the Insights/Logs page (click on the triple dot and then go to Insights):

Sending HTTP Requests

In this section, we will show how to extend our component to be able to receive messages, call an external API and send the result to the output port so that other connected components can work with the data. Let's now again extend the component manifest file (component.json) by adding an output port:

{
    "name": "appmixer.myservice.mymodule.HelloAppmixer",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg==",
    "inPorts": [
        {
            "name": "in",
            "schema": {
                "type": "object",
                "properties": {
                    "text": { "type": "string" },
                    "count": { "type": "number" }
                },
                "required": ["text"]
            },
            "inspector": {
                "inputs": {
                    "text": {
                        "type": "text",
                        "group": "transformation",
                        "label": "Text",
                        "index": 1
                    },
                    "count": {
                        "type": "number",
                        "group": "transformation",
                        "label": "Count",
                        "index": 2
                    }
                },
                "groups": {
                    "transformation": {
                        "label": "Transformation"
                    }
                }
            }
        }
    ],
    "outPorts": [
        {
            "name": "out",
            "options": [
                { "label": "Hello Data", "value": "mydata" }
            ]
        }
    ]
}
$ cd myservice/mymodule/HelloAppmixer/
$ npm init -y
$ npm install axios --save

Now we can use the library in our component module (HelloAppmixer.js):

const axios = require('axios');
module.exports = {
    receive(context) {
        let count = context.messages.in.content.count || 1;
        let text = context.messages.in.content.text;
        return axios.get('https://postman-echo.com/get?text=' + text + '&count=' + count)
            .then(response => {
                return context.sendJson({
                    mydata: 'Received from Postman echo: ' + JSON.stringify(response.data.args)
                }, 'out');
            });
    }
};

Notice the context.messages object that is a dictionary with keys pointing to input ports and values containing the content property that gives us a dictionary with all our properties that we defined on the input port.

Now we can republish our component again and create a flow like this:

When you run this flow, you should see a new tweet in your Twitter account that looks something like this:

You can also check the Insights page for this flow to see the logs:

Conclusion

In this tutorial, we demonstrated how you can create a custom component that processes incoming messages, calls an external HTTP API and sends outgoing messages for other connected components to consume. We used simple example flows with the OnStart component as a trigger. However, instead of our OnStart trigger component, we could have used other triggers, such as schedulers, polling components (e.g. the included PipeDrive.NewPerson trigger that checks for new contacts in the Pipedrive CRM), webhooks and more. We suggest to browse the online documentation to learn more about all the features of Appmixer and how you can apply them in your custom components.

Using Appmixer SDK

The Appmixer JavaScript SDK allows you to embed any of the UI widgets of Appmixer into your own web products. You can also take advantage of the SDK methods to communicate with the Appmixer engine REST API.

Open the Appmixer SDK demo

Start with opening the Appmixer SDK demo in your browser:

$ cd appmixer/frontend/appmixer/
$ open demo.html

Or you can download the necessary files from the Appmixer front-end:

# Download the Appmixer SDK:
$ wget http://localhost:8080/appmixer/appmixer.js
# Download the demo page:
$ wget http://localhost:8080/appmixer/demo.html
# Download the example theme object:
$ wget http://localhost:8080/appmixer/theme.js
$ open demo.html

Notice that this won't work yet since we haven't configured basic required variables. First we need to edit the demo HTML file to add base URL of our Appmixer engine REST API and our user credentials. Open the frontend/appmixer/demo.html file in your editor and find the following section:

    <script>
        var BASE_URL = '<your-base-url>';
        var USERNAME = '<your-username>';
        var PASSWORD = '<your-password>';

The demo shows a plain HTML page that embeds the Appmixer UI widgets via the Appmixer JS SDK. Try to switch to different widgets using the select control at the top:

Study the source code of the demo.html file to understand how the Appmixer SDK can be used to embed Appmixer UI into your own page. As you can see, we start by authenticating the user:

appmixer.api.authenticateUser(USERNAME, PASSWORD).then((auth) => {
    appmixer.set('accessToken', auth.token);
    start();
});

Then we initialize the Appmixer UI widgets passing a reference to a <div> container element they will render in:

<div id="your-flow-manager"></div>
var flowManager = appmixer.ui.FlowManager({ el: '#your-flow-manager' });

Once our widgets are initialized, we can just call open() and close() methods to render the widgets inside their container or close them:

flowManager.open();

And react on events the UI widgets provide. For example, if the user clicks a flow inside the flow manager, the flow manager widget triggers the "flow:open" event which we can react on to open the designer for that flow:

flowManager.on('flow:open', (flowId) => {
    designer.set('flowId', flowId);
    flowManager.close();                        
    designer.open();
});

To revoke the authenticated user access, unset the access token:

appmixer.set('accessToken', null);

Webpack usage

We recommend to include Appmixer SDK as a script (the same way it is used in demo.html) whenever possible. This is because Appmixer SDK is too big to be processed as a module in a bundler like Webpack, which can lead to increased bundle processing times for both development and production environments.

Nevertheless, if your entry html file is being generated by Webpack using html-webpack-plugin, you would not be able to include manually the Appmixer SDK on a script tag. In this case you can use add-asset-html-webpack-plugin plugin to include it in your generated html file. Usage example:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
        }),

        new AddAssetHtmlPlugin({
            filepath: require.resolve('path/to/appmixer/appmixer.js'),
        }),
    ],
};

Getting Started

Click on the "Sign up" button to create a new account:

Once you have created an account, you should see a page that lists all your flows (3 sample flows are pre-populated in the Trial package):

Click on the "Create Flow" button, drag&drop components from the left palette to the canvas and connect them together by dragging lines from output ports of components to input ports of other components:

Then fill in all the details for the GetCurrentWeather component (in our case, we enter the city name):

Now we're ready to start our flow by clicking on the "Start Flow" button. Once you do that, you'll see the Designer shows you the flow is now in the running state. In this state, the flow can only be viewed but not edited. If you want to change the configuration of your flow, you have to stop it first and start again.

Congrats, you run your first flow! Now when you visit your Twitter account, you should see a new tweet:

NodeJS

This file exports a plain NodeJS module. Normally, this module exports virtual methods that the Appmixer engine understands (the most important one is the ). For now, we just leave the module empty. It's enough to create bare minimum for the engine to be able to work with our component.

Now you can pack and publish your component using the . When you then refresh the Designer page in your browser, you should see a new app in the left pane:

Custom App and Component
Custom Component Inspector panel

This is because we have defined an input port on our component but did not yet implement the method.

Our component behaviour will also change. We will call an external HTTP endpoint and pass the result to our output port. For a convenience, we will use a NodeJS library to send HTTP requests. Before we can start using the library, we have to install it first. This can simply be done by creating a package.json file with the and installing our library:

Custom Component Inspector panel
Tweet received from Appmixer Custom Component
Insights page

Replace <your-base-url> with http://localhost:2200 and <your-username> and <your-password> with the user credentials that you used in the guide to sign-up your first user. Now you can open the demo.html file in your browser. You should see something like this:

Appmixer SDK Demo
Insights page in Appmixer SDK Demo

To learn more about the Appmixer JavaScript SDK, please visit the section.

If you successfully installed the Appmixer self-managed package, you should be able to open the Appmixer front-end application at . You should see the sign-in page:

Sign-in page
Sign-up page
FlowManager page
Designer page

Our first flow contains the OnStart component that fires immediately when you click on the "Start Flow" button, GetCurrentWeather component that requests current weather information from the API and CreateTweet component that creates a new tweet. The right panel tells you what is the missing required configuration for the flow to be able to start. In our case, we need to authenticate to Twitter. Click on the Twitter component and fill in all the details. As you can see in the Inspector, we're entering the text of the tweet. You can use the "variables picker" to insert placeholders containing data from components back in the chain of connected components. These placeholders will be eventually replaced by real data once it is available (when the flow runs).

Inspector panel
Inspector panel
Running flow
Appmixer CLI tool
Axios
Node package manager
Getting Started
Appmixer SDK
http://localhost:8080
http://openweathermap.org
receive()
receive()

Using Appmixer API

The Appmixer API allows you to access all the features that the UI works with via a REST API. If you followed the "Getting Started" section, you should have a user signed-up in Appmixer. In order to access the data of the user via the API, you need to have their access token. Use the following API call to get the token (don't forget to replace the "abc@example.com" and "abc321" with your own user's username and password):

curl -XPOST "http://localhost:2200/user/auth" -d '{"username": "abc@example.com", "password": "abc321"}' -H 'Content-Type: application/json'

You should see a response that looks like this:

{"user":{"id":"5c88c7cc04a917256c726c3d","username":"abc@example.com","isActive":false,"email":"abc@example.com","plan":"free"},"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkzMTA0LCJleHAiOjE1NTUwODUxMDR9.sdU3Jt2MjmVuBNak0FwR0ETcjleRyiA6bqjMPA6f-ak"}

Copy the token and use it to authorize the user in your next API calls. For example, to get the number of flows the user created, use:

curl "http://localhost:2200/flows/count" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"

You should see a response that tells you how many flows the user has in their account:

{"count":5}

Please see the section to learn more about all the endpoints that you use.

API

ACL

Routes for setting ACL. Used in Backoffice.

Get ACL types

GET https://api.appmixer.com/acl-types

There are two types of access control lists, for components and for API routes. Restricted to admin users only.

[
    "routes",
    "components"
]

Get ACL rules for components|routes

GET https://api.appmixer.com/acl/:type

Get list of all the ACL rules for given type. Restricted to admin users only.

Path Parameters

Name
Type
Description

type

string

components | routes

[
    {
        "role": "admin",
        "resource": "*",
        "action": [
            "*"
        ],
        "attributes": [
            "non-private"
        ]
    },
    {
        "role": "user",
        "resource": "*",
        "action": [
            "*"
        ],
        "attributes": [
            "non-private"
        ]
    },
    {
        "role": "tester",
        "resource": "*",
        "action": [
            "*"
        ],
        "attributes": [
            "non-private"
        ]
    }
]

Update ACL rules

POST https://api.appmixer.com/acl/:type

Update ACL rule set for given type. Restricted to admin users only.

Path Parameters

Name
Type
Description

type

string

components | routes

Request Body

Name
Type
Description

array

Body has to be an array of ACL rules, where each rule has the following structure: { role: string - admin | user | tester ... resource: string - flows | appmixer.utils.* ... action: array of strings - * | read ... attributes: array of strings - * | non-private | ... }

Get available resource values

GET https://api.appmixer.com/acl/:type/resources

Get available values for resource property for an ACL rule. This is used for building UI in Backoffice for setting ACL rules. Restricted to admin users only.

Path Parameters

Name
Type
Description

type

string

components | routes

['*', 'flows']

Get available action values

GET https://api.appmixer.com/acl/:type/actions

Get available values for action property for an ACL rule. This is used for building UI in Backoffice for setting ACL rules. Restricted to admin users only.

Path Parameters

Name
Type
Description

type

string

components | routes

['*', 'read', '!read', 'create', '!create', 'update', '!update', 'delete', '!delete']

Get available options for attributes property.

GET https://api.appmixer.com/acl/:type/resource/:resource/attributes

Get available values for attributes property for an ACL rules. This is used for building UI in Backoffice for setting ACL rules. Restricted to admin users only.

Path Parameters

Name
Type
Description

type

string

components | routes

resource

string

resource name - flows, appmixer.utils.controls.*, ...

Custom Component Shapes

Fully customize appearance of components in diagrams.

Usage

Shape registration in the Appmixer SDK:

var appmixer = new Appmixer({
    componentShapes: {
        action: myCustomComponentShape,
        trigger: myCustomComponentShape,
        myUniqueShape: myCustomComponentShape
    },
});

Use "action" and "trigger" keys to override defaults. You can define a custom shape for any component in the system, shape reference in a component manifest looks like this:

{
    shape: "myUniqueShape"
}

Built-in shapes are action, trigger, actionVertical and triggerVertical.

Definition

Key

Description

options.updateCallback

An optional method called before the component is updated. Accepts element argument.

attributes

states

Definition for particular states of the component. The object structure is the same as the current scope (except for the "states" entry).

@active- the component is being modified

@invalid- the component configuration is invalid

@referenced- the component is highlighted

@unchecked- the component is selected

@checked - the component is deselected

@running - the flow is in a running state

ports.attributes

ports.states

Definition for particular states of individual ports.

The object structure is the same as the current scope (except for the "states" entry).

@connected - the port is connected

link.attributes

Special selectors

Dynamic properties of the component, such as label and icon, are mapped to optional selectors in the markup.

Selector

tagName

Description

label

text

Contains a label of the component.

icon

image

Contains an icon of the component.

element-halo-copy-tooltip

title

"Copy" button tooltip.

element-halo-cut-tooltip

title

"Cut" button tooltip.

element-halo-remove-tooltip

title

"Remove" button tooltip.

Special attributes

Key

Value

Description

event

"remove"

The entry acts as a remove button.

var myCustomComponentShape = {
    attributes: {
        size: { height: 60, width: 60 },
        markup: [{
            tagName: 'rect',
            selector: 'body',
        }, {
            tagName: 'text',
            selector: 'label'
        }, {
            tagName: 'image',
            selector: 'icon',
        }],
        attrs: {
            body: {
                event: 'remove', // Click on the "body" will remove the element
                refWidth: '100%',
                refHeight: '100%',
                stroke: 'black',
                strokeWidth: '4px',
                fill: 'white',
            },
            label: {
                ref: 'body',
                textAnchor: 'middle',
                refX: 0.5,
                refY: '100%',
                refY2: 12,
                fill: 'black',
                fontFamily: 'Helvetica, Arial, sans-serif',
            },
            icon: {
                ref: 'body',
                refX: 0.5,
                refY: 0.5,
                xAlignment: 'middle',
                yAlignment: 'middle',
                height: 30,
                width: 30,
            },
        },
    },
    ports: {
        attributes: {
            in: {
                attrs: {
                    '.port-label': {
                        fontFamily: 'Helvetica, Arial, sans-serif',
                        fontSize: 12,
                    },
                },
            },
            out: {
                attrs: {
                    '.port-label': {
                        fontFamily: 'Helvetica, Arial, sans-serif',
                        fontSize: 12,
                    },
                },
            },
        },
    },
    link: {
        attributes: {
            router: {
                name: 'metro',
            },
        },
        attrs: {
            line: {
                event: "remove", // Click on the line will remove the link
            },
        },
    },
    states: {
        '@active': {
            attributes: {
                attrs: {
                    body: {
                        stroke: 'blue',
                    },
                },
            },
            link: {
                attributes: {
                    attrs: {
                        line: {
                            stroke: 'blue',
                        },
                    },
                },
            },
        },
    },
};

Installation GCP

Installation of Appmixer Self-Managed Package on Google Cloud Platform

Apps

Get Apps

GET https://api.appmixer.com/apps

Returns all applications (services or modules) available. curl "https://api.appmixer.com/apps" -H "Authorization: Bearer [ACCESS_TOKEN]"

Get App Components

GET https://api.appmixer.com/apps/components

Returns all components of an app including their manifest files. curl "https://api.appmixer.com/apps/components?app=appmixer.dropbox" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Get All Components

GET https://api.appmixer.com/components

Get all available components. curl "https://api.appmixer.com/components" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Publish A Component/Module/Service

POST https://api.appmixer.com/components

Publish a new component, new module or an entire service or update an existing component/module/service. The uploaded entity must be zipped and must have a proper directory/file structure inside (See Basic Component Structure for more details). Note that you can use the Appmixer CLI tool to pack your component/service/module to a zip archive (appmixer pack command). Alternatively, you can also use a regular zip command line utility but you should omit the node_modules/ directories before archiving and the root directory of the zip archive must match the vendor name. The directory hierarchy must have a well defined structure: [vendor]/[service]/[module]/[component]. The size limit of the zip archive is 10MB. Publishing a new component can take more time than a lifespan of a single HTTP request. Therefore, the result of the publishing HTTP call is a JSON object with ticket property. You can use the ticket to check for progress of the publish using the /components/uploader/:ticket endpoint. curl -XPOST "https://api.appmixer.com/components" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-type: application/octet-stream" --data-binary @appmixer.myservice.zip

Check for Publishing Progress

GET https://api.appmixer.com/components/uploader/:ticket

Check for progress of publishing a component. curl -XGET "https://api.appmixer.com/components/uploader/2e9dd726-2b7f-46f7-bea4-8db7f7175aa8" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-type: application/json"

Path Parameters

Delete a Component/Module/Service

DELETE https://api.appmixer.com/components/:selector

Delete a component/module or an entire service. curl -XDELETE "https://api.appmixer.com/components/appmixer.myservice" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-type: application/json"

Path Parameters

Authentication

All API calls to Appmixer must include the Authorization header set to Bearer ACCESS_TOKEN.

Sign-in User

POST https://api.appmixer.com/user/auth

Sign-in a user with credentials and get their access token. curl -XPOST "https://api.appmixer.com/user/auth" -H "Content-type: application/json" -d '{ "username": "abc@example.com", "password": "abc321" }'

Request Body

Create User

POST https://api.appmixer.com/user

Create user. curl -XPOST "https://api.appmixer.com/user" -H "Content-type: application/json" -d '{ "username": "abc@example.com", "email": "abc@example.com", "password": "abc321" }'

Request Body

Get User Information

GET https://api.appmixer.com/user

Get user information. curl "https://api.appmixer.com/user" -H "Authorization: Bearer [ACCESS_TOKEN]"

System Webhooks

Appmixer Engine Events.

Component Errors

If you want to listen for component errors you can set the WEBHOOK_FLOW_COMPONENT_ERROR environment variable to a URL that the Appmixer engine will send an HTTP POST request to whenever an error occurs. It can you any external (but accessible) URL or you can even create a flow in Appmixer and point this URL to a Webhook component to handle these errors using Appmixer itself (e.g. by sending an email, Slack notification, ...).

Set the WEBHOOK_FLOW_COMPONENT_ERROR variable in the docker-compose.yml file:

Example of an error POSTed to your URL (a validation error in this case):

This error was triggered in a sample flow that looks like this:

The email address in the "To" configuration field of the SendEmail component points to a variable from the OnStart component. Its value is dynamic, it is only known during runtime. Therefore the user is allowed to start such a flow, but it will fail at runtime. Start time variable contains a date string, but the SendEmail address requires a valid email address in the To input field. The error will be logged and seen in the log viewer.

Consider the flow is configured correctly this time and the email address is valid. But there is a network error and Appmixer cannot send the email.

This error will be logged and the user will be able to see it in the log viewer or in the Insights. And the JSON representing this error will look like this:

Element attributes, see:

orts attributes of

Link attributes, see:

Follow instructions in this document:

Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description

Components might throw exceptions. Such exceptions/errors are logged and users can see them in the . However, you might want to implement your custom logic or notification system for when this occurs. You may want to notify your users or handle/log the errors in a totally custom way.

Manifest
https://docs.google.com/document/d/e/2PACX-1vRNIciiW_uIQnKIlpFrhsRNqAyHmY5fueyJXAGt2ARgUPkkdwZVMnquKt33Z3nPt1jjJ3xwsltdlT1R/pub
{
    "appmixer.asana": {
        "name": "appmixer.asana",
        "label": "Asana",
        "category": "applications",
        "description": "Asana is a collaborative information manager for workspace. It helps you organize people and tasks effectively.",
        "icon": "data:image/png;base64,iVBORw0KGgoA....kJggg=="
    },
    "appmixer.calendly": {
        "name": "appmixer.calendly",
        "label": "Calendly",
        "category": "applications",
        "description": "Calendly helps you schedule meetings without the back-and-forth emails. It does not work with the free Basic account. It works with Premium or Pro account.",
        "icon": "data:image/png;base64,iVBORw0KGgoA....kJggg=="
    },
    "appmixer.clearbit": {
        "name": "appmixer.clearbit",
        "label": "Clearbit",
        "category": "applications",
        "description": "Clearbit is a data API that lets you enrich your person and company records with social, demographic, and firmographic data.",
        "icon": "data:image/png;base64,iVBORw0KGgoA....kSuQmCC"
    },
    "appmixer.dropbox": {
        "name": "appmixer.dropbox",
        "label": "Dropbox",
        "category": "applications",
        "description": "Dropbox is a home for all your photos, documents, videos, and other files. Dropbox lets you access your stuff from anywhere and makes it easy to share with others.",
        "icon": "data:image/svg+xml;base64,PHN2Z....3N2Zz4="
    },
    "appmixer.evernote": {
        "name": "appmixer.evernote",
        "label": "Evernote",
        "category": "applications",
        "description": "Evernote is a powerful note taking application that makes it easy to capture ideas, images, contacts, and anything else you need to remember. Bring your life's work together in one digital workspace, available on all major mobile platforms and devices.",
        "icon": "data:image/png;base64,iVBORw0KGgoA....kSuQmCC"
    }
}

app

string

ID of an app as defined in service.json or module.json.

[
    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <david@client.io>",
        "icon": "data:image/png;base64,iVBORw...gg==",
        "description": "Send SMS text message through Twilio.",
        "auth": { "service": "appmixer:twilio" },
        "inPorts": [
            {
                "name": "message",
                "schema": {
                    "type": "object",
                    "properties": {
                        "body": { "type": "string" },
                        "to": { "type": "string" }
                    },
                    "required": [ "to" ]
                },
                "inspector": {
                    "inputs": {
                        "body": {
                            "type": "text",
                            "label": "Text message",
                            "tooltip": "Text message that should be sent.",
                            "index": 1
                        },
                        "to": {
                            "type": "text",
                            "label": "To number",
                            "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                            "index": 2
                        }
                    }
                }
            }
        ],
        "properties": {
            "schema": {
                "properties": {
                    "fromNumber": { "type": "string" }
                },
                "required": [ "fromNumber" ]
            },
            "inspector": {
                "inputs": {
                    "fromNumber": {
                        "type": "select",
                        "label": "From number",
                        "tooltip": "Select Twilio phone number.",
                        "index": 1,
                        "source": {
                            "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                            "data": {
                                "transform": "./transformers#fromNumbersToSelectArray"
                            }
                        }
                    }
                }
            }
        }
    },
    {
        "name": "appmixer.twilio.calls.NewCall",
        "author": "David Durman <david@client.io>",
        "icon": "data:image/png;base64,iVBORw...gg==",
        "description": "Receive a call through Twilio.",
        "auth": { "service": "appmixer:twilio" },
        "webhook": true,
        "webhookAsync": true,
        "outPorts": [
            {
                "name": "call",
                "options": []
            }
        ],
        "properties": {
            "schema": {
                "properties": {
                    "generateInspector": { "type": "boolean" },
                    "url": {}
                }
            },
            "inspector": {
                "inputs": {
                    "url": {
                        "source": {
                            "url": "/component/appmixer/twilio/calls/NewCall?outPort=call",
                            "data": {
                                "properties": {
                                    "generateInspector": true
                                }
                            }
                        }
                    }
                }
            }
        }
    }
]

manifest

string

If set to "yes", the endpoint returns all components including their manifest files.

[
    "appmixer.asana.projects.CreateProject",
    "appmixer.asana.projects.NewProject",
    "appmixer.asana.tasks.CreateStory",
    "appmixer.calendly.events.InviteeCanceled",
    "appmixer.calendly.events.InviteeCreated",
    "appmixer.clearbit.enrichment.FindCompany",
    "appmixer.clearbit.enrichment.FindPerson"
]
{
    "ticket":"a194d145-3768-4a8a-84a4-4f1e4e08c4ad"
}

ticket

string

Ticket that you got from the POST /component request.

// Successful upload. No errors:
{
  "finished": "2020-02-28T15:57:34.549Z"
}


// Upload finished. Errors encountered:
{
  "finished": "2020-02-28T15:25:39.515Z",
  "err": "Invalid schema for appmixer/utils/service.json",
  "data": [
    {
      "keyword": "required",
      "dataPath": "",
      "schemaPath": "#/required",
      "params": {
        "missingProperty": "label"
      },
      "message": "should have required property 'label'"
    },
    {
      "keyword": "required",
      "dataPath": "",
      "schemaPath": "#/required",
      "params": {
        "missingProperty": "description"
      },
      "message": "should have required property 'description'"
    }
  ]
}

selector

string

A selector that uniquely identifies the service/module/component to delete. The selector is of the form [vendor].[service].[module].[component]. For example "appmixer.google.calendar.CreateEvent" (removes just one component) or "appmixer.google.calendar" (removes the calendar module) or "appmixer.google" (removes the entire Google service including all modules and components).

curl "https://api.appmixer.com/apps" -H "Authorization: Bearer [ACCESS_TOKEN]"

password

string

Password.

username

string

Username.

{
    "user": {
        "id": "5c88c7cc04a917256c726c3d",
        "username":"abc@example.com",
        "isActive": false,
        "email": "abc@example.com", 
        "plan":"free"
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
}

password

string

Password.

email

string

Email address.

username

string

Email address.

{
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
}
{
  "id": "58593f07c3ee4f239dc69ff7",
  "username": "tomas@client.io",
  "isActive": true,
  "email": "tomas@client.io",
  "scope": [
    "user"
  ],
  "plan": "beta"
}
  engine:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      - mongodb
      - rabbitmq
      - elasticsearch
      - redis
    environment:
      # this variable has to point to your Webhook
      - WEBHOOK_FLOW_COMPONENT_ERROR=https://your.example.com/error-handling-ur
{
    "err": {
        "message": "Validation error on ports: in",
        "error": {
            "in": [
                [
                    {
                        "keyword": "format",
                        "dataPath": ".to",
                        "schemaPath": "#/properties/to/format",
                        "params": {
                            "format": "email"
                        },
                        "message": "should match format \"email\""
                    }
                ]
            ]
        },
        "code": "GRID_ERR_VAL_PORTS",
        "name": "ValidationFlowError",
        "stack": "ValidationFlowError: Validation error on ports: in\n    at MessagesProcessor.process (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/MessagesProcessor.js:67:19)\n    at Context.prepare (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/Context.js:68:31)\n    at ContextHandler.createContext (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/ContextHandler.js:60:17)\n    at async InputQueue.consume (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/InputQueue.js:151:23)\n    at async InputQueue.onMessage (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/AMQPClient.js:378:20)"
    },
    "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
    "flowName": "New flow",
    "userId": "5f804b96ea48ec47a8c444a7",
    "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
    "componentType": "appmixer.utils.email.SendEmail",
    "inputMessages": {
        "in": [
            {
                "properties": {
                    "correlationId": "339bc448-a806-4e61-8d38-4211fcedaf12",
                    "gridInstanceId": null,
                    "contentType": "application/json",
                    "contentEncoding": "utf8",
                    "sender": {
                        "componentId": "e8c581b4-9985-4f2c-bf30-895bf1d5541b",
                        "type": "appmixer.utils.controls.OnStart",
                        "outputPort": "out"
                    },
                    "destination": {
                        "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
                        "inputPort": "in"
                    },
                    "correlationInPort": null,
                    "componentHeaders": {},
                    "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
                    "messageId": "47293e2c-4e33-4558-8805-386de392ef04",
                    "flowRunId": 1607683798995
                },
                "content": {
                    "to": "2020-12-11T10:49:59.050Z"
                },
                "scope": {
                    "e8c581b4-9985-4f2c-bf30-895bf1d5541b": {
                        "out": {
                            "started": "2020-12-11T10:49:59.050Z"
                        }
                    }
                },
                "originalContent": {
                    "started": "2020-12-11T10:49:59.050Z"
                }
            }
        ]
    },
    "accessTokenId": null
}
{
    "err": {
        "stack": "Error: getaddrinfo ENOTFOUND mandrillapp.com\n    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)",
        "message": "getaddrinfo ENOTFOUND mandrillapp.com",
        "errno": "ENOTFOUND",
        "code": "ENOTFOUND",
        "syscall": "getaddrinfo",
        "hostname": "mandrillapp.com",
        "name": "Error"
    },
    "flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
    "flowName": "Invalid email test",
    "userId": "5fd744d5e9ed7d0011ca35f9",
    "componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
    "componentType": "appmixer.utils.email.SendEmail",
    "inputMessages": {
        "in": [
            {
                "properties": {
                    "correlationId": "254ad628-f9c1-4483-81ed-33a22ac3ddc6",
                    "gridInstanceId": null,
                    "contentType": "application/json",
                    "contentEncoding": "utf8",
                    "sender": {
                        "componentId": "bcbeda1d-9036-45af-a25d-a57cf06e3f90",
                        "type": "appmixer.utils.controls.OnStart",
                        "outputPort": "out"
                    },
                    "destination": {
                        "componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
                        "inputPort": "in"
                    },
                    "correlationInPort": null,
                    "componentHeaders": {},
                    "flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
                    "messageId": "33b9fcbf-b326-4eb4-bba6-cf34979f4ba2",
                    "flowRunId": 1607944841843,
                    "quotaId": "qs-4882d03d-65e1-44dc-983e-d2a33071779d"
                },
                "content": {
                    "to": [
                        {
                            "email": "martin@client.io",
                            "type": "to"
                        }
                    ],
                    "from_email": "no-reply@appmixer.com"
                },
                "scope": {
                    "bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
                        "out": {
                            "started": "2020-12-14T11:20:41.858Z"
                        }
                    }
                },
                "originalContent": {
                    "started": "2020-12-14T11:20:41.858Z"
                }
            }
        ]
    },
    "accessTokenId": null
}
jointjs.dia.Cell.define
P
joint.dia.Element.ports.interface
jointjs.dia.Link
Insights
Log viewer in Designer.

Files

Appmixer allows you to upload files to use them in your flows.

Get file info

GET https://api.appmixer.com/files/metadata/:fileId

Get the information for the specified file. Note that the file content is not included.

Path Parameters

Name
Type
Description

fileId

string

The UUID of the required file.

{
    "length": 146737,
    "chunkSize": 261120,
    "uploadDate": "2020-07-24T19:19:49.755Z",
    "filename": "chart1.png",
    "md5": "1d0ed5eb2cacbab4526de272837102dd",
    "metadata": {
        "userId": "5f15d59cef81ecb3344fab55"
    },
    "contentType": "image/png",
    "fileId": "f179c163-2ad8-4f9d-bce5-95200691b7f9"
}

Upload a file

POST https://api.appmixer.com/files

Upload file to Appmixer. Uploads by chunks are supported, and whether if sent file is treated as a chunk or a new file depends on the headers sent. Also, Content-type must be set to multipart/form-data.

Headers

Name
Type
Description

Content-type

string

Must be set to multipart/form-data

uploader-file-id

string

If set, the current file will be appended as a chunk to the file specified by this. If not present, a new file will be created. The response includes the resulting file's ID, so it can be used in subsequent requests to add chunks to the generated file.

uploader-file-name

string

The name of the file. This will be ignored in case that uploader-file-id header is present.

uploader-chunk-size

string

The size in bytes of the file/chunk being uploaded.

uploader-chunk-number

string

This header is uploader-chunk-number. The ordinal number of this chunk - e.g. the first chunk is 1, the second is 2 and so on.

uploader-chunks-total

string

This header is uploader-chunks-total. The total number of chunks that compose the file. If set to 1, it means that this is the complete file

Request Body

Name
Type
Description

file

string

The file/chunk to be uploaded

{
  "length": 146737,
  "chunkSize": 261120,
  "uploadDate": "2020-07-24T20:23:38.565Z",
  "filename": "chart1.png",
  "md5": "1d0ed5eb2cacbab4526de272837102dd",
  "metadata": {
    "userId": "5f15d59cef81ecb3344fab55"
  },
  "contentType": "image/png",
  "fileId": "8cb8fed0-0cd8-4478-8372-9f7cb4eb16e3"
}

Data Stores

Access Data Stores (built-in key-value store).

Get All Stores

GET https://api.appmixer.com/stores

Get all key-value stores. curl "https://api.appmixer.com/stores" -H "Authorization: Bearer [ACCESS_TOKEN]"

[{
    "name": "My Store 1",
    "storeId": "5c6fc9932ff3ff000747ead4"
}, {
    "name": "My Store 2",
    "storeId": "2a3fc9512bb3fca23747lai2"
}]

Get One Store metadata

GET https://api.appmixer.com/stores/:id

Get name of a store. curl "https://api.appmixer.com/stores/5c6fc9932ff3ff000747ead4" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

id

string

Store ID.

{
    "name": "My Store 1",
    "storeId": "5c6fc9932ff3ff000747ead4"
}

Get Number of Records in a Store

GET https://api.appmixer.com/store/count

Get number of records in a store. curl "https://api.appmixer.com/store/count?storeId=5c6fc9932ff3ff000747ead4" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

storeId

string

Store ID.

{
    "count": 681
}

Get Store Records

GET https://api.appmixer.com/store

Get records. Supports search and pagination. curl "https://api.appmixer.com/store?storeId=5b213e0ef90a6200113abfd4&offset=0&limit=30&sort=updatedAt:-1" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

storeId

string

Store ID.

sort

string

Store record parameter to sort by. Followed by ":" and the sort order -1 (descending) or 1 (ascending).

offset

number

Index of the first record returned.

limit

number

Maximum number of records returned.

[{
    "key":"Box 8RE1",
    "storeId":"5b214ba6f90a6200113abfd8",
    "userId":"583c06511afb7b0016ef120b",
    "updatedAt":"2019-03-06T10:02:20.419Z",
    "value":"321",
    "createdAt":"2019-03-06T10:02:20.419Z"
},{
    "key":"T-shirt T41B",
    "storeId":"5b214ba6f90a6200113abfd8",
    "userId":"583c06511afb7b0016ef120b",
    "updatedAt":"2019-03-06T10:01:59.360Z",
    "value":"18",
    "createdAt":"2019-03-06T10:01:59.360Z"
},{
    "key":"T-shirt A12C",
    "storeId":"5b214ba6f90a6200113abfd8",
    "userId":"583c06511afb7b0016ef120b",
    "updatedAt":"2019-03-06T10:01:45.204Z",
    "value":"12",
    "createdAt":"2019-03-06T10:01:45.204Z"
}]

Create a new Store

POST https://api.appmixer.com/stores

Create a new key-value store. Returns the newly created Store ID. curl -XPOST "https://api.appmixer.com/stores" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "name": "My Store" }'

Request Body

Name
Type
Description

name

string

Name of the store.

"5c7f9bfe51dbaf0007f08db0"

Delete a Store

DELETE https://api.appmixer.com/stores/:id

Delete a store and all the records in the store. curl -XDELETE "https://api.appmixer.com/stores/5c7f9bfe51dbaf0007f08db0" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

id

string

Store ID.

Rename a Store

PUT https://api.appmixer.com/stores/:id

Rename an existing store. curl -XPUT "https://api.appmixer.com/stores/5c7f9bfe51dbaf0007f08db0" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "name": "My New Name" }'

Path Parameters

Name
Type
Description

id

string

Store ID.

Request Body

Name
Type
Description

name

string

New name of the store.

{
    "oldName":"My Old Store Name",
    "storeId":"5c7f9bfe51dbaf0007f08db0"
}

Create a new Store Item

POST https://api.appmixer.com/stores/:id/:key

Create a new value in the store under a key. curl -XPOST "https://api.appmixer.com/stores/5c7f9bfe51dbaf0007f08db0/mykey" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: text/plain" -d "my value"

Path Parameters

Name
Type
Description

key

string

Key under which the posted value will be stored.

id

string

Store ID.

Request Body

Name
Type
Description

string

Value to store under the key.

{
    "key":"mykey",
    "value":"myvalue",
    "createdAt":"2019-03-06T10:17:58.796Z",
    "updatedAt":"2019-03-06T10:17:58.796Z"
}

Delete Store Items

DELETE https://api.appmixer.com/store

Delete one or multiple items from a store. curl -XDELETE "https://api.appmixer.com/store" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '[{ key: "mykey", storeId: "5c7f9bfe51dbaf0007f08db0" }, { "key": "mykey2", "storeId": "5c7f9bfe51dbaf0007f08db0" }]'

Request Body

Name
Type
Description

items

array

Array of items to delete. Each item is an object of the form { key, storeId }.

{
    "deletedCount": 1
}

Modifiers

Get Modifiers

GET https://api.appmixer.com/modifiers

Get available modifiers. Appmixer is shipped by default with its own set of modifiers by default. Checkout this API to see what they are. You can then redefine the default set with the following API endpoint.

{
    "categories": {
        "object": {
            "label": "Object",
            "index": 1
        },
        "list": {
            "label": "List",
            "index": 2
        },
        ...
    },
    "modifiers": {
        "g_stringify": {
            "name": "stringify",
            "label": "Stringify",
            "category": [
                "object",
                "list"
            ],
            "description": "Convert an object or list to a JSON string.",
            "arguments": [
                {
                    "name": "space",
                    "type": "number",
                    "isHash": true
                }
            ],
            "returns": {
                "type": "string"
            },
            "helperFn": "function(value, { hash }) {\n\n                return JSON.stringify(value, null, hash.space);\n            }"
        },
        "g_length": {
            "name": "length",
            "label": "Length",
            "category": [
                "list",
                "text"
            ],
            "description": "Find length of text or list.",
            "arguments": [],
            "returns": {
                "type": "number"
            },
            "helperFn": "function(value) {\n\n                return value && value.hasOwnProperty('length') ? value.length : 0;\n            }"
        },
        ...
    }
    
}

Edit Modifiers

PUT https://api.appmixer.com/modifiers

Change the modifiers and their categories as a whole. Restricted to admin users only. Before editing existing modifiers or adding new ones, checkout the GET /modifiers API to see the structure.

Request Body

Name
Type
Description

categories

object

The object containing available modifier categories. Each category is composed by the following properties: - label: label to be shown in the Modifier Editor. It can be overridden by string customization. - index: position on which this category is displayed in the Modifier Editor.

modifiers

object

The object containing available modifiers. Each modifier is composed of the following properties: - label: label to be shown in the Modifier Editor. Can be overridden by string customization. - category: an array containing the categories which the modifier will appear under. - description: short description of the modifier. It will appear under modifier's label. Can be overridden by string customization. - arguments (optional): an array of objects describing the arguments for the modifier if there is any. For each argument, an inspector field is generated in the editor. The structure of each object is { name: String, type: String, isHash: Boolean, Optional } - returns (optional): an object with just one property type indicating the expected return type. - isBlock (optional): boolean. Indicates if this is a block modifier. - private (optional): boolean. Indicates if the modifier is private and therefore not available for users. - variableArguments (optional): boolean. If set to true the modifier accepts any number of arguments. The inspector will render an "Add" button to add as many arguments as the user wants. - helperFn: the javascript function as a string, which does the transformation of the variable. The function definition can have arguments being the first one always the variable value, and the subsequent each of the modifier's arguments, in the same order they are defined on arguments array.

{
    "categories": {
        "object": {
            "label": "Object",
            "index": 1
        },
        "list": {
            "label": "List",
            "index": 2
        },
        ...
    },
    "modifiers": {
        "g_stringify": {
            "name": "stringify",
            "label": "Stringify",
            "category": [
                "object",
                "list"
            ],
            "description": "Convert an object or list to a JSON string.",
            "arguments": [
                {
                    "name": "space",
                    "type": "number",
                    "isHash": true
                }
            ],
            "returns": {
                "type": "string"
            },
            "helperFn": "function(value, { hash }) {\n\n                return JSON.stringify(value, null, hash.space);\n            }"
        },
        "g_length": {
            "name": "length",
            "label": "Length",
            "category": [
                "list",
                "text"
            ],
            "description": "Find length of text or list.",
            "arguments": [],
            "returns": {
                "type": "number"
            },
            "helperFn": "function(value) {\n\n                return value && value.hasOwnProperty('length') ? value.length : 0;\n            }"
        },
        ...
    }
    
}

Delete Modifiers

DELETE https://api.appmixer.com/modifiers

Delete all modifiers. Restricted to admin users only.

{}

Test Modifier Function

POST https://api.appmixer.com/modifiers/test

This endpoint can be used to test a helper function with the desired arguments, to check how it will behave under different conditions. curl -XPOST "https://api.appmixer.com/modifiers/test" -H "Content-type: application/json" -H "Authorization: Bearer [ACCESS-TOKEN]" -d '{ "helperFn": "function(value) { return value && value.hasOwnProperty('\''length'\'') ? value.length : 0; }", "arguments": ["test"]}'

Request Body

Name
Type
Description

helperFn

string

The Javascript helper function as a string.

arguments

array

The arguments to be passed to the helper function.

4
{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "The function returned \"undefined\""
}

People Task

Each task carries an email address of the requester and approver of the task together with title, description and due date. Tasks can have registered webhooks that the Appmixer engine calls when the status of the task changes (pending -> approved and pending -> rejected). Components can register these webhooks and trigger their outputs based on the result of the task resolution.

Get Tasks

GET https://api.appmixer.com/people-task/tasks

Return all tasks of a user.

Query Parameters

Name
Type
Description

secret

string

Approver or requester secret. This is the secret that you get in the approverSecret or requesterSecret property when you create a new task. This secret allows you to list tasks of any user for which you have the secret (instead of just the user identified by the token in the Authorization header).

limit

number

Maximum items returned. Default is 100. Useful for paging.

offset

number

The index of the first item returned. Default is 0. Useful for paging.

projection

string

Exclude task object properties. Example: "-description".

sort

string

Sorting parameter. Can be any task object property followed by semicolon and 1 (ascending), -1 (descending). Example: "decisionBy:-1".

filter

string

Filter tasks by their property values. Example: "status:pending" returns only pending tasks.

pattern

string

Filter tasks by a search term (searches the tasks title).

role

string

Filter tasks by role ("approver" or "requester").

 [{
    "approver": "approver@example.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "requester@example.com",
    "status": "pending",
    "title": "Example title",
    "id": "5da9ed9ff29cd51c5fa27380"
}, {
    "approver": "approver@example.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "requester@example.com",
    "status": "pending",
    "title": "Example title",
    "id": "5da9ed9ff29cd51c5fa27380"
}]

Get Task Count

GET https://api.appmixer.com/people-task/tasks-count

Get the number of all tasks of a user.

Query Parameters

Name
Type
Description

secret

string

Approver or requester secret. This is the secret that you get in the approverSecret or requesterSecret property when you create a new task. This secret allows you to list tasks of any user for which you have the secret (instead of just the user identified by the token in the Authorization header).

filter

string

Filter tasks by their property values. Example: "status:pending" returns only pending tasks.

pattern

string

Filter tasks by a search term (searches the tasks title).

role

string

Filter tasks by role ("approver" or "requester").

{
    "count": 23
}

Get Task

GET https://api.appmixer.com/people-task/tasks/:id

Get a task detail.

Path Parameters

Name
Type
Description

id

string

ID of a task.

{
    "approver": "approver@example.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "requester@example.com",
    "status": "pending",
    "title": "Example title",
    "id": "5da9ed9ff29cd51c5fa27380"
}

Create a New Task

POST https://api.appmixer.com/people-task/tasks

Request Body

Name
Type
Description

decisionBy

string

Date by which the task is due. ISO 8601 format.

status

string

Status of the task. One of "pending", "approved" or "rejected".

description

string

The description of the task.

title

string

The title of the task.

requester

string

Requester's email address.

approver

string

Approver's email address.

{
    "approver": "approver@example.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "requester@example.com",
    "status": "pending",
    "public": true,
    "title": "Example title",
    "approverSecret": "3dbd67d6db7a2c45b413ed7a0788175e764c4a7d11d44289bd2706e09ea4318f",
    "requesterSecret": "440197b197b9743b457172d37bcf98db3e006644657f9e19192efcc428125aae",
    "id": "5da9ed9ff29cd51c5fa27380"
}

Register a Webhook for a Task

POST https://api.appmixer.com/people-task/webhooks

Register a new webhook URL for a task. Appmixer will send a POST request to this URL whenever the status of the task changes. This is usually done right after creating a new task so that you can get notified as soon as the task gets approved or rejected.

Request Body

Name
Type
Description

url

string

URL to be triggered when the status of the task changes.

taskId

string

ID of a task.

{
    "url": "https://api.appmixer.com/flows/551945f2-6bbb-4ea4-a792-fb3635943372/components/a54b47fa-e7ce-463f-87bc-715ceb612947?correlationId=ee325876-4b3e-4537-9a12-58b4929f6cd8&correlationInPort=task",
    "taskId": "1234",
    "status": "pending",
    "id": "5da9ee4cf29cd51c5fa27381"
}

Delete a Webhook

GET https://api.appmixer.com/people-task/webhooks/:id

Delete a previously registered webhook.

Path Parameters

Name
Type
Description

id

string

Webhook ID, i.e. the id returned from the /people-task/webhooks when registering a new webhook.

Edit a Task

PUT https://api.appmixer.com/people-task/tasks/:id

Edit an existing task.

Path Parameters

Name
Type
Description

id

string

Id of a task.

Request Body

Name
Type
Description

decisionBy

string

Date by which the task is due. ISO 8601 format.

status

string

The status of the task. One of "pending", "approved" or "rejected".

description

string

The description of the task.

title

string

The title of the task.

requester

string

Requester's email address.

approver

string

Approver's email address.

{
    "approver": "approver@example.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "requester@example.com",
    "status": "pending",
    "title": "Example title",
    "id": "5da9ed9ff29cd51c5fa27380"
}

Approve a Task

PUT https://api.appmixer.com/people-task/tasks/:id/approve

This endpoint approves a task triggering all the registered webhooks for this task announcing a new approved status.

Path Parameters

Name
Type
Description

id

string

ID of a task.

Request Body

Name
Type
Description

secret

string

Approver secret. This is the secret that you get in the approverSecret property when you create a new task. This secret allows you to approve a task of any user for which you have the secret.

{
    "id": "5da9ed9ff29cd51c5fa27380",
    "approver": "e2etest@gmail.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "e2etest@gmail.com",
    "status": "approved",
    "title": "Example title"
}

Reject a Task

PUT https://api.appmixer.com/people-task/tasks/:id/reject

This endpoint rejects a task triggering all the registered webhooks for this task announcing a new rejected status.

Path Parameters

Name
Type
Description

id

string

ID of a task.

Request Body

Name
Type
Description

secret

string

Approver secret. This is the secret that you get in the approverSecret property when you create a new task. This secret allows you to reject a task of any user for which you have the secret.

{
    "id": "5da9ed9ff29cd51c5fa27380",
    "approver": "e2etest@gmail.com",
    "decisionBy": "2020-01-01 19:00:00",
    "description": "Example description",
    "requester": "e2etest@gmail.com",
    "status": "rejected",
    "title": "Example title"
}

Appmixer Architecture

High-Level Architecture

Appmixer consists of multiple technologies that interact with each other:

  • Appmixer Engine: the main system that manages flows, orchestrates components within running flows and provides a HTTP REST API interface to access all the entities within Appmixer. The Appmixer engine is implemented in NodeJS and can run on a single node or in cluster. The engine is horizontally scalable providing high availability and fault tolerance capabilities.

  • Appmixer UI SDK: the JavaScript HTML 5 SDK that allows to seamlessly embed any of the Appmixer UI widgets (including the drag&drop flow designer) to any 3rd party web page. The SDK communicates with the engine via REST API.

  • Appmixer Backoffice: the admin panel UI providing overview of all the flows and users in Appmixer together with configuration.

  • Appmixer CLI: the command line tool that is mainly used to manage custom connectors (upload, delete, ...).

  • Supporting Technologies: RabbitMQ message broker, MongoDB NoSQL database, ElasticSearch search engine, Logstash collector of logs and Redis key-value store. All the supporting technologies can also run either on a single node or in cluster.

Charts

Create Chart

POST https://api.appmixer.com/charts

This method is not meant to be implemented by applications embedding Appmixer SDK. Creating chart requires complex objects (options, query, traces), their structure goes beyond this documentation. appmixer.ui.InsightsChartEditor SDK component should be used to build Charts.

Request Body

Update Chart

PUT https://api.appmixer.com/charts/:chartId

The same properties as in Create Chart API endpoint.

Path Parameters

Get Charts

GET https://api.appmixer.com/charts

Get a list of all charts the user has configured in their Insights Dashboard.

Query Parameters

Get One Chart

GET https://api.appmixer.com/charts/:id

Path Parameters

Delete a Chart

DELETE https://api.appmixer.com/charts/:id

Path Parameters

The People Task Appmixer module provides an API that lets you create tasks that can be approved or rejected by other people. This module is used by the module in combination with the appmixer.utils.tasks.RequestApproval and appmixer.utils.tasks.RequestApprovalEmail components. Please see the for more details.

Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
appmixer.ui.PeopleTask UI SDK
People Tasks tutorial

traces

object

The aggregations that are represented on the chart along with their sources (flows, components).

query

object

Object representing time range for the chart.

options

object

Object with the visualization options for the chart.

index

number

The position of the chart in the dashboard.

type

string

Type of the chart. bar, line, scatter, area, pie

name

string

Name of the chart.

{
    chartId: '5defb3901f17d98d974fbb00'
}
{
    "name":"Test create chart",
    "index":1,
    "type":"bar",
    "options":{
        "layout":{
            "showlegend":true,
            "xaxis":{
                "showline":true
            },
            "yaxis":{
                "showline":false
            },
            "barmode":"group",
            "bargap":0.2,
            "bargroupgap":0.1
        },
        "showgrid":true,
        "showticklabels":true,
        "horizontal":false
    },
    "query":{
        "range":{
            "from":{
                "endOf":null,
                "startOf":"day",
                "subtract":[7,"day"]
            },
            "to":{}
        }
    },
    "traces":{
        "2f985875-4149-4c7b-a4ab-e204269c0c0f":{
            "name":"Trace 1",
            "hidden":false,
            "agg":{
                "date_histogram":{
                    "interval":"1d",
                    "min_doc_count":"0",
                    "field":"@timestamp"
                }
            },
            "options":{
                "type":"bar",
                "linewidth":0,
                "opacity":1
            },
            "source":{
                "type":"flow",
                "targets":{
                    "dbd206a4-23b3-44a4-a6c4-59db74aa3fb5":[]
                }
            }
        }
    }
}

string

{
    chartId: "5defa30cbd1ca06288202346"
    index: 1
    mtime: "2019-12-10T13:52:12.288Z"
    name: "Updated Chart"
    options: {,…}
    query: {,…}
    traces: {,…}
    type: "bar"
    userId: "5dee76c19462fe6b3fd42d79"   
}

pattern

string

Regex that will be used to match name property.

limit

number

Maximum items returned. Default is 100. Used for paging.

offset

number

The index of the first item returned. Default is 0. Used for for paging.

sort

string

Sorting parameter. Can be any chart object property followed by semicolon and 1 (ascending) or -1 (descending). Example: "mtime:-1".

projection

string

Exclude chart object properties. Example: "-traces".

[
    {
        "chartId": "5defa30cbd1ca06288202346",
        "userId": "5dee76c19462fe6b3fd42d79",
        "name": Chart 1",
        "index": 0,
        "type": "bar",
        "query": { ... },
        "options": { ... },
        "traces": { ... },
        "mtime": "2019-12-10T13:52:12.288Z"
    },
    {
        "chartId": "5defa30cbd1ca06288202347",
        "userId": "5dee76c19462fe6b3fd42d79",
        "name": Chart 2",
        "index": 0,
        "type": "bar",
        "query": { ... },
        "options": { ... },
        "traces": { ... },
        "mtime": "2019-13-10T13:52:12.288Z"
    }
]

id

string

ID of the chart to return.

{
        "chartId": "5defa30cbd1ca06288202346",
        "userId": "5dee76c19462fe6b3fd42d79",
        "name": Chart 1",
        "index": 0,
        "type": "bar",
        "query": { ... },
        "options": { ... },
        "traces": { ... },
        "mtime": "2019-12-10T13:52:12.288Z"
}

id

string

ID of a chart.

Insights

Get list of all messages passing through your flows and usage information (telemetry).

Get Logs and Histogram

GET https://api.appmixer.com/logs

Get logs for a single flow or list of flows or all user's flows. Filtering and sorting supported. Logs contain data messages getting into component's input port(s) and messages sent to component's output port(s). They also contain any errors that occurred during flow run or while trying to start/stop a flow. curl "https://api.appmixer.com/logs?from=0&size=30&sort=@timestamp:desc&query=@timestamp:[2019-03-04+TO+2019-03-08]&flowId=9c4673d7-a550-45a2-91c1-ad057fac0385" -H "Authorization: Bearer [ACCESS_TOKEN]" curl "https://api.appmixer.com/logs?from=0&size=30&sort=@timestamp:desc&query=@timestamp:[2019-03-04+TO+2019-03-08]" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

portType

string

string: in or out . Or it can be array portType=in&portType=out. Used to filter only input messages or output messages. in and out by default.

flowId

string

The flow ID to filter on. This parameter can be used multiple times to filter on more flows. If not present, it will return logs for all user's flows (even flows that are being shared with signed in user).

exclude

string

A comma separated field names to exclude from the log objects returned.

query

string

Query string. Uses the Lucene query syntax: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html

sort

string

A parameter to sort the result. Optionally followed by ":desc" to change the order. asc by default.

size

number

Maximum number of logs returned. Useful for pagination. 50 records by default.

from

number

Index of the first log returned. Useful for pagination.

{
  "buckets": [
   {
     "key_as_string": "2018-04-16T00:00:00.000Z",
     "key": 1523836800000,
     "doc_count": 35
   },
   {
     "key_as_string": "2018-04-17T00:00:00.000Z",
     "key": 1523923200000,
     "doc_count": 60
   }
  ],
  "hits": [
    {
      "severity": "info",
      "componentType": "appmixer.slack.list.SendChannelMessage",
      "componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
      "portType": "in",
      "senderId": "c062e744-2de1-4c80-afce-713be3145315",
      "@timestamp": "2018-04-06T14:02:04.517Z",
      "port": "message",
      "senderType": "appmixer.utils.controls.OnStart",
      "correlationId": "a5128135-3a23-4837-92f8-9dc099ff0700",
      "id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
      "gridTimestamp": "2018-04-06T14:02:04.472Z",
      "flowId": "339d216c-48e0-4110-9210-a4c176b30f84",
      "entity": "input-queue",
      "_id": "AWKbQ6Vr9I6rzDWu4NbG",
      "_index": "appmixer-201804"
    },
    {
      "severity": "info",
      "componentType": "appmixer.slack.list.SendChannelMessage",
      "componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
      "portType": "in",
      "senderId": "c062e744-2de1-4c80-afce-713be3145315",
      "@timestamp": "2018-04-03T20:22:10.971Z",
      "port": "message",
      "senderType": "appmixer.utils.controls.OnStart",
      "correlationId": "7ed0bbb4-0b05-4469-8168-401cd909e5d2",
      "id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
      "gridTimestamp": "2018-04-03T20:22:10.927Z",
      "flowId": "339d216c-48e0-4110-9210-a4c176b30f84",
      "entity": "input-queue",
      "_id": "AWKNLJEg9I6rzDWu3F8E",
      "_index": "appmixer-201804"
    }
  ]
}

Get Log Detail

GET https://api.appmixer.com/log/:logIndex/:logId

Get a detail of a log. Log detail gives you information on the actual data of the message between two components. curl "https://api.appmixer.com/log/93198d48-e680-49bb-855c-58c2c11d1857/appmixer-201804/AWKbQ6Vr9I6rzDWu4NbG" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

logId

string

Log ID. Use the "_id" property of the log object returned from flow logs.

logIndex

string

Log index. Use the "_index" property of the log object returned from flow logs.

{
  "_index": "appmixer-201804",
  "_type": "engine",
  "_id": "AWKbQ6Vr9I6rzDWu4NbG",
  "_version": 1,
  "_source": {
    "severity": "info",
    "msg": {
      "text": "Hey Slack!"
    },
    "componentType": "appmixer.slack.list.SendChannelMessage",
    "componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
    "bundleId": "86a83327-1b13-4cab-a7cd-bbcce5f2402d",
    "portType": "in",
    "senderId": "c062e744-2de1-4c80-afce-713be3145315",
    "@timestamp": "2018-04-06T14:02:04.517Z",
    "port": "message",
    "@version": "1",
    "senderType": "appmixer.utils.controls.OnStart",
    "correlationId": "a5128135-3a23-4837-92f8-9dc099ff0700",
    "id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
    "gridTimestamp": "2018-04-06T14:02:04.472Z",
    "flowId": "339d216c-48e0-4110-9210-a4c176b30f84"
  }
}

Get Logs (Aggregations)

POST https://api.appmixer.com/logs

This method works the same as its /GET counterpart, but it also allows to get aggregations within the matched data, by passing a request body specifying desired aggregations.

Query Parameters

Name
Type
Description

flowId

string

The flow ID to filter on. This parameter can be used multiple times to filter on more flows.

exclude

string

A comma separated field names to exclude from the log objects returned.

query

string

Query string. Uses the Lucene query syntax: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html

sort

string

A parameter to sort by. Optionally followed by ":desc" to change the order.

size

number

Maximum number of logs returned. Useful for pagination.

from

number

Index of the first log returned. Useful for pagination.

Request Body

Name
Type
Description

aggs

object

An object describing the desired aggregations. Uses Elasticsearch aggregation search structure: https://elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html

{
    "aggregations": {
        "avg_price": {
            "value": 10
        },
        "sum_income": {
            "value": 2000
        }
    },
    "hits": [
        { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
        { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
        ...
    ]
}

Get Usage Information

GET https://api.appmixer.com/telemetry

Get usage information. curl "https://api.appmixer.com/telemetry?from=2018-03-17&to=2018-04-17" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

to

string

To date.

from

string

From date.

{
  "messageCounts": {
    "from": "2018-03-17",
    "to": "2018-04-17",
    "count": 348,
    "userId": "58593f07c3ee4f239dc69ff7"
  },
  "runningFlows": {
    "userId": "58593f07c3ee4f239dc69ff7",
    "count": 4
  },
  "activeConnectors": {
    "userId": "58593f07c3ee4f239dc69ff7",
    "count": 8
  },
  "usedApps": [
    "appmixer.utils",
    "appmixer.slack",
    "appmixer.asana",
    "appmixer.salesforce",
    "appmixer.twilio"
  ]
}

Unprocessed Messages

Unprocessed MessagesAfter a message fail to be processed a certain number of retries, Appmixer stops trying to process the message and stores it. You can fetch, delete, and even retry those messages

Get messages

GET https://api.appmixer.com/unprocessed-messages

Get the list of the unprocessed messages for the current user.

Path Parameters

Name
Type
Description

string

[
    {
        "messageId": "a9b78d3c-ec9a-4c0e-81c2-b1df12bd46d7",
        "flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
        "componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
        "messages": {
            "in": [
                {
                    "properties": {
                        "correlationId": "0dcb7b2a-5933-481a-bb9c-c08a865656c0",
                        "gridInstanceId": null,
                        "contentType": "application/json",
                        "contentEncoding": "utf8",
                        "sender": {
                            "componentId": "3961d498-83f8-4714-85ba-0539d3055892",
                            "type": "appmixer.utils.controls.OnStart",
                            "outputPort": "out"
                        },
                        "destination": {
                            "componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
                            "inputPort": "in"
                        },
                        "correlationInPort": null,
                        "componentHeaders": {},
                        "flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
                        "messageId": "12374d7e-5c66-40d1-8772-37c424bd4182",
                        "flowRunId": 1603726768950
                    },
                    "content": {
                        "key": "hello"
                    },
                    "scope": {
                        "3961d498-83f8-4714-85ba-0539d3055892": {
                            "out": {
                                "started": "2020-10-26T15:39:29.003Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-10-26T15:39:29.003Z"
                    }
                }
            ]
        },
        "created": "2020-10-26T15:39:29.097Z",
        "err": //Stringyfied error object...
    }
]

Get message

GET https://api.appmixer.com/unprocessed-messages/:messageId

Get a single message.

Path Parameters

Name
Type
Description

messageId

string

{
        "messageId": "a9b78d3c-ec9a-4c0e-81c2-b1df12bd46d7",
        "flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
        "componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
        "messages": {
            "in": [
                {
                    "properties": {
                        "correlationId": "0dcb7b2a-5933-481a-bb9c-c08a865656c0",
                        "gridInstanceId": null,
                        "contentType": "application/json",
                        "contentEncoding": "utf8",
                        "sender": {
                            "componentId": "3961d498-83f8-4714-85ba-0539d3055892",
                            "type": "appmixer.utils.controls.OnStart",
                            "outputPort": "out"
                        },
                        "destination": {
                            "componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
                            "inputPort": "in"
                        },
                        "correlationInPort": null,
                        "componentHeaders": {},
                        "flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
                        "messageId": "12374d7e-5c66-40d1-8772-37c424bd4182",
                        "flowRunId": 1603726768950
                    },
                    "content": {
                        "key": "hello"
                    },
                    "scope": {
                        "3961d498-83f8-4714-85ba-0539d3055892": {
                            "out": {
                                "started": "2020-10-26T15:39:29.003Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-10-26T15:39:29.003Z"
                    }
                }
            ]
        },
        "created": "2020-10-26T15:39:29.097Z",
        "err": //Stringyfied error object...
    }

DELETE https://api.appmixer.com/unprocessed-messages/:messageId

Delete a message.

Path Parameters

Name
Type
Description

messageId

string

{}

Retry a message

POST https://api.appmixer.com/unprocessed-messages/:messageId

Put the message back into Appmixer engine.

Path Parameters

Name
Type
Description

messageId

string

{}

Flows

Get Flows

GET https://api.appmixer.com/flows

Return all flows of a user. curl "https://api.appmixer.com/flows" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

filter

string

Filter flows by their property values. Example: "userId:123abc" returns only flows who's owner is the user with ID "123abc" (i.e. shared flows are excluded). Note that you can also search on nested fields. This is especially useful with the customFields metadata object. For example: "filter=customFields.category:healthcare".

sharedWithPermissions

string

Filter flows by their sharing setting. Example: "read,start". All possible permission are currently "read", "start", "stop".

projection

string

Exclude flow object properties. Example: "-flow,-thumbnail".

sort

string

Sorting parameter. Can be any flow object property followed by semicolon and 1 (ascending), -1 (descending). Example: "mtime:-1".

pattern

string

A term to filter flows containing pattern in their names.

offset

number

The index of the first item returned. Default is 0. Useful for paging.

limit

number

Maximum items returned. Default is 100. Useful for paging.

[
  {
    "userId": "58593f07c3ee4f239dc69ff7",
    "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
    "stage": "stopped",
    "name": "Flow #4",
    "btime": "2018-03-29T19:24:08.950Z",
    "mtime": "2018-04-05T12:50:15.952Z",
    "sharedWith": [{
      "email": "david@client.io",
      "permissions": ["read", "start", "stop"]
    }],
    "flow": {
      "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
        "type": "appmixer.utils.http.Uptime",
        "label": "Uptime",
        "source": {},
        "x": 110,
        "y": 90,
        "config": {}
      },
      "43f1f63a-ecd2-42dc-a618-8c96b4acc767": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
          "in": {
            "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
              "up"
            ]
          }
        },
        "x": 320,
        "y": -10,
        "config": {
          "transform": {
            "in": {
              "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
                "up": {
                  "type": "json2new",
                  "lambda": {
                    "from_email": "info@appmixer.com",
                    "text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}} is back UP.\nDowntime: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.downTimeText}}}\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.statusCode}}}",
                    "subject": "Appmixer: Site UP ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}})"
                  }
                }
              }
            }
          }
        }
      },
      "416150af-b0d4-4d06-8ad1-75b17e578532": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
          "in": {
            "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
              "down"
            ]
          }
        },
        "x": 320,
        "y": 195,
        "config": {
          "transform": {
            "in": {
              "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
                "down": {
                  "type": "json2new",
                  "lambda": {
                    "from_email": "info@appmixer.com",
                    "subject": "Appmixer: Site DOWN ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}})",
                    "text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}} is DOWN.\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.statusCode}}}"
                  }
                }
              }
            }
          }
        }
      }
    },
    "mode": "module",
    "thumbnail": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D...",
    "started": "2018-04-05T12:33:15.357Z"
  },
  {
    "userId": "58593f07c3ee4f239dc69ff7",
    "flowId": "93198d48-e680-49bb-855c-58c2c11d1857",
    "stage": "stopped",
    "name": "Flow #5",
    "btime": "2018-04-03T15:48:52.730Z",
    "mtime": "2018-04-11T07:41:22.767Z",
    "flow": {
      "ce0742f4-4f72-4ea2-bea6-62cfaa2def86": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
          "in": {
            "3d71d67f-df0b-4723-bf85-20c97f6eaff6": [
              "weather"
            ]
          }
        },
        "x": 485,
        "y": 95,
        "config": {
          "transform": {
            "in": {
              "3d71d67f-df0b-4723-bf85-20c97f6eaff6": {
                "weather": {
                  "type": "json2new",
                  "lambda": {
                    "from_email": "info@appmixer.com",
                    "subject": "Appmixer: Current Weather",
                    "text": "Temperature: {{{$.3d71d67f-df0b-4723-bf85-20c97f6eaff6.weather.main.temp}}} dgC\nPressure: {{{$.3d71d67f-df0b-4723-bf85-20c97f6eaff6.weather.main.pressure}}} hPa\nHumidity: {{{$.3d71d67f-df0b-4723-bf85-20c97f6eaff6.weather.main.humidity}}}%\nCloudiness: {{{$.3d71d67f-df0b-4723-bf85-20c97f6eaff6.weather.clouds.all}}}%",
                    "to": ""
                  }
                }
              }
            }
          }
        }
      },
      "3d71d67f-df0b-4723-bf85-20c97f6eaff6": {
        "type": "appmixer.utils.weather.GetCurrentWeather",
        "label": "GetCurrentWeather",
        "source": {
          "location": {
            "b4d1ddbc-4bed-4de3-8fe1-9d9542d03cf0": [
              "out"
            ]
          }
        },
        "x": 290,
        "y": 95,
        "config": {
          "transform": {
            "location": {
              "b4d1ddbc-4bed-4de3-8fe1-9d9542d03cf0": {
                "out": {
                  "type": "json2new",
                  "lambda": {
                    "city": "Prague",
                    "units": "metric"
                  }
                }
              }
            }
          }
        }
      },
      "b4d1ddbc-4bed-4de3-8fe1-9d9542d03cf0": {
        "type": "appmixer.utils.controls.OnStart",
        "label": "OnStart",
        "source": {},
        "x": 105,
        "y": 95,
        "config": {}
      }
    },
    "mode": "module",
    "thumbnail": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D...",
    "started": "2018-04-06T12:59:29.631Z"
  }
]

Get Flow

GET https://api.appmixer.com/flows/:id

Return one flow. curl "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

id

string

{
  "userId": "58593f07c3ee4f239dc69ff7",
  "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
  "stage": "stopped",
  "name": "Flow #4",
  "btime": "2018-03-29T19:24:08.950Z",
  "mtime": "2018-04-05T12:50:15.952Z",
  "flow": {
    "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
      "type": "appmixer.utils.http.Uptime",
      "label": "Uptime",
      "source": {},
      "x": 110,
      "y": 90,
      "config": {}
    },
    "43f1f63a-ecd2-42dc-a618-8c96b4acc767": {
      "type": "appmixer.utils.email.SendEmail",
      "label": "SendEmail",
      "source": {
        "in": {
          "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
            "up"
          ]
        }
      },
      "x": 320,
      "y": -10,
      "config": {
        "transform": {
          "in": {
            "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
              "up": {
                "type": "json2new",
                "lambda": {
                  "from_email": "info@appmixer.com",
                  "text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}} is back UP.\nDowntime: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.downTimeText}}}\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.statusCode}}}",
                  "subject": "Appmixer: Site UP ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}})"
                }
              }
            }
          }
        }
      }
    },
    "416150af-b0d4-4d06-8ad1-75b17e578532": {
      "type": "appmixer.utils.email.SendEmail",
      "label": "SendEmail",
      "source": {
        "in": {
          "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
            "down"
          ]
        }
      },
      "x": 320,
      "y": 195,
      "config": {
        "transform": {
          "in": {
            "e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
              "down": {
                "type": "json2new",
                "lambda": {
                  "from_email": "info@appmixer.com",
                  "subject": "Appmixer: Site DOWN ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}})",
                  "text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}} is DOWN.\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.statusCode}}}"
                }
              }
            }
          }
        }
      }
    }
  },
  "mode": "module",
  "thumbnail": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D...",
  "started": "2018-04-05T12:33:15.357Z"
}

Get Flows Count

GET https://api.appmixer.com/flows/count

Return the number of all flows of a user. curl "https://api.appmixer.com/flows/count" -H "Authorization: Bearer [ACCESS_TOKEN]"

{
    "count": 29
}    

Create Flow

POST https://api.appmixer.com/flows

Create a new flow. curl -XPOST "https://api.appmixer.com/flows" -H "Content-Type: application/json" -d '{ "flow": FLOW_DESCRIPTOR, "name": "My Flow #1", "customFields": { "category": "healthcare" } }'

Request Body

Name
Type
Description

name

string

Name of the flow.

customFields

object

An object with any custom properties. This is useful for storing any custom metadata and later using the metadata values to filter returned flows.

thumbnail

string

Flow thumbnail image.

flow

object

Flow descriptor.

{
    "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}

Update Flow

PUT https://api.appmixer.com/flows/:id

Update an existing flow. curl -XPUT "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Content-Type: application/json" -d '{ "flow": FLOW_DESCRIPTOR, "name": "My Flow #2" }'

Path Parameters

Name
Type
Description

id

string

Flow ID.

Request Body

Name
Type
Description

object

An object with flow, name , customFields and thumbnail parameters. flow is the Flow descriptor.

{
    "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b",
    "result": "updated"
}

Delete Flow

DELETE https://api.appmixer.com/flows/:id

Delete an existing flow. curl -XDELETE "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

id

string

Flow ID.

{
    "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}

Start/Stop Flow

POST https://api.appmixer.com/flows/:id/coordinator

Start or Stop an existing flow. curl -XPOST "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Content-Type: application/json" -d '{ "command": "start" }'

Path Parameters

Name
Type
Description

id

string

Flow ID.

Request Body

Name
Type
Description

command

string

The command to send to the flow coordinator. It can be either "start" or "stop".

{
    "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}

Get Variables

GET https://api.appmixer.com/variables/:flowId

Get variables. Variables are placeholders that can be used in component config or inputs. These placeholders are replaced either at runtime by data coming from components connected back in the chain (dynamic variables) or by real values (static variables). Get component config variables: curl "https://api.appmixer.com/variables/93198d48-e680-49bb-855c-58c2c11d1857?componentId=e25dc901-f92a-46a2-8d29-2573d4ad65e5" -H "Authorization: Bearer [ACCESS_TOKEN]" Get component input variables: In this case, we identify the connection (one path in the flow graph) by source and target components, output port of the source component and input port of the target component. This address uniquely identifies one "link" in the flow graph. curl "https://api.appmixer.com/variables/93198d48-e680-49bb-855c-58c2c11d1857?srcComponentId=ba09820f-db59-4739-b22d-414826842495&srcComponentOut=trigger&tgtComponentId=e25dc901-f92a-46a2-8d29-2573d4ad65e5&tgtComponentIn=message" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

flowId

string

Flow ID.

Query Parameters

Name
Type
Description

srcComponentOut

string

Name of the output port of the source component.

tgtComponentIn

string

Name of the input port of the target component.

tgtComponentId

string

ID of the target component ID.

srcComponentId

string

ID of the source (connected) component ID.

componentId

string

ID of the component for which we're requesting config static variables.

{
    "dynamic":[],
    "static":{
        "channelId":[
            { "label": "my channel", "value":"CA0M22WU8" },
            { "label": "appmixer","value":"C6CMEGA9J" }
        ]
    }
}

If no parameters besides flowId are passed, variables for the entire flow are returned in the following format:

{
    'component1': {
        'properties': {
            // properties variables
        },
        'inputs': {
            'in1': {
                'srcComponentId1': {
                    'out1': {
                        'dynamic': [],
                        'static': {}
                    },
                    'out2': {
                        'dynamic': [],
                        'static': {}
                    },
                },
                'srcComponentId2': {
                    'out': {
                        'dynamic': [],
                        'static': {}
                    }
                }
            },
            'in2': {
                'srcComponentId1': {
                    'out1': {
                        'dynamic': [],
                        'static': {}
                    }
                }
            }
        }
    },
    'component2': {
        'properties': {
            'dynamic': [],
            'groups': null,
            'inputs': null,
            'schema': null,
            'static': {}
        }
        // component2 has no input ports so there's no 'inputs'
    }
}

Service Configuration

Appmixer allows you to save global configuration values for your service. If a component contains either an auth section or authConfig section, values for specified service will be injected.

Only users with admin scope can use these endpoints.

Get Services Configuration

GET /service-config

Get a list of stored configurations.

Query Parameters

Name
Type
Description

pattern

string

A term to filter configurations containing pattern on their service id

sort

string

Sorting parameter. Service id can be used to sort results alphabetically by their id. Example: serviceId:1

offset

number

Index of the first item returned. Default is 0. Useful for paging.

limit

number

Maximum items returned. Default is 100. Useful for paging.

[
	{
		"serviceId": "appmixer:google",
		"clientID": "my-global-client-id",
		"clientSecret": "my-global-client-secret"
	},
	{
		"serviceId": "appmixer:evernote",
		"sandbox": true,
	}
]

Get Service Configuration

GET /service-config/:serviceId

Get the configuration stored for the given service.

Path Parameters

Name
Type
Description

string

The service id. Example: appmixer:google

{
	"serviceId": "appmixer:google",
	"clientID": "my-global-client-id",
	"clientSecret": "my-global-client-secret"
}

Create Service Configuration

POST /service-config

Creates a new configuration for a service. The only required parameter on the payload is the serviceId. The rest of the payload can be any key/value pairs that will be the desired configuration for the service. For example: { "serviceId": "appmixer:google", "clientID": "my-global-client-id", "clientSecret": "my-global-client-secret" }

Request Body

Name
Type
Description

whatever

string

Any value for the whatever-key

serviceId

string

The serviceId. It should be in the form vendor:service. Example: appmixer:google

{
	"serviceId": "appmixer:google",
	"clientID": "my-global-client-id",
	"clientSecret": "my-global-client-secret"
}

Update Service Configuration

PUT /service-config/:serviceId

Updates the stored configuration for the given service. The payload should contain the whole configuration, as the payload content will overwrite the configuration stored under the service.

Path Parameters

Name
Type
Description

serviceId

string

The service id. Example

Request Body

Name
Type
Description

whatever-key

string

Any value you need

{
	"serviceId": "appmixer:google",
	"clientID": "my-global-client-id",
	"clientSecret": "my-global-client-secret"
}

Delete Service Configuration

DELETE /service-config/:serviceId

Removes the configuration from the given service.

Path Parameters

Name
Type
Description

serviceId

string

The service id. Example: appmixer:google

{}

API Reference

JavaScript Appmixer SDK API reference. Unless otherwise stated, all methods return a promise that resolves on success and rejects on error with error object containing further details.

Accounts

Authentication to apps.

Get Accounts

GET /auth/:componentType

Get the list of accounts the user has previously authenticated with for this component type. curl "https://api.acme.com/auth/appmixer.slack.list.SendChannelMessage?componentId=e15ef119-8fcb-459b-aaae-2a3f9ee41f15" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

componentType

string

Component Type.

Query Parameters

Name
Type
Description

componentId

string

Component ID.

  "componentType": "appmixer.slack.list.SendChannelMessage",
  "auth": {
    "accounts": {
      "5a6e21f3b266224186ac7d03": {
        "accessTokenValid": true,
        "accountId": "5a6e21f3b266224186ac7d03",
        "tokenId": "5a6e21f3b266224186ac7d04",
        "componentAssigned": true,
        "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5",
        "scopeValid": true,
        "authorizedScope": [
          "channels:read",
          "chat:write:user"
        ],
        "name": "U0UFJ0MFG - client IO",
        "displayName": "client IO"
      }
    }
  }
}

Get All Accounts

GET /accounts

Get the list of all accounts the user has authenticated with to any component. curl "https://api.appmixer.com/accounts" -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

filter

string

You can filter accounts.

[
  {
    "accountId": "5a6e21f3b266224186ac7d03",
    "name": "U0UFJ0MFG - client IO",
    "displayName": null,
    "service": "appmixer:slack",
    "userId": "58593f07c3ee4f239dc69ff7",
    "profileInfo": {
      "id": "U0UFJ0MFG - client IO"
    },
    "icon": "data:image/png;base64,...rkJggg==",
    "label": "Slack"
  },
  {
    "accountId": "5a7313abb3a60729efe76f1e",
    "name": "t.o.mas@client.io",
    "displayName": null,
    "service": "appmixer:pipedrive",
    "userId": "58593f07c3ee4f239dc69ff7",
    "profileInfo": {
      "name": "tomas",
      "email": "t.o.mas@client.io"
    },
    "icon": "data:image/png;base64,...rkJggg==",
    "label": "Pipedrive"
  }
]  

Example of filtering certain accounts:

// filtering acme accounts and aws accounts
curl --request GET 'http://api.acme.com/accounts?filter=service:!acme:[service]&filter=service:!appmixer:aws' \
--header 'Authorization: Bearer [ACCESS_TOKEN]'

Update Account Info

PUT /accounts/:accountId

Update account information. Currently, only the display name can be updated. The display name is visible in the Designer inspector when selecting available accounts for a component type and also on the Accounts page. curl -XPUT "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "displayName": "My Account Name" }'

Path Parameters

Name
Type
Description

accountId

string

The ID of the account to update.

Request Body

Name
Type
Description

string

Human-readable name of the account.

Create Account

POST /accounts

This can be used to create an account. Usually, an account is created when the user authenticates a component. There are scenarios where it is beneficial to create an account without user interaction (Integrations). There has to be an authentication module (auth.js) installed in Appmixer corresponding to the `service` ID. All the built-in authentication types are supported (Oauth1, Oauth2, API Key).

Query Parameters

Name
Type
Description

validateScope

string

If false, then the scope of the token from the body won't be validated against components installed in Appmixer.

requestProfileInfo

string

If false, then the auth module requestProfileInfo function won't be called.

Request Body

Name
Type
Description

displayName

string

Display name property of the account. This overrides the name of the account in the frontend.

name

string

Name of the account, the authentication will determine the name of the account using the accountNameFromProfileInfo property.

service

string

ID (vendor:service) of the service - `appmixer:google` for example.

token

object

The structure of this object depends on the authentication type (Oauth1, Oauth2, API Key).

profileInfo

object

Can be provided directly. If not, requestProfileInfo from the authentication module will be called.

{
    "accountId": "5f841f3a43f477a9fa8fa4e9",
    "name": "[Name of the account]",
    "displayName": null,
    "service": "[vendor:service]",
    "userId": "5f804b96ea48ec47a8c444a7",
    "profileInfo": {
        
    },
    "pre": {},
    "revoked": false
}

Below is an example of a request to create s Slack (Oauth2) account.

curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:slack",
    "token": {
        "accessToken": "[slack access token]",
        "scope": [
            "channels:write", 
            "groups:write", 
            "channels:read", 
            "channels:history", 
            "groups:read", 
            "groups:history", 
            "users:read", 
            "chat:write:user"
        ]
    },
    "profileInfo": {
        "id" : "[Name of the account that will be used in the frontend]"
    }
}'

Below is another example, this time for Google (Oauth2) account with access token expiration:

curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:google",
    "token": {
        "token": "[google access token]",
        "expDate": "2021-02-04 15:34:48.833Z",
        "refreshToken": "[google refresh token]",
        "scope": [
            "https://www.googleapis.com/auth/analytics", 
            "https://www.googleapis.com/auth/analytics.readonly", 
            "https://www.googleapis.com/auth/calendar", 
            "https://www.googleapis.com/auth/calendar.readonly", 
            "https://www.googleapis.com/auth/drive", 
            "https://www.googleapis.com/auth/drive.appdata", 
            "https://www.googleapis.com/auth/drive.file", 
            "https://mail.google.com/", 
            "https://www.googleapis.com/auth/gmail.compose", 
            "https://www.googleapis.com/auth/gmail.send", 
            "https://www.googleapis.com/auth/gmail.readonly", 
            "https://spreadsheets.google.com/feeds", 
            "profile", 
            "email"
        ]
    }
}'

One more example, this time an API Key account:

curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:aws",
    "token": {
        "accessKeyId" : "[AWS access key ID]",
        "secretKey" : "[AWS secret key]"
    }
}'

Test Account

POST /accounts/:accountId/test

Test account. Check if all the credentials (tokens) are still valid for the account. curl -XPOST "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03/test" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

accountId

string

Account ID.

{ "5a6e21f3b266224186ac7d04": "valid" }

Remove Account

DELETE /accounts/:accountId

Remove the account and stop all the flows that this account is used in. curl -XDELETE "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

accountId

string

Account ID.

{ "accountId": "5abcd0ddc4c335326198c1b2" }

List All Flows Using Account

GET /accounts/:accountId/flows

List all the flows where the account is used. curl "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03/flows" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

accountId

string

Account ID.

[
  {
    "flowId": "9251b4b6-4cdb-42ad-9431-1843e05307be",
    "name": "Flow #1"
  },
  {
    "flowId": "777d3024-43f6-4034-ac98-1cb5f320cb3a",
    "name": "Flow #2"
  },
  {
    "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
    "name": "Flow #3"
  }
]

Generate Authentication Session Ticket

POST /auth/ticket

Generate an authentication session ticket. This is the first call to be made before the user can authentication to a service. The flow is as follows: 1. Generate an authentication session ticket. 2. Get an authentication URL. 3. Start an authentication session. 4. Open the authentication URL in a browser to start the authentication flow. 5. Once the user completes the authentication flow, the browser redirects the user to a special Appmixer page which posts a message of the form "appmixer.auth.[success/failure].[ticket]" via the window.postMessage() call: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage. Note that this is a low-level mechanism that you don't have to normally deal with. The Appmixer JS SDK handles all this for you. curl "https://api.appmixer.com/auth/ticket" -H "Authorization: Bearer [ACCESS_TOKEN]"

{ "ticket": "58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" }

Get Authentication URL

GET /auth/:componentType/auth-url/:ticket

Get an authentication URL. curl "https://api.appmixer.com/auth/appmixer.slack.list.SendChannelMessage/auth-url/58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

ticket

string

Authentication ticket.

componentType

string

Component type.

Query Parameters

Name
Type
Description

string

Component ID.

{
    "authUrl": "https://slack.com/oauth/authorize?response_type=code&client_id=25316748213.218351034294&redirect_uri=http%3A%2F%2Flocalhost%3A2200%2Fauth%2Fslack%2Fcallback&state=38133t07c3ee4f369dc69ff7%3A1d2a90df-b192-4a47-aaff-5a80bab66de5&scope=channels%3Aread%2Cchat%3Awrite%3Auser"
}

Start Authentication Session

PUT /auth/:componentType/ticket/:ticket

Start the authentication session. curl -XPUT "https://api.appmixer.com/auth/appmixer.slack.list.SendChannelMessage/ticket/58593f07c3ee4f239dc69ff7:68982d38-d00c-4345-9a4a-82360d7e1649" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

ticket

string

Authentication session ticket.

componentType

string

Component type.

{
    "service": "appmixer:slack"
}

Clear Authentication From Component

DELETE /auth/component/:componentId

Clear authentication associated with the component. Note that this call does not remove the account, it only removes the association of an account with a component. curl -XDELETE "https://api.appmixer.com/auth/component/e25dc901-f92a-46a2-8d29-2573d4ad65e5" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

componentId

string

Component ID.

{ "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5" }

Assign Account To Component

PUT /auth/component/:componentId/:accountId

Assign an account to a component. curl -XPUT "https://api.appmixer.com/auth/component/e25dc901-f92a-46a2-8d29-2573d4ad65e5/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]"

Path Parameters

Name
Type
Description

accountId

string

Account ID.

componentId

string

Component ID.

{
    "accountId":"5a6e21f3b266224186ac7d03",
    "componentId":"e25dc901-f92a-46a2-8d29-2573d4ad65e5"
}

Designer

appmixer.ui.Designer

The appmixer.ui.Designer is a UI widget that displays the drag&drop Flow Designer.

Designer Events

Example

Constructor

Appmixer

Getting Started

Appmixer can be embedded into your own web products. By including Appmixer into your own product, you can give your users a whole new set of workflow automation features with a very little effort.

Embed Appmixer UI in your own web pages with the Appmixer SDK

You can use the provided Appmixer SDK to include the Appmixer UI widgets (including the Drag&Drop Designer, Flows manager, Insights, ...) any web page, right into your own web products, completely white-labeled. First, include the appmixer.js in your page:

Note we're also creating two <div> elements that will serve as containers for our designer and flow manager.

Once you included the Appmixer SDK, you need to create an instance of the global Appmixer class and pass the base URL of the engine:

Now you can create your first user:

Notice how we set the user's access token. The token is used by the Appmixer backend to identify the user. You should store the token together with the username and password in your DB with your own product user's record. This way, you can always associate users of your product with Appmixer (virtual) users.

Now we can create the first flow and open the Appmixer Designer to let your user design their first flow.

At this point, you should see the Appmixer Designer rendered inside your <div id="my-am-designer"> element.

Once you create a user in Appmixer (appmixer.api.signupUser()) and have stored their username and password in your system, you can always request a new token by calling appmixer.api.authenticateUser():

Full Example

FlowManager

appmixer.ui.FlowManager

The appmixer.ui.FlowManager is a UI widget that displays a list of flows the user created.

Flow Manager Events

Example

API

appmixer.api

Custom Component Strings

Appmixer lets you manage the components' inspector fields through the manifest or the strings object.

There are two ways how to customize component's strings:

  • Through a localization object inside the component's manifest

  • Adding custom strings to components namespace inside strings object

Component's manifest localization object

You can include custom strings inside the component's manifest using a localization object. The following is an example how to do it:

You can customize the component's label and description. You can customize component's input/output port labels, the inspector input field labels and output variables as well.

There's a slightly different specification when localizing output variables. As you can see in the example, after outPorts[0].options the next path fragment is the option's value, instead of the index. This is because the component could have a dynamic output instead and different output variables can share the same index, so we use the value to specify them instead.

To switch the language in UI, you call the Appmixer instance set method:

Note that if you want to customize the whole UI, you must use this in conjunction with the strings object. Here's an example:

Strings object's component namespace

The alternative way to customize the component's strings is using the Strings Object. There is a root namespace components which contains all the custom strings definitions for components:

service.json and module.json localization

Example using localization object in service.json

Example using Strings object

It follows the same pattern as in components, but we use the service/module path as a key for the definition:

Strings resolving

When rendering the component's inspector, the strings are resolved with the following priority:

  1. Localization object in manifest (component.json).

  2. Strings object components namespace.

  3. Property in manifest (component.json).

For example when resolving an input's label, it will first look if there is a localization object in the manifest with a path to that input's label. If not, it will search the Strings Object. If none of that is defined, it will use values from the manifest.

Appmixer High-Level Architecture

The profileInfo object is optional. If you provide it it will be used. If you do not provide it then the from the authentication module will be used to get the profile info. Slack access tokens do not expire, therefore there is neither an expiration date nor a refresh token in the request.

As you can see, there's a localization object at the end whose keys are language codes. This allows you to support multiple languages. Each value is an object whose keys are paths to the elements that will be customized (label, tooltip, placeholder, etc). The paths follow syntax.

Each key in components object is the path to the component and the value is an object whose keys are the paths to elements (label, tooltip, placeholder, etc). This path follows the syntax. For more information about the Strings Object refer to the section.

Not only you can localize component's strings, but also services and modules. This allows you to change the label and description of the applications in the designer's left-side panel (the one you drag the applications from). To do it we can use either localization object in the service.json or module.json manifest or use the .

requestProfileInfo

Event

Callback

Triggered when...

flow:insights

function(flowId)

the user clicks the insights menu item.

flow:clone

function(flowId)

the user clicks the clone menu item.

var designer = appmixer.ui.Designer({
    el: '#your-designer-div',
    options: {
        menu: [
            { label: 'Rename', event: 'flow:rename' },
            { label: 'Insights Logs', event: 'flow:insights-logs' },
            { label: 'Clone', event: 'flow:clone' },
            { label: 'Share', event: 'flow:share' },
            { label: 'Export to SVG', event: 'flow:export-svg' },
            { label: 'Export to PNG', event: 'flow:export-png' },
            { label: 'Print', event: 'flow:print' }
        ]
    }
});
appmixer.api.createFlow('New flow').then(function(flowId) {
    designer.set('flowId', flowId);
    designer.open();
}).catch(function(error) {
    console.log('Something went wrong creating a new flow.', error);
});
here
<!DOCTYPE html>
<html>    
    <body>

        <div id="my-am-designer" class="am-designer"></div>
        <div id="my-am-flow-manager" class="am-flow-manager-container"></div>
        <script src="./appmixer.js"></script>
        ...
    </body>
</html>
<script>
    var appmixer = new Appmixer({ baseUrl: 'https://api.appmixer.com' });
</script>
<script>
    appmixer.api.signupUser('first-username', 'first-password-123').then(function(auth) {
        appmixer.set('accessToken', auth.token);
    }).catch(function(err) {
        alert('Something went wrong.');
    });
</script>
<script>
    var designer = appmixer.ui.Designer({ el: '#my-am-designer' });
    appmixer.api.createFlow('My First Flow').then(function(flowId) {
        designer.set('flowId', flowId);
        designer.open();
    }).catch(function(err) {
        alert('Something went wrong.');
    });
</script>
<script>
    appmixer.api.authenticateUser('first-username', 'first-password-123').then(function(auth) {
        appmixer.set('accessToken', auth.token);
    }).catch(function(err) {
        alert('Something went wrong.');
    });
</script>
<!DOCTYPE html>
<html>
    <body>
        <div id="my-am-designer" class="am-designer"></div>
        <div id="my-am-flow-manager" class="am-flow-manager-container"></div>
        <script src="./appmixer.js"></script>
        <script>
           var appmixer = new Appmixer({ baseUrl: 'https://api.qa.appmixer.com' });
           var yourUserUsername = '123ABC45678@example.com'; // User ID of the user in YOUR system (must be in an email format!).
           var appmixerUserPassword = '[POPULATE_FROM_YOUR_DB]'; // A password of the virtual user in Appmixer.
           appmixer.api.authenticateUser(yourUserUsername, appmixerUserPassword).then(function(auth) {
               appmixer.set('accessToken', auth.token);
               onAppmixerReady();
           }).catch(function(err) {
               if (err.response && err.response.status === 403) {
                   // Virtual user not yet created in Appmixer. Create one with a random password and save the password in YOUR system
                   // so that you can authenticate the user later.
                   appmixerUserPassword = Math.random().toString(36).slice(-8);
                   appmixer.api.signupUser(yourUserUsername, appmixerUserPassword).then(function(auth) {
                       appmixer.set('accessToken', auth.token);
                       // ... Store auth.token and appmixerUserPassword in your DB.
                       onAppmixerReady();
                   }).catch(function(err) {
                       alert('Something went wrong.');
                   });
               } else {
                   alert('Something went wrong.');
               }
           });
           
           function onAppmixerReady() {
               
               var designer = appmixer.ui.Designer({ el: '#my-am-designer' });
               var flowManager = appmixer.ui.FlowManager({ el: '#my-am-flow-manager' });
               
               flowManager.open();
               flowManager.on('flow:open', function(flowId) {
                   designer.set('flowId', flowId);
                   flowManager.close();
                   designer.open();
               });
               flowManager.on('flow:create', function() {
                   flowManager.state('loader', true);
                   appmixer.api.createFlow('New Flow').then(function(flowId) {
                       designer.set('flowId', flowId);
                       flowManager.close();
                       designer.open();
                   }).catch(function(err) {
                       flowManager.state('error', 'Error creating a new flow.');
                   });
               });
           }
        </script>
    </body>
</html>

Method

Description

appmixer.ui.FlowManager(config)

A function that accepts a config object and returns an instance of flow manager. The config object must contain at least the el property that points to a container DOM element where the flow manager will be rendered. el can either be a CSS selector or a reference to a DOM element. config.options.menu can optionally define the flow menu. For example: { options: { menu: [ { label: 'Rename', event: 'flow:rename' }, { label: 'Insights', event: 'flow:insights' }, { label: 'Clone', event: 'flow:clone' }, { label: 'Export to SVG', event: 'flow:export-svg' }, { label: 'Export to PNG', event: 'flow:export-png' }, { label: 'Print', event: 'flow:print' } ] } defines a menu with 6 items. Note that each item can also optionally contain icon property (which we omitted from the example above to save space. The icon property is a URL of an image. config.sharePermissions can optionally define the sharing options for the flows. For example: sharePermissions: [ { label: 'Read', value: 'read' }, { label: 'Start', value: 'start' }, { label: 'Stop', value: 'stop' } ] Define a share dialog box that gives the user permission to share their flows for all the read/start/stop permissions. Additionally, you can also define the scope of the share with config.shareTypes: shareTypes: [ { label: 'Email', value: 'email', placeholder: 'Enter an email' }, { label: 'Scope', value: 'scope', placeholder: 'Enter a scope' }, { label: 'Domain', value: 'domain', placeholder: 'Enter a domain' }, ] config.options.customFilter is an optional object that can contain properties that we'd like to filter on. This is especially useful in connection with customFields metadata object as it allows you to display multiple different Flow Managers each listing a different category of flows. For example: options: { customFilter: { 'customFields.template': true 'customFields.category': 'healthcare'

} }

config.options.layout.default is an optional string that determines the default layout of the flow manager. Currently list and thumbnail are supported. If not specified, the default layout is thumbnail.

config.options.layout.table is an optional object that allows customizing the list layout column headers and values displayed. For example: options: { layout: { table: { columns: [ { property: 'name', label: 'Flow Name' }, { property: 'status', label: 'Current Status' }, { property: 'btime', label: 'Start Time' }, { property: 'mtime', label: 'Last Updated' } ] } } } The label property makes reference to the label displayed on the column header, while property makes reference to the flow property value that will be displayed on that column.

open()

Render the flow manager inside its container.

close()

Close the flow manager.

on(event, handler)

React on events of the flow manager. See below for the list of events the flow manager supports.

off([event, handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

state(name, value, [details])

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the flow manager UI, just call state("loader", true). If you want to display an error in the flow manager UI, call state("error", "Something went wrong."). The optional details parameter allows you to add details to the error message, for exmaple: state('error', 'Something went wrong', 'Loading failed because ....').

reload()

Re-render the flow manager. Call this when you changed the state of a flow (e.g. renamed a flow, started a flow, ...) to make sure the flow manager reflects the new changes.

Event

Callback

Triggered when...

flow:open

function(flowId)

the user clicks on a flow to open it.

flow:create

function()

the user clicks the "Create Flow" button.

flow:start

function(flowId)

the user clicks the button to start the flow.

flow:stop

function(flowId)

the user clicks the button to stop the flow.

flow:insights

function(flowId)

the user clicks the button to open Insights for the flow.

flow:clone

function(flowId)

the user clicks the button to clone the flow.

flow:remove

function(flowId)

the user clicks the button to delete the flow.

var flowManager = appmixer.ui.FlowManager({
    el: '#your-flow-manager-div',
    options: {
        menu: [
            { label: 'Edit', event: 'flow:open' },
            { label: 'Clone', event: 'flow:clone' },
            { label: 'Share', event: 'flow:share' },
            { label: 'Insights', event: 'flow:insights' },
            { label: 'Delete', event: 'flow:remove' }
        ]
    }
});
flowManager.on('flow:start', function(flowId) {
    flowManager.state('loader', true);
    appmixer.api.startFlow(flowId).then(function() {
        flowManager.state('loader', false);
        flowManager.reload();
    }).catch(function(error) {
        flowManager.state('error', 'Starting flow failed.');
    });
});
flowManager.on('flow:create', function() {
    flowManager.state('loader', true);
    appmixer.api.createFlow('New flow').then(function(flowId) {
        flowManager.state('loader', false);
        designer.set('flowId', flowId);
        designer.open();
    }).catch(function(error) {
        flowManager.state('error', 'Creating flow failed.');
    });
});
flowManager.open();
{
    "name": "appmixer.twilio.sms.SendSMS",
    "author": "David Durman <david@client.io>",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVp...",
    "description": "Send SMS text message through Twilio.",
    "private": false,
    "auth": {
        "service": "appmixer:twilio"
    },
    "outPorts": [
        {
            "name": "sent",
            "options": [
                { "label": "Message Sid", "value": "sid" }
            ]
        }
    ],
    "inPorts": [
        {
            "name": "message",
            "schema": {
                "type": "object",
                "properties": {
                    "body": { "type": "string" },
                    "to": { "type": "string" },
                    "from": { "type": "string" }
                },
                "required": [
                    "from", "to"
                ]
            },
            "inspector": {
                "inputs": {
                    "body": {
                        "type": "text",
                        "label": "Text message",
                        "tooltip": "Text message that should be sent.",
                        "index": 1
                    },
                    "from": {
                        "type": "select",
                        "label": "From number",
                        "placeholder": "Type number",
                        "tooltip": "Select Twilio phone number.",
                        "index": 2,
                        "source": {
                            "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                            "data": {
                                "transform": "./transformers#fromNumbersToSelectArray"
                            }
                        }
                    },
                    "to": {
                        "type": "text",
                        "label": "To number",
                        "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                        "index": 3
                    }
                }
            }
        }
   ],
   "localization": {
       "cs": {
           "label": "Pošli SMS",
           "description": "Pošli SMS pomocí Twilia",
           "inPorts[0].name": "Zpráva",
           "inPorts[0].inspector.inputs.body.label": "Textová zpráva",
           "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
           "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo",
           "outPorts[0].name": "Odesláno",
           "outPorts[0].options[sid].label": "Sid zprávy"
       },
       "sk": {
           "label": "Pošli SMS",
           "description": "Pošli SMS pomocou Twilia",
           "inPorts[0].name": "Správa",
           "inPorts[0].inspector.inputs.body.label": "Textová správa",
           "inPorts[0].inspector.inputs.from.label": "číslo volajúceho",
           "outPorts[0].name": "Odoslané",
           "outPorts[0].options[sid].label": "Sid správy"
       }
   }
}
// Create an SDK instance
var appmixer = new Appmixer()

// Will use the strings under 'cs' key
appmixer.set('lang', 'cs')

// Will switch the strings to the ones under 'sk' key
appmixer.set('lang', 'sk')
var appmixer = new Appmixer();

var mySkStrings = { /* Strings definition for sk language */ };
var myCsStrings = { /* Strings definition for cs language */ };

// This function will be called when the user clicks on some
// "Switch to sk" button
function setLangToSk() {
    appmixer.set('lang', 'sk');
    appmixer.set('strings', mySkStrings);
}

// This function will be called when the user clicks on some
// "Switch to cs" button
function setLangToCs() {
    appmixer.set('lang', 'cs');
    appmixer.set('strings', myCsStrings);
}
{
	components: {
		"appmixer.twilio.sms.SendSMS": {
			"inPorts[0].inspector.inputs.body.label": "Textová zpráva",
      "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
      "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo"
		}
	}

	// Other namespaces (designer, storage, accounts...)
}
{
    "name": "appmixer.twilio",
    "label": "Twilio",
    "category": "applications",
    "description": "Twilio is an easy tool for developers to send and receive SMS and voice calls.",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMj...",
    "localization": {
        "cz": {
            "label": "Modul Twilio",
            "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
        },
        "sk": {
            "label": "Modul Twilio",
            "description": "Twilio je ľahký nástroj pre vývojárov na odosielanie a prijímanie SMS a hlasových hovorov."
        }
    }
}
"appmixer.twilio": {
    "label": "Modul Twilio",
    "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
}
Custom Component Strings
jsonpath
jsonpath
Custom Strings
Strings Object

Method

Description

appmixer.ui.Designer(config)

Constructor function that acceptsconfigobject. Theconfig object must contain at least the el property that points to a container DOM element where the Designer will be rendered. el can either be a CSS selector or a reference to a DOM element. Note that the recommended width of your container is at least 1200px for best Designer UI/UX. config.options.menu can optionally define the flow menu inside Designer. For example: { options: { menu: [ { label: 'Rename', event: 'flow:rename' }, { label: 'Insights', event: 'flow:insights' }, { label: 'Clone', event: 'flow:clone' }, { label: 'Export to SVG', event: 'flow:export-svg' }, { label: 'Export to PNG', event: 'flow:export-png' }, { label: 'Print', event: 'flow:print' } ] } defines a menu with 6 items. Note that each item can also optionally contain icon property (which we omitted from the example above to save space. The icon property is a URL of an image. It's important to note that the designer supports built-in events: "flow:rename", "flow:export-svg", "flow:export-png" and "flow:print". These events cannot be re-defined. These events trigger built-in UX for renaming, exporting to SVG and PNG, and printing flows. If you don't want to use this built-in behaviour, just redefine your menu with your own custom events. config.sharePermissions can optionally define the sharing options for the flow. For example:

sharePermissions: [

{ label: 'Read', value: 'read' },

{ label: 'Start', value: 'start' },

{ label: 'Stop', value: 'stop' }

]

Define a share dialog box that gives the user permission to share the flow for all the read/start/stop permissions. Additionally, you can also define the scope of the share with config.shareTypes:

shareTypes: [

{ label: 'Email', value: 'email', placeholder: 'Enter an email' },

{ label: 'Scope', value: 'scope', placeholder: 'Enter a scope' },

{ label: 'Domain', value: 'domain', placeholder: 'Enter a domain' },

]

toolbar: [ ['undo', 'redo'], ['zoom-to-fit', 'zoom-in', 'zoom-out'], ['logs'], [{ tooltip: 'Lorem ipsum', widget: { template: 'My button' }, }], ]

set(property, value)

Set a property of the designer. Currently only "flowId" is supported. Use it to set the flow the designer should open for (set("flowId", "123abc456")). You can also call this to change the currently opened flow in the designer dynamically.

get(property)

Return a property value.

open()

Render the Designer inside its container.

close()

Close the designer.

on(event, handler)

React on events of the designer. See below for the list of events the designer supports.

off([event, handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

state(name, value)

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the flow manager UI, just call state("loader", true). If you want to display an error in the flow manager UI, call state("error", "Something went wrong.").

reload()

Re-render the designer. Call this when you changed flow's state (started/stopped) to make sure the state change is reflected.

Method

Description

appmixer.api.authenticateUser(username, password)

Authenticate a user to Appmixer. Note that this can be a "virtual" user that exists for the sole purpose of associating a real user of your own product to a user in Appmixer. Each user in Appmixer can have a set of flows, can run and stop flows and can see data going through their flows. The returned promise is either resolved with an object that contains token (which you need to set with appmixer.set('accessToken', token) to be able to make calls to the API backend. Or the promise is rejected with an error object. If the error object returns 403 status code (i.e. err.response.status === 403), the user does not exist in Appmixer.

appmixer.api.signupUser(username, password)

Create a new user in Appmixer. The returned promise is either resolved with an authentication object (containing the token property) or rejected if the sign-up fails.

appmixer.api.createFlow(name, [descriptor], [properties])

Create a new flow in Appmixer. The returned promise resolves to the ID of the newly created flow. The properties object can contain your own custom metadata inside the customFields property. This is especially useful for filtering flows based on your own custom metadata.

appmixer.api.deleteFlow(flowId)

Delete an existing flow identified by flowId.

appmixer.api.getFlow(flowId)

appmixer.api.getFlows(query)

Get all flows of the user or filter them by query. query is an object with the following properties: limit, offset, pattern (a string to filter flows containing pattern in their names), sort, projection (allows you to exclude properties from the returned flow objects), sharedWithPermissions and filter.Example: { limit: 20, offset: 0, pattern: "slack", projection: "-flow,-thumbnail", sort: "mtime:-1", sharedWithPermission: "read", filter: "userId:423jfdsalfjl4234fdsa" }

appmixer.api.getFlowsCount(query)

Get the number of all flows of the user or filter them by query. query is an object with pattern property that can include a string to filter flows containing a pattern in their names. Example: { "pattern": "dropbox" }.

appmixer.api.updateFlow(flowId, update)

Update an existing flow. update can contain the following information: { flow, name, customFields }, where flow is the Flow Descriptor of the flow and customFields is an object with your own custom metadata for this flow.

appmixer.api.startFlow(flowId)

Start a flow.

appmixer.api.stopFlow(flowId)

Stop a flow.

appmixer.api.cloneFlow(flowId)

Create a copy of an existing flow. The returned promise resolves to the ID of the newly created flow.

appmixer.api.getUser()

Get current user. The returned promise resolves to an object with username.

appmixer.api.getStores()

Get all the data stores. The returned promise resolves to an array of stores each an object with name and storeId properties.

appmixer.api.getStore(storeId)

Get one store. The returned promise resolves to an object with name and storeId properties.

appmixer.api.getStoreRecordsCount(query)

Get the number of records in a store. query is an object with storeId and pattern properties where pattern is a string to filter records that contain the string in their keys or values.

appmixer.api.getStoreRecords(query)

Get store records. query is an object with storeId, pattern (string to search for in keys/values), limit , offset and sort properties. Example: { limit: 30, offset: 0, pattern: “foo”, sort: “updatedAt:-1", storeId: “5c6d643f4849f447eba55c1d” }

appmixer.api.createStore(name)

Create a new store. The returned promise resolves to the ID of the newly created store.

appmixer.api.deleteStore(storeId)

Delete a store.

appmixer.api.renameStore(storeId, newName)

Rename an existing store.

appmixer.api.createStoreItem(storeId, key, value)

Create a new record in a store.

appmixer.api.deleteStoreItems(items)

Delete store items. items is an array of objects each having a key and storeId properties identifying the item and store from which the item should be removed.

appmixer.api.createAccount(params, data)

Create a custom account.

appmixer.api.getAccounts(filter)

appmixer.api.getComponentAccounts(componentType, componentId)

Get a list of accounts connected to a specific component.

appmixer.api.getAccountFlows(accountId)

Get a list of flows this account is used in. The returned promise resolves to an array of objects with flowId and name properties.

appmixer.api.setAccountName(accountId, newName)

Rename a connected account. Note that this name is displayed in the Accounts widget and also in the Inspector UI of the Designer.

appmixer.api.getLogs(query)

Get logs. query is an object of the form { from, size, sort, query }. Example: { from: 0, size: 30, sort: "@timestamp:desc", query: "@timestamp:[2018-01-01 TO 2018-01-01]" }. To get logs of a specific flow, use e.g. { from: 0, size: 30, sort: "@timestamp:desc", query: "@timestamp:[2018-01-01 TO 2018-01-01] AND +flowId:FLOW_ID" }

appmixer.api.getLog(logId, index)

Get one log. logId and index are values returned from getLogs().

appmixer.api.getPeopleTasks(query)

Get all tasks of the user. query.role can be either "approver" or "requester" and allows you to filter tasks based on the role. query.pattern filters returned tasks by a term that must be contained in the task title. Settingquery.secret to either the approverSecret or requesterSecret allows you to get a list of tasks of a different user for which you have the secret (other than the one identified by the access token, i.e. the currently signed-in user).

appmixer.api.getPeopleTasksCount(query)

Returns the number of tasks based on the query. See getPeopleTasks(query) for more info.

appmixer.api.getPeopleTask(id)

Return one task identified by id.

appmixer.api.approveTask(id, [params])

Approve a task identified by id. params is an optional object that can contain the secret property (approver secret). Having the secret allows you to approve a task of any user for which you have the secret, not just the currently signed-in user.

appmixer.api.rejectTask(id, [params])

Reject a task identified by id. params is an optional object that can contain the secret property (approver secret). Having the secret allows you to reject a task of any user for which you have the secret, not just the currently signed-in user.

appmixer.api.getCharts()

Returns all the Insights charts of the user.

appmixer.api.getChart(chartId)

Return one Insights chart identified by chartId.

appmixer.api.deleteChart(chartId)

Delete an Insights chart identified by chartId.

InsightsLogs

appmixer.ui.InsightsLogs

The appmixer.ui.InsightsLogs is a UI widget that displays logs and histogram of all messages that passed through the user's flows. It can display logs of one flow or combine logs of all the user's flows.

Method

Description

appmixer.ui.InsightsLogs(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the InsightsLogs will be rendered.

set(property, value)

Set a property value. Currently only "flowId" is supported. Use it to set the flow for which you want to display logs. If you don't set the "flowId", logs of all the flows of the user will be displayed.

get(property)

Get a property value.

open()

Render the InsightsLogs inside its container.

close()

Close the InsightsLogs.

state(name, value)

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the flow manager UI, just call state("loader", true). If you want to display an error in the flow manager UI, call state("error", "Something went wrong.").

reload()

Refreshes the InsightsLogs. Since the UI contains a way to reload flows, calling this can be useful only when any flows has changed, so the changes are reflected in the flows filter.

PeopleTasks

appmixer.ui.PeopleTasks

Method

Description

appmixer.ui.PeopleTasks(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the PeopleTasks widget will be rendered.

set(property, value)

Set a property of the people tasks. Currently only secret and role is supported. Setting the secret allows you to open the people task dashboard of any user for which you have the secret (not just the currently signed-in user. This secret comes from either the approverSecret or requesterSecret property of the object returned when you create a new task. Setting role to either "approver" or "requester" allows you to change the default view of the people task dashboard. In other words, if a user is both an approver and a requester, they have different lists of tasks for both roles.

reload()

Refresh the PeopleTasks. This can be called when tasks have been modified through API, to make sure changes are reflected in the UI.

Components

The appmixer.ui.Components is a UI widget that displays all available applications and components.

Method

Description

appmixer.ui.Components(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the widget will be rendered.

open()

Render the widget inside its container.

close()

Terminate widget and empty the container.

reload()

Reload the widget data.

Example

var components = appmixer.ui.Components({
    el: '#your-accounts-div'
});
components.open();

Storage

appmixer.ui.Storage

The appmixer.ui.Storage is a UI widget that displays the user's data stores.

Method

Description

appmixer.ui.Storage(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the Storage widget will be rendered.

open()

Render the Storage inside its container.

close()

Close the Storage widget.

state(name, value)

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the flow manager UI, just call state("loader", true). If you want to display an error in the flow manager UI, call state("error", "Something went wrong.").

reload()

Reload the Storage. Calling this can be useful when stores have been added or removed, so the changes are reflected in the UI.

InsightsChartEditor

appmixer.ui.InsightsChartEditor

The appmixer.ui.InsightsChartEditor is a UI widget that allows the user to configure a new chart visualization of any data of their flows. Different types of charts are supported including bar, line, area, scatter, pie and more. Moreover, the user can select from a wide range of aggregations to create an aggregated visualization of their data such as Date Histogram, Sum, Min, Max, Average, Unique Count, Range and Filter.

Method

Description

appmixer.ui.InsightsChartEditor(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the InsightsChartEditor will be rendered.

open()

Render the InsightsChartEditor inside its container.

close()

Close the InsightsChartEditor.

on(event, handler)

React on events of the InsightsChartEditor. See below for the list of events the InsightsChartEditor supports.

off([event], [handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

set(property, value)

Set a property. Currently only "chartId" is supported. Use it to set the chart the chart editor should open for (set("chartId", "123abc456")). You can also call this to change the currently opened chart dynamically.

get(property)

Get a property value.

state(name, value)

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the InsightsChartEditor UI, just call state("loader", true). If you want to display an error in the InsightsChartEditor UI, call state("error", "Something went wrong.").

reload()

Refresh the content of the chart editor.

InsightsChartEditor Events

Event

Callback

Triggered when...

close

function()

the user clicks on the close button.

Example

var insightsChartEditor = appmixer.ui.InsightsChartEditor({
    el: '#your-insights-chart-editor-div'
});
appmixer.api.getCharts().then(function(charts) {
    var myChart = charts[0];   // Assuming we have at least one chart.
    insightsChartEditor.set('chartId', myChart.chartId);
    insightsChartEditor.open();
});

InsightsDashboard

appmixer.ui.InsightsDashboard

The appmixer.ui.InsightsDashboard is a UI widget that displays all the charts that the user has defined. The charts are interactive and can be re-arranged with drag&drop, refreshed to reflect the latest state, cloned and removed. Usually, you would listen to the chart:open event to open the appmixer.ui.InsightsChartEditor for the user to be able to re-configure their chart.

Method

Description

appmixer.ui.InsightsDashboard(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the InsightsDashboard will be rendered.

open()

Render the InsightsDashboard inside its container.

close()

Close the InsightsDashboard.

on(event, handler)

React on events of the InsightsDashboard. See below for the list of events the InsightsDashboard supports.

off([event], [handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

state(name, value)

Change the state of the UI. Currently, only "loader" and "error" states are supported. For example, in order to show a loader in the InsightsDashboard UI, just call state("loader", true). If you want to display an error in the InsightsDashboard UI, call state("error", "Something went wrong.").

reload()

Refresh the content of InsightsDashboard.

InsightsDashboard Events

Event

Callback

Triggered when...

chart:open

function(chartId)

the user clicks the "tripple-dot" tool button.

chart:remove

function(chartId)

the user clicks the trash icon tool button.

Example

var insightsDashboard = appmixer.ui.InsightsDashboard({
    el: '#your-insights-dashboard-div'
});
var insightsChartEditor = appmixer.ui.InsightsChartEditor({
    el: '#your-insights-chart-editor-div'
});
insightsDashboard.on('chart:open', function(chartId) {
    insightsChartEditor.open();
    insightsChartEditor.set('chartId', chartId);
});
insightsDashboard.on('chart:remove', function(chartId) {
    appmixer.api.deleteChart(chartId).then(function() {
        insightsDashboard.reload();
    });
});
insightsDashboard.open();

Accounts

appmixer.ui.Accounts

The appmixer.ui.Accounts is a UI widget that displays the user's connected accounts.

Method

Description

appmixer.ui.Accounts(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the Accounts will be rendered. Filter accounts by setting a custom query string to config.options.filter.

open()

Render the Accounts inside its container.

close()

Close the Accounts.

on(event, handler)

React on events of the accounts widget. See below for the list of events the accounts widget supports.

off([event, handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

reload()

Refresh the Accounts.

Accounts Events

Event

Callback

Triggered when...

flow:open

function(flowId)

the user clicks to open a flow the account is associated with.

Example

var accounts = appmixer.ui.Accounts({
    el: '#your-accounts-div'
});
accounts.on('flow:open', function(flowId) {
    designer.set('flowId', flowId);
    designer.open();
});
accounts.open();

Services

Open Appmixer Backoffice and click the Services item in the main menu.

There are more ways how to save those secrets in Appmixer. Using Appmixer Backoffice is the easiest one. Let's say you want to start using Slack components. First, you need to register your Slack application on the Slack website, then you will be provided with clientId and clientSecret. Once you have those, you can save them into Appmixer like this:

As a Service ID you use appmixer:slack. This is the same value that is used in Slack component.json files in the auth section.

This tells Appmixer that the NewChannelMessageRT component uses appmixer:slack authentication module. Appmixer will try to locate authentication file auth.js in appmixer/slack directory.

We can now add the clientId and clientSecret.

Then add clientId key with your client Id.

And then the clientSecret.

After that, you can use the Slack components in Appmixer.

You can add any key/value pair here. All the keys and their values will be available in your component's code at context.auth object and in the case of auth.js files directly in the context object.

Developer mode

Developer mode enables tools that are useful to create and debug your flows

Enabling developer mode

Developer mode is disabled by default on the SDK instance. There are two ways to enabling/disabling developer mode. Through the SDK constructor:

Or using the SDK instance set method:

Using the setter allows you to enable/disable developer mode dynamically.

Developer mode tools

Currently developer mode expose a show/hide logs button on the Designer UI:

This log panel allows you to inspect the messages of your running flow in real time, without needing to navigate away from the Designer UI. This is very useful when you are creating your flow and you want to make sure everything is working as expected. Moreover, you can get details from each message by clicking on it:

Wizard

appmixer.ui.Wizard

The appmixer.ui.Wizard is a UI widget that creates a custom integration instance.

Wizard Events

Example

Managing Authentication

This little tutorial will show you how to manage Appmixer SDK authentication.

Introduction

Appmixer SDK provides a simple way to set an access token on the instance, so it can make authenticated requests to the API. Nevertheless, it is up to the implementing application to manage token persistence. The reason for this is to allow the implementing application to store the token wherever it wants - e.g. localStorage, sessionStorage, database, Amazon S3, etc.

Storing the token somewhere allows to reuse the token, instead of requesting a new one each time the Appmixer SDK instance is re-created, for example when the user reloads the page.

Token management example

For this example, we are going to use browser's sessionStorage to retrieve and store the access token. So let's imagine we have a simple login form with username and password fields and a submit button. We write a login function that is called when the login form is submitted:

As you can see, we use Appmixer API authenticate method to retrieve a token from the engine. After we get it, we do two things:

  • Set it on our SDK instance

  • Store it on browser's sessionStorage under the key myAccessToken. This will allow us to persist the token even after the SDK instance is destroyed.

Now in order to use our stored token, every time our application loads, we need to check if there is a stored token. Depending on if there is a token or not, our application would behave differently:

  • If there isn't a stored token, we redirect to our login form

  • If there's a stored token, we set it on our SDK instance and redirect to our home page

So to achieve this, your application would need to call a function like this every time it loads:

This way, the cycle is closed and you are able to persist and reuse your access tokens after your SDK instances are lost.

Check token expiration

Appmixer uses expirable JWT tokens to authenticate their users. For this reason it is desirable to not only check if we have a stored token, but also if this token is still valid.

Appmixer SDK also provides a convenient way to handle invalid tokens, specifically allowing to execute custom code whenever an invalid token is used in a request made by the SDK. The SDK instance exposes onInvalidAccessToken(callbackFn) method, which receives a single function argument which is going to be called whenever the SDK is being used with an invalid token. Usually inside this callback you normally would provide the users a way to re-authenticate themselves, like redirecting them to a login form. Now we show how to use this method to achieve the same result as the previous example:

Multi-user management

In order to work with multiple SDK users at the same time, you can simply use more than one SDK instance at the time:

Integrations

appmixer.ui.Integrations

The appmixer.ui.Integrations is a UI widget that displays available and active integrations.

Integrations events

Example

Getting Started

And sign in with your Appmixer user account.

Method

Description

Appmixer({ baseUrl })

A constructor function to create an instance of the Appmixer SDK. Pass the base URL of the engine (Appmixer API).

appmixer.set(key, value)

Set a property on the appmixer SDK object. Currently only accessToken should be set to be able to make calls to the Appmixer API.

appmixer.onInvalidAccessToken(callbackFn)

Allows setting a callback function that will be called whenever the current access token on the SDK is not valid. More information can be found on the tutorial.

config.options.toolbarcan define buttons of the Designer toolbar which is empty by default. Use presets (all shown in the example below) or Vue Component options for custom buttons. The custom button definition is the same as for . Complete toolbar definition example:

Get flow. The returned promise resolves to an object with the following information: { id, flow, name, stage, btime, mtime, thumbnail }, where flow is the Flow Descriptor, stage is either 'running' or 'stopped', btime is the time the flow was created ("birth" time), mtime is the time the flow was modified and thumbnail contains a thumbnail image (self-contained, in the format).

Get a list of connected accounts of the user. filter is a custom query string (see the for an example). The returned promise resolves to an array of objects of the form { accountId, name, displayName, service, icon, profileInfo }.

The appmixer.ui.PeopleTasks is a UI widget that displays people task dashboard for the user. Please see the for more details.

Appmixer comes with a set of components. Some of those can be used directly, but some require user authentication into a 3rd party system (Slack for instance). This authentication is usually done through the OAuth protocol. That protocol requires pair of stores in Appmixer.

You can use this configuration even for components that do not require user authentication into 3rd party applications. component is a good example. In this case, you need an API key in order to use the Deep AI API. But you don't want your users to provide that API key. You want to use your own API key for all your users. More about that .

For this purpose, we need to decode the token in order to get the expiration date. You can use the library to decode JWT tokens and be able to access the information within. So now we expand the last example by also checking if the JWT token is valid as well:

Appmixer Backoffice is an administration UI for Appmixer. You need an admin user account in order to log into Backoffice. More about that in .

Appmixer Backoffice runs on port 8081 by default. After you the Appmixer package you can open http://localhost:8081.

Custom Inspector Fields
Data URI
People Tasks tutorial
GET /accounts
// Create new instance with developer mode enabled
var appmixer = new Appmixer({
    devMode: true
})
// Enable developer mode at runtime
appmixer.set('devMode', true)

Method

Description

appmixer.ui.Wizard(config)

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the Wizard will be rendered.

open()

Render the Wizard inside its container.

close()

Close the Wizard.

on(event, handler)

React on events of the Wizard widget. See below for the list of events the Wizard widget supports.

off([event, handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

reload()

Refresh the Wizard.

Event

Callback

Triggered when ...

flow:start

function(flowId)

The user clicked the submit button and the integration was started.

cancel

function()

The user clicked the cancel button.

var wizard = appmixer.ui.Wizard({
    el: '#your-wizard-div',
    flowId: 'your-integration-id',
});
wizard.on('flow:start', function(flowId) {
    wizard.close();
});
wizard.open();
// Method called when a login form is submitted
function login(username, password) {
  // Request a token from the engine
  appmixer.api.authenticate(username, password)
    .then((data) => {
        // We retrieve the token from the response data object
        const { token } = data;
      
        // We set the token onto our SDK instance  
        appmixer.set('accessToken', token);
        
        // Now this is where we persist our token using the
        // sessionStorage. Remember that you can store the token
        // pretty much on any storage system you can access from
        // your application
        sessionStorage.setItem('myAccessToken', token);
        
        // After this you can redirect to home page
    })
    .catch((err) => {
        console.log('Something wrong happened');
        console.log(err);
    });  
}
// Appmixer is initialized somewhere when your application starts
const appmixer = new Appmixer();

function initialize() {
    
    // We look into sessionStorage using the same key we use for
    // storing the token when we authenticate into our application
    const token = sessionStorage.getItem('myAccessToken');
    
    if (token){
        // If token exists, we set it on the Appmixer instance
        // and redirect to home page
        appmixer.set('accessToken', token);
        
        // Redirect to home page
    } else {
        // There is no token stored, redirect to login page
    }
}
// Appmixer is initialized somewhere when your application starts
const appmixer = new Appmixer();

// This is a helper function that returns if a token is expired or not
function tokenIsExpired(token){
    // We assume jsonwebtoken has been imported/exposed somewhere
    const decoded = jsonwebtoken.decode(token);
    
    if (decoded && decoded.exp) {
        const timeInSeconds = Math.ceil(new Date().getTime() / 1000);
        return decoded.exp < timeInSeconds;
    }
    return true;
}

function initialize() {
    
    // We look into sessionStorage using the same key we use for
    // storing the token when we authenticate into our application
    const token = sessionStorage.getItem('myAccessToken');
    
    // Now in addition to checking if there's a stored token
    // we also check if is still valid
    if (token && !tokenIsExpired(token)){
        // If token exists, we set it on the Appmixer instance
        // and redirect to home page
        appmixer.set('accessToken', token);
        
        // Redirect to home page
    } else {
        // There is no token stored, redirect to login page
    }
}
// Appmixer is initialized somewhere when your application starts
const appmixer = new Appmixer();

appmixer.onInvalidAccessToken(function(){
    // Inside this function we could redirect users to the login form,
    // or if you have what's neccessary you can generate new token by
    // calling the proper function (appmixer.api.authenticate) which
    // will return new fresh token.
});

function initialize() {
    
    // We look into sessionStorage using the same key we use for
    // storing the token when we authenticate into our application
    const token = sessionStorage.getItem('myAccessToken');
    
    // Now in addition to checking if there's a stored token
    // we also check if is still valid
    if (token){
        // If token exists, we set it on the Appmixer instance
        // and redirect to home page
        appmixer.set('accessToken', token);
        
        // Redirect to home page
    } else {
        // There is no token stored, redirect to login page
    }
}
const instance1 = new Appmixer(...);
const instance2 = new Appmixer(...);

const persistedToken1 = sessionStorage.getItem('token1');
const persistedToken2 = sessionStorage.getItem('token2');

instance1.set('accessToken', persistedToken1);
instance2.set('accessToken', persistedToken2);
Managing Authentication

Method

Description

appmixer.ui.Integrations()

Constructor function that accepts config object. The config object must contain at least the el property that points to a container DOM element where the Integrations will be rendered.

open()

Render the Integrations inside its container.

close()

Close the Integrations.

on(event, handler)

React on events of the Integrations widget. See below for the list of events the Integrations widget supports.

off([event, handler])

Remove event listeners. If no arguments are provided, remove all event listeners. If only the event is provided, remove all listeners for that event. If both event and handler are given, remove the listener for that specific handler only.

reload()

Refresh the Integrations.

Event

Callback

Triggered when ...

integration:create

function(templateId)

User clicks to opens an available Integration.

integration:edit

function(integrationId)

User clicks to edits an active Integration.

var integrations = appmixer.ui.Integrations({
    el: '#your-integrations-div',
});
integrations.on('integration:create', async function(templateId) {
    // Create integration flow as a clone of the template. Disconnect
    // accounts because they might not be shared with the end user.
    var integrationId = await appmixer.api.cloneFlow(templateId, { connectAccounts: false });
    // Identify the clone as an integration.
    await appmixer.api.updateFlow(integrationId, { templateId });

    // Open appmixer.ui.Wizard
    var wizard = appmixer.ui.Wizard({
        el: '#your-wizard-div',
        flowId: integrationId,
    });
    wizard.open();
    integrations.reload();
});
integrations.on('integration:edit', function(integrationId) {
    // Open appmixer.ui.Wizard
    var wizard = appmixer.ui.Wizard({
        el: '#your-wizard-div',
        flowId: integrationId,
    });
    wizard.open();
});
integrations.open();

Flows Metadata & Filtering

Appmixer flows can contain custom metadata specific to your application. This is especially useful for filtering flows and displaying different Flow Managers for different kinds of flows.

Setting Custom Metadata

Each flow can have assigned a custom metadata object. This metadata object is called customFields throughout the documentation and can be any JSON object. In the examples below, we show how to define custom metadata on a flow using each of the available Appmixer interfaces (REST API, Appmixer SDK).

Setting Custom Metadata using the REST API

Use the customFields object when creating new flows:

$ curl -XPOST "https://api.appmixer.com/flows" -H "Content-Type: application/json" -d '{ "flow": FLOW_DESCRIPTOR, "name": "My Flow #1", "customFields": { "category": "healthcare" } }'

... or when updating existing flows:

$ curl -XPUT "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Content-Type: application/json" -d '{ "customFields": { "category": "healthcare" } }'

Setting Custom Metadata using the Appmixer SDK

Create new flows with customFields:

appmixer.api.createFlow('myFlow', FLOW_DESCRIPTOR, {
    "customFields": { "category": "healthcare" }
});

... or update existing flows with:

appmixer.api.updateFlow('9089f275-f5a5-4796-ba23-365412c5666e', {
    "customFields": { "category": "healthcare" }
});

Search Flows Based on Custom Metadata

Now when you have flows with your own custom metadata defined, you can use the filters to search for flows based on values of your metadata.

Filter Flows using the REST API

$ curl "https://api.appmixer.com/flows?filter=customFields.category:healthcare" -H "Authorization: Bearer [ACCESS_TOKEN]"

Moreover, you can use the following operators in your filter queries: ['!', '^', '$', '~', '>', '<', '$in']. For example, to filter flows that do not fall into our "healthcare" category, we can use:

$ curl "https://api.appmixer.com/flows?filter=customFields.category:!healthcare" -H "Authorization: Bearer [ACCESS_TOKEN]"

Filter Flows using the Appmixer SDK

appmixer.api.getFlows({
    limit: 20,
    offset: 0,
    projection: "-thumbnail",
    sort: "mtime:-1",
    filter: "customFields.category:healthcare"
})

Appmixer UI SDK FlowManager with Custom Metadata

The main power of custom metadata is that you can display multiple FlowManager UI instances on your pages for different categories of flows. A typical example is to have a custom field "template" and split all flows in Appmixer to "template" flows and actual flow instances. You can achieve that by having two list of flows in your UI displayed with the FlowManager UI widget, one for flow templates and one for actual flow runs. The flow templates would only display flows with "template:true" filter and the other list flows with "template:!true" filter. Now based on where the user actually created the new flow, you would assign either template:true or template:false custom field to the flow object.

Filter Flows with FlowManager

Use the customFilter option when creating an instance of your flow manager. The FlowManager widget will then use this filter whenever requesting list of flows to display from the API:

const flowManagerTemplates = new appmixer.ui.FlowManager({
    el: '#your-flow-manager-templates-div',
    options: {
        customFilter: {
            'customFields.template': 'true'
        }
    }
});
flowManagerTemplates.open();

You can have another instance of FlowManager with non-template flows:

const flowManagerFlows = new appmixer.ui.FlowManager({
    el: '#your-flow-manager-flows-div',
    options: {
        customFilter: {
            'customFields.template': '!true'
        }
    }
});
flowManagerFlows.open();

jsonwebtoken
secrets
here
install
customFields
Appmixer CLI

Customizing modifiers

This tutorial shows you how you can customize your variable modifiers

Introduction

Appmixer is shipped with predefined modifiers which allows you to transform the variables in your flow on many different ways. The modifiers are organised in categories. Every modifier belongs to one or more categories.

Changing existing modifiers

Let's add a new modifier to the existing ones. This new modifier will take a date as an input and return it in a format specified as another argument. We are going to use Appmixer API to accomplish this.

The first thing we need is the current modifiers definition. To obtain it, we are going to use the GET /modifiers endpoint, you should get a response with the following structure:

{
    "categories": {
        "object": {
            "label": "Object",
            "index": 1
        },
        "list": {
            "label": "List",
            "index": 2
        },
        ...
    },
    "modifiers": {
        "g_stringify": {
            "name": "stringify",
            "label": "Stringify",
            "category": [
                "object",
                "list"
            ],
            "description": "Convert an object or list to a JSON string.",
            "arguments": [
                {
                    "name": "space",
                    "type": "number",
                    "isHash": true
                }
            ],
            "returns": {
                "type": "string"
            },
            "helperFn": "function(value, { hash }) {\n\n                return JSON.stringify(value, null, hash.space);\n            }"
        },
        "g_length": {
            "name": "length",
            "label": "Length",
            "category": [
                "list",
                "text"
            ],
            "description": "Find length of text or list.",
            "arguments": [],
            "returns": {
                "type": "number"
            },
            "helperFn": "function(value) {\n\n                return value && value.hasOwnProperty('length') ? value.length : 0;\n            }"
        },
        ...
    }
    
}

Next step is changing the modifiers definition, we need to use the PUT /modifiers endpoint. The request body must contain the whole modifiers definition, like the one we obtained previously, in addition to desired modifications. So to add the modifier, we will add a new key under the modifiers object:

"formatDate": {
    "label": "Format Date",
    "category": ["date"],
    "description": "Transforms the value into given date format",
    "arguments": [
        { "name": "Format", "type": "string", "isHash": false }
    ],
    "returns": {
        "type": "string"
    },
    "helperFn": "function(value, format, { helpers }) { return helpers.moment(value).format(format) }"
}

Note three things in this definition:

  • The name property has been deprecated and is no longer required.

  • The isHash property in the argument determines if this argument will be an ordinal argument in the helperFn or it will be included in the hash object of the last argument.

  • The last argument is always an options object which includes a hash object with the hash arguments and the helpers. The helpers object includes the moment library for date manipulations.

To illustrate better this points, the following form is equivalent:

"formatDate": {
    "label": "Format Date",
    "category": ["date"],
    "description": "Transforms the value into given date format",
    "arguments": [
        { "name": "format", "type": "string", "isHash": true }
    ],
    "returns": {
        "type": "string"
    },
    "helperFn": "function(value, format, { hash: { format }, helpers }) { return helpers.moment(value).format(format) }"
}

With our modifications to modifiers definition, we send the request with the whole definition as the body to the PUT /modifiers endpoint. We should receive as a response the modifiers definition, including the new modifier:

{
    "categories": {
        "object": {
            "label": "Object",
            "index": 1
        },
        "list": {
            "label": "List",
            "index": 2
        },
        ...
    },
    "modifiers": {
        "g_stringify": {
            "name": "stringify",
            "label": "Stringify",
            "category": [
                "object",
                "list"
            ],
            "description": "Convert an object or list to a JSON string.",
            "arguments": [
                {
                    "name": "space",
                    "type": "number",
                    "isHash": true
                }
            ],
            "returns": {
                "type": "string"
            },
            "helperFn": "function(value, { hash }) {\n\n                return JSON.stringify(value, null, hash.space);\n            }"
        },
        "g_length": {
            "name": "length",
            "label": "Length",
            "category": [
                "list",
                "text"
            ],
            "description": "Find length of text or list.",
            "arguments": [],
            "returns": {
                "type": "number"
            },
            "helperFn": "function(value) {\n\n                return value && value.hasOwnProperty('length') ? value.length : 0;\n            }"
        },
        ...
        // At some place inside the object
        "formatDate": {
            "label": "Format Date",
            "category": ["date"],
            "description": "Transforms the value into given date format",
            "arguments": [
                { "name": "Format", "type": "string", "isHash": false }
            ],
            "returns": {
                "type": "string"
            },
            "helperFn": "function(value, format, { helpers }) { return helpers.moment(value).format(format) }"
        }
    }
}

Now we can use our new modifier on our flows:

Something important to note, is that all modifiers will return the original value if there is a runtime error during the execution of the helperFn. For example, if the value we are applying our new modifiers is a string apples, the modifier will return apples, since the value is not a valid date and the helperFn will fail, so try to make sure that the modifiers are applied always on valid values.

Appmixer CLI

Appmixer command-line tool.

Use the Appmixer CLI tool to interact with the Appmixer engine remotely from the command line. It can be used to list flows, start/stop flows but more importantly, to develop and publish custom components.

Installation

  • npm install -g appmixer

Help

Display the command options with the -h option:

$ appmixer -h
Usage: appmixer [options] [command]

Appmixer command line interface.

Options:
  -v, --version  output the version number
  -h, --help     output usage information

Commands:
  download|d     Download component.
  flow|f         Flow commands.
  init|i         Initialize component.
  login|l        Login into Appmixer API.
  logout|o       Logout from Appmixer API.
  pack|p         Pack component into archive.
  publish|pu     Publish component.
  remove|rm      Remove component.
  test|t         Test component, authentication module, ...
  url|u <url>    Set Appmixer API url.
  help [cmd]     display help for [cmd]

Go to https://docs.appmixer.com/appmixer/ to find more information.

Each command has its own help information:

$ appmixer flow -h
Usage: appmixer flow <command>

Flow commands.

Options:
  -h, --help         output usage information

Commands:
  start|s <flowId>   Start flow.
  stop|t <flowId>    Stop flow.
  remove|r <flowId>  Remove flow.
  ls|l [flowId]      Ls flow.
  help [cmd]         display help for [cmd]

Initialization

$ appmixer url https://api.appmixer.com

Login to your Appmixer account and enter your password:

$ appmixer login david@client.io
prompt: password:

Login successful.

Creating Custom Components

Generate a sample component

The best way to start implementing your own custom components is to use the generator tool to generate a sample component. This gives you the basic skeleton of your component/service that you can later tweak. Use the appmixer init example command to generate a sample component:

$ appmixer init example

Usage: appmixer init example <type> [path]

Options:
  --vendor [vendor]  Your vendor name.
  --verbosity        Verbosity level of the generated output log. Number in [0 - 2] range. Defaults to 1.
  -h, --help         output usage information

Examples:
  $ appmixer init example no-auth

Environment Variables:
AM_GENERATOR_COMPONENT_PATH		Components base directory. [path] argument overrides this variable if used.

As you can see, we have to give this command a type of example we want to generate. We will start with the most simple service type that does not use any authentication (OAuth1, OAuth2, API keys). In other words, the component will not ask the user to connect any account in the Inspector panel when using the component in a flow.

$ appmixer init example no-auth
Created directory structure ./appmixer/myservice/mymodule/MyComponent
Creating component appmixer.myservice.mymodule.MyComponent
Created component manifest file ./appmixer/myservice/mymodule/MyComponent/component.json.
Created component package file ./appmixer/myservice/mymodule/MyComponent/package.json.
Created component behaviour file ./appmixer/myservice/mymodule/MyComponent/MyComponent.js.
Created service manifest file ./appmixer/myservice/service.json.
Created quota module file ./appmixer/myservice/quota.js.

Example successfully generated.

my-components
└── appmixer
    └── myservice
        ├── mymodule
        │   └── MyComponent
        │       ├── MyComponent.js
        │       ├── component.json
        │       └── package.json
        ├── quota.js
        └── service.json

You can now use appmixer pack appmixer/myservice && appmixer publish commands to upload it.

The command prints all the generated files and the directory structure. Note that we have just generated a working component with the myservice.mymodule.MyComponent type.

Pack your component

Now we're ready to pack our service (i.e. create a zip archive with all the generated files) using the appmixer pack appmixer/myservicecommand:

$ appmixer pack appmixer/myservice
Packing component directory: /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice

Files found in /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice
- mymodule
- quota.js
- service.json

You are in a directory with service.json file.
I'm going to create directory structure based on name in your service.json file.


3866 total bytes
appmixer.myservice.zip

You can pack the entire service or just the module or even individual components by providing the path to the appmixer pack command.

The pack command generated the appmixer.myservice.zip file in the current directory.

Publish your component

The last step is to publish our component for our users to use. This is done using the appmixer publish command:

$ appmixer publish appmixer.myservice.zip
Publishing archive: /Users/daviddurman/Projects/appmixer/my-components/appmixer.myservice.zip
Published.

Our component is now published and ready to be used:

Notice the labels and icons of the service in the component panel on the left and of the component in the Inspector selector match our definitions from the myservice/service.json and myservice/mymodule/MyComponent/component.json manifest files.

List all available components

To see all the available components uploaded to Appmixer, use the appmixer component ls command:

$ appmixer component ls
appmixer.actimo.contacts.CreateContact
appmixer.actimo.contacts.DeleteContact
appmixer.actimo.contacts.GetContact
appmixer.actimo.contacts.GetContacts
appmixer.actimo.contacts.UpdateContact
appmixer.actimo.groups.GetGroups
appmixer.actimo.messages.SendMessage
appmixer.apify.crawlers.Crawl
appmixer.asana.projects.CreateProject
appmixer.asana.projects.NewProject
appmixer.asana.tasks.CreateStory
appmixer.asana.tasks.CreateSubtask
appmixer.asana.tasks.CreateTask
appmixer.asana.tasks.NewComment
...

Remove your component

If you decide your component is no longer needed, you can remove your component from the system with the appmixer remove command:

appmixer remove appmixer.myservice.mymodule.MyComponent

The remove command lets you specify any portion of the fully qualified component type. For example, if you want to remove the entire service, you can do that with:

appmixer remove appmixer.myservice

It's important to note that removing a component that is used in any of the running flows will cause the flows to stop working. The flows will start generating errors that will be visible in the Insights section. The remove command does not automatically stop the flows. Always make sure the component is not used in any of the running flows before you remove it. IMPORTANT: Removing a component cannot be undone!

Updating your component

You can re-publish (appmixer publish) your component which effectively replaces the old component with the new one.

Note that re-publishing a component will not replace the component in a running flow. Flows wanting the new version of the component have to be stopped and started again to load the new version of the component.

Also note that if you "dramatically" change your component, e.g. removing ports, flows using the component type might require re-configuration since the ports that were used to connect to other components will not exist. The same goes for other major changes: changing output parameters that are used in connected components, changing inspector config and properties, etc.

Testing your component

Writing/updating code of your component, re-publishing it and re-configuring/restarting your flow every time you need to test your component would be a long process. Therefore, the Appmixer CLI tool provides a command to test your component locally on your machine, before publishing it to Appmixer. The appmixer test command allows you to test your component by sending messages to it, configuring properties and testing authentication methods:

$ appmixer test -h
Usage: appmixer test <command>

Dev tools for testing your files.

Options:
  -h, --help                   output usage information

Commands:
  dump|d <moduleName>          Get stored authentication data from previous commands.
  auth|a <authModuleFile>      Authenticate service.
  component|c <componentFile>  Test component.
  help [cmd]                   display help for [cmd]

We'll start by exploring the component testing tool:

$ appmixer test component -h
Usage: appmixer test component [options] [componentDir]

Options:
  -f, --transform [transform]    specify transformer
  -i, --input [input]            input test message object (default: [])
  -m, --mime [mime]              mime type, application/json by default
  -p, --properties [properties]  component properties (JSON format)
  -s, --no-state                 do not show component's state
  -t, --tickPeriod [tickPeriod]  tick period (in ms), default is 10000 ms
  -h, --help                     output usage information

Examples:
  Following example will send input message { "to": "your@email.com" } to component's input port 'in'.
  You always have to specify to which input port you want to send message.
  $ appmixer test component [path-to-your-component-directory] -i '{ "in": { "to": "your@email.com" } }'

  This is how to specify transformer function from transformer file.
  $ appmixer test component [path-to-component] -i '{}' -f './transformers#channelsToSelectArray'

  How to set properties and tick period:
  $ appmixer t c [path-to-component] -p '{ "channelId: "123XYZ" }' -t 2000

  You can send more than one message:
  $ appmixer t c [path-to-component] -i '{ "in": { "to": "first@email.com" }}' -i '{ "in": { "to": "second@email.com" }}'

  You can run appmixer command in your component's directory:
  $ appmixer test c
  Directory has to contain component.json file and component's source code file.

If you're developing component that needs authentication, use 'appmixer test auth' before.
If you're developing Oauth2 component you might need to refresh access token before calling this command.
Use 'appmixer test auth refresh' for such purposes.

Some of the feature (context.store, context.componentStaticCall, ... will work only if you are logged in into Appmixer.
If you want to test them in your component, call appmixer login first.

For more information, checkout our documentation at:
https://docs.appmixer.com/appmixer/component-definition/authentication
https://docs.appmixer.com/appmixer/appmixer-trial/custom-component-helloappmixer

Let's say we want to send a message to our component and see how it reacts, i.e. what is the output of our component. We know our component has an input port called in and requires the sourceData property as part of the incoming messages (see the component.json file of your MyComponent, especially the inPorts section). We can use the test command for this:

$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:
{"name":"component","hostname":"MacBook-Pro.local","pid":26397,"level":30,"msg":"{\"properties\":{\"correlationId\":\"8f2ce09e-3f6d-48dd-81bd-e80189f70bb4\",\"gridInstanceId\":null,\"contentType\":\"application/json\",\"contentEncoding\":\"utf8\",\"sender\":{\"componentId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"type\":\"appmixer.myservice.mymodule.MyComponent\",\"outputPort\":\"out\"},\"destination\":null,\"correlationInPort\":null,\"componentHeaders\":{},\"signal\":false},\"content\":{\"sourceData\":\"foo\"},\"scope\":{\"_walkthrough\":[{\"targetId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"links\":[]}]}} { componentId: '70eb49e9-88df-4d0e-9549-d4e4f0f755c0',\n  flowId: 'c5d05118-13b5-4ec8-a8fe-2e6ef51fdd52',\n  userId: '5da735715abc4a671dfb592f',\n  componentType: 'appmixer.myservice.mymodule.MyComponent',\n  type: 'data',\n  portType: 'out',\n  port: 'out',\n  inputMessages: { in: [ [Object] ] },\n  annotatedMsg: { sourceData: 'foo' } }","time":"2019-10-16T15:21:22.169Z","v":0}

Component sent a message to its output port: out
{ sourceData: 'foo' }

Component's receive method finished in: 45 ms.

Return value from receive method:
undefined

Component's state at the end:
State is empty, component did not store anything into state.

Stopping component.

Destroying component.

The command prints out a lot of useful information, for example the output of the component (see "Component sent a message to its output port: out"). Since our component just forwards the same data that it received, we see the same object we sent to it: { sourceData: 'foo' }.

Now suppose we have a bug in our code in MyComponent.js, a syntax error:

module.exports = {
    receive(context) {
        myBadError
        context.sendJson(context.messages.in.content, 'out');
    }
}

Now re-running the test gives us:

$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:

[ERROR]: myBadError is not defined

Downloading your component

When your component is published, you can always download it back to your local file system. To download the source code of your component, use the appmixer download command:

$ appmixer download -h
Usage: appmixer download selector

Options:
  -o, --out [dir]  Where you want save it.
  -h, --help       output usage information

Examples:
  Download all files for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail

  Download only package.json file for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail/package.json

  Download main SendEmail.js file only:
  $ appmixer download vendor.google.gmail.SendEmail/SendEmail.js

  Download all gmail components:
  $ appmixer download vendor.google.gmail

  Download auth.js file for gmail:
  $ appmixer download vendor.google.gmail/auth.js

  Download quota.js file for gmail:
  $ appmixer download vendor.google.gmail/quota.js

In our case, the download command would look like:

$ appmixer download appmixer.myservice
$ ls
appmixer.zip
$ unzip appmixer.zip
$ tree appmixer/
appmixer
└── myservice
    ├── mymodule
    │   └── MyComponent
    │       ├── MyComponent.compiled.js
    │       ├── MyComponent.js
    │       ├── component.json
    │       ├── package-lock.json
    │       ├── package.json
    │       └── sourcemap-register.js
    └── service.json

3 directories, 7 files

Working With Flows

Listing Flows

You can list all your flows using the appmixer flow ls command:

$ appmixer flow ls
[Get Current Weather] : [a5769b32-8835-44ad-82e1-ece2874ea3e3] : [stopped]
[Uptime Monitor] : [5b5fd3a0-0ef2-4fc5-9a60-164f5e44c660] : [stopped]
[Daily Rainy Day Alert] : [ec1103e5-c66c-41c4-9223-029d2b328f5c] : [stopped]

If you want to see see just one flow and its stage (running/stopped), you can pass the ID of the flow in the flow ls command:

$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow: a5769b32-8835-44ad-82e1-ece2874ea3e3
Stage: stopped

To see the flow descriptor of a flow (i.e. JSON object that represents the entire configuration of the components in the flow and their connections), add the --descriptor or -d flag:

$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3 --descriptor
{
    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
        "type": "appmixer.utils.controls.OnStart",
        "label": "OnStart",
        "source": {},
        "config": {},
        "x": 88,
        "y": 110
    },
    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
        "type": "appmixer.utils.weather.GetCurrentWeather",
        "label": "GetCurrentWeather",
        "source": {
            "location": {
                "5ba2740c-929b-4599-b09e-fc8f8d5dac82": [
                    "out"
                ]
            }
        },
        "config": {
            "transform": {
                "location": {
                    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
                        "out": {
                            "type": "json2new",
                            "lambda": {
                                "city": "Prague",
                                "units": "metric"
                            }
                        }
                    }
                }
            }
        },
        "x": 286,
        "y": 110
    },
    "ab6a22f8-916d-4aab-a5e3-2abedc03917c": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
            "in": {
                "0f366972-08fe-4cd4-80c0-227cfb6db54b": [
                    "weather"
                ]
            }
        },
        "config": {
            "transform": {
                "in": {
                    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
                        "weather": {
                            "type": "json2new",
                            "lambda": {
                                "from_email": "info@appmixer.com",
                                "subject": "Appmixer: Current Weather",
                                "text": "Temperature: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.temp}}} dgC\nPressure: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.pressure}}} hPa\nHumidity: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.humidity}}}%\nCloudiness: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.clouds.all}}}%",
                                "to": ""
                            }
                        }
                    }
                }
            }
        },
        "x": 484,
        "y": 110
    }
}  

Starting and Stopping Flows

You can start and stop flows using the appmixer flow start and appmixer flow stop commands:

$ appmixer flow start a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully started.

$ appmixer flow stop a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully stopped.

Removing Flows

To remove flows, use the appmixer flow remove command:

$ appmixer flow remove 2058a1ee-9c19-4e94-bd7a-0da7f9bed973
Flow 2058a1ee-9c19-4e94-bd7a-0da7f9bed973 successfully removed.

Note that the removed flow will be automatically stopped if it was running. Also note that this action cannot be undone.

Working with Modifiers

You need admin privileges to run these commands. You should not modify Modifiers if there are any running flows using them. Such operation would break the flows.

Downloading Modifiers

The next command will download modifiers from Appmixer and save into modifiers.json file.

$ appmixer modifiers get

Publishing Modifiers

The next command will publish modifiers into Appmixer.

$ appmixer modifiers publish file-with-your-modifiers.json

Deleting Modifiers

The next command will delete existing modifiers from Appmixer. The next time Appmixer starts, it will load the default set of Modifiers.

$ appmixer modifiers delete

Sharing Flows

This tutorial shows how flows can be shared in Appmixer either using the API or the SDK.

Introduction

Appmixer provides a built-in support for sharing flows with other users. When a user shares their flow, they provide a list of email addresses of other Appmixer users that they want to share their flow with:

When a flow is shared, users from the share list can see the flow in their accounts.

Note that the flow is shared in the sense that there's only one instance of the flow in the Appmixer engine. Therefore, if the recipient of the shared flow e.g. starts the flow, the flow appears started also for the owner of the flow and for the other recipients too. If you want to have separate instances, clone the original flow and share each clone with one recipient.

Sharing Flows Using the Appmixer JavaScript SDK

To share a flow using the Appmixer SDK, update the flow with the special sharedWith list. The list contains objects with email and permissions properties where email is the email address of another Appmixer user and permissions is a list of permissions. Currently, the following permissions are available:

Permission

Description

read

The user can read the flow but cannot edit it.

start

The user can start the flow.

stop

The user can stop the flow.

Sharing flows with other users for editing is not yet available.

Example:

appmixer.api.updateFlow('9089f275-f5a5-4796-ba23-365412c5666e', {
    sharedWith: [{
        email: 'david@client.io',
        permissions: ['read', 'start', 'stop']
    }]
}).then(() => {
    console.log('Flow successfully shared.');
}).catch((err) => {
    console.log('Something went wrong.', err);
});

To unshare a flow, simply update the sharedWith array with an empty list:

appmixer.api.updateFlow('9089f275-f5a5-4796-ba23-365412c5666e', {
    sharedWith: []
}).then(() => {
    console.log('Flow successfully unshared.');
}).catch((err) => {
    console.log('Something went wrong.', err);
});

Note that the sharedWith array is returned from the appmixer.api.getFlow() method. However, only the owner can see the list of users they shared the flow with. The recipients of the shared flow only see one item in the list and that's the email address and permissions for themselves.

Define Sharing Options in the Share Dialog

The FlowManager and Designer UI widgets contain menus that allow the user to share their flows. Invoking the share menu item brings up a dialog box that looks like this:

The Appmixer SDK allows you to configure the sharing options available to the user. For example, you can define that the share dialog will give the user permission to share their flow only for read (and not stop/start) with:

appmixer.ui.FlowManager({
  // ... other options ...
  options: {
    sharePermissions: [
      { label: 'Read', value: 'read' }
    ]
  }
});

...

appmixer.ui.Designer({
  // ... other options ...
  options: {
    sharePermissions: [
      { label: 'Read', value: 'read' }
    ]
  }
});

You can also define the share types (email, domain, scope):

appmixer.ui.FlowManager({
  // ... other options ...
  options: {
    shareTypes: [
      { label: 'Email', value: 'email', placeholder: 'Enter an email address' },
      { label: 'Scope', value: 'scope', placeholder: 'Enter a scope' },
      { label: 'Domain', value: 'domain', placeholder: 'Enter a domain' }
    ],
    sharePermissions: [
      { label: 'Read', value: 'read' }
    ]
  }
});

Sharing Flows Using the Appmixer API

Sharing flows with the Appmixer API follows the same pattern as sharing flows using the Appmixer SDK. To share a flow, update the flow with the sharedWith list:

curl -XPUT "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" -H "Content-Type: application/json" -d '{ "sharedWith": [{ "email": "david@client.io", "permissions": ["read", "start", "stop"]}] }'

Setting ACL

This tutorial will show you how to set ACL rules for different groups of users and for different types of resources.

Components

If you open Appmixer Backoffice you can see an ACL section in the left menu. Then you can choose among routes and components.

This is the default ACL setting you will see.

If you want to set ACL for different component start with deleting the general rule that allows users to access all components. Because if you did not do this step, the ultimate rule user - * - * would overrule any other rule you would create for user scope.

If you open Appmixer now (signed in as ordinary user) you will see no components in Designer.

Let's now add all components from appmixer vendor back with the following rule:

When you refresh Appmixer now, you will see all the Appmixer components back.

Let's break down those four properties you can set for each ACL rule.

  • role: admin | user - those are the default roles/scopes in the system. You can also use an email address or a domain. It means you can define ACL(s) for a single users (email address) or for all users from certain email domain. Let's say your company is called acme and your employees all have an email address their-name@acme.com. Then the domain for ACL rule would be acme.com.

  • resource: component type prefix (appmixer.google.gmail* for example). This allows to create rules for components belonging to certain vendor, service or module. In the example above we created a rule for all appmixer components. The resource string was appmixer* which will cover all appmixer components.

  • action: action the rule is for. In case of components the only action is use. You can keep it to *. There are more actions when it comes to rules for API routes.

  • attributes: private or non-private. If set to non-private the rule will apply to component that do not have private: true set in component.json. If set to private it will allow users to see private components as well.

Let's try more examples. Show only utils, google and slack components. We're going to add the following three rules:

When you refresh Appmixer Designer you can see this:

Because we used attribute non-private for all our rules we can see only components where private: true is not set in their manifest. Most appmixer component modules user private auxiliary components for listing items into menus (slack channels for example). Therefore we don't see those private components in this menu:

Let's change the ACL rule for slack module to see those private components as well.

User can see private components now.

When you start writing your own components you should use your own vendor (not appmixer). We are adding Hello World component for example under vendor acme. Its component type is acme.custom.component.HelloWorld. Then we have to publish the component into Appmixer and create new ACL rule that will make it accessible to users.

At this point we can find the new Hello World component among other components.

Another example will show how you can user resource property to display only gmail module.

Routes

If you want to limit users from certain role, first you need to delete the general rule. We will show it on user role.

Those rules are store in mongo DB in the following form:

{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "tester",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*"
            ]
        }
    ]
}

Attributes

{
    "_id" : ObjectId("5f3696f502ea801d405bb112"),
    "userId" : ObjectId("5f3696f102ea801d405bb10e"),
    "flowId" : "c57f1d30-52fc-4a48-97d9-5d1c296064da",
    "stage" : "stopped",
    "name" : "Flow with metadata",
    "btime" : ISODate("2020-08-14T13:51:49.900Z"),
    "mtime" : ISODate("2020-09-01T14:57:43.536Z"),
    "flow" : {
    },
    "mode" : "module",
    "sharedWith" : [],
    "customFields" : {
        "category" : "test-example"
    }
}

where flow.customFields.category = "test-example". This can be done through API. But we want only admin users to be able to do so. The following set of ACL rules will allow admins to create custom fields, but it will allow users to only read those fields.

{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*   // are able to read all attributes, including custom fields
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", "!customFields"   // but they're not able to create them
            ]
        }
    ]
}

So if you try to send an API request to create new flow with the following body (as a user with user scope).

{
    "flow":{},
    "name":"New flow",
    "customFields": {
        "category": "test-category"
    }
}

The custom field category won't be created. Property customFields will be an empty object. Although if you create the same flow with admin user that customFields property with category will be created and your user with user role will be able to read it (API will return it).

With the following set of rules, users with user role won't be able to create and read property customFields.

{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }
    ]
}

The same way you restrict access to each property on flows collection.

The next JSON shows different syntax with similar functionality. User with role user can read/create flows with properties flow, name, flowId, stage, sharedWith, but they won't be able to create customFields property.

{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read", 
                "create"
            ],
            "attributes" : [ 
                "flow", 
                "name", 
                "flowId", 
                "stage", 
                "sharedWith"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "!create"
            ],
            "attributes" : [ 
                "customFields"
            ]
        }
    ]
}

You can also use nested objects as attributes. Let's say the customFields object contains two properties:

{
    "_id" : ObjectId("5f4e654c2c2d2f4d59ca89f9"),
    "userId" : ObjectId("5f3696f102ea801d405bb10e"),
    "flowId" : "46d80c28-72c3-42f1-94ba-2c161e26f041",
    "stage" : "stopped",
    "name" : "New flow",
    "btime" : ISODate("2020-09-01T15:14:20.999Z"),
    "mtime" : ISODate("2020-09-01T15:14:20.999Z"),
    "flow" : {},
    "mode" : "module",
    "sharedWith" : [],
    "customFields" : {
        "visible" : "test visible",
        "not-visible" : "test non visible"
    }
}

And we want the users to be able to see only visible property, the rules would look like this:

{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*", 
                "!customFields.not-visible"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }
    ]
}

Users are not able to create customFields, they are allowed to read customFields.visible property, but not the customFields.not-visible.

Highrise

We keep the module in Appmixer for the older customers who have their Oauth2 application. If you are a new Appmixer customer, you won't be able to create the OAuth2 application and therefore use the Highrise module. In that case, you should remove it from the platform.

User-Agent

Requests to the Highrise API contain headers with User-Agent property. This is set by default to Appmixer, but it can be overwritten using Backoffice:

DeepAI

https://deepai.org/

Sign-up at DeepAI

Visit https://deepai.org and sign-up:

Copy api-key from your profile dashboard:

Set the DEEPAI_API_KEY environment variable in your Appmixer engine deployment.

Or set it through the Backoffice.

And then add you own apiKey value:

Slack

Register OAuth2 app

Name your application and select a workspace.

Then you receive your clientID and clientSecret.

Copy and paste those secrets into the Backoffice.

The next thing is adding the redirect URL and scopes.

And add these scopes.

In order to use the application accross workspaces you have to distribute it.

Events API

The Request URL has to point to your Appmixer backend server. The rest of the URL /services/appmixer/slack/events has to remain like this. Your Appmixer backend server has to be running before you enter the URL. Slack will immediately try to connect to that URL.

Then you need to subscribe to messages.channels and message.groups events.

Don't forget to hit Save Changes:

You can now use the appmixer.slack.list.NewChannelMessageRT component in a flow.

Slack app migration

In order to migrate your Slack legacy app, go to Oauth & Permission section.

Then scroll down to Scopes and hit the Update Scopes button.

Ignore the first page, because that's all about scopes for bots.

Scroll all the way down and hit continue. You get to a second page with user scopes.

Select channels:read, channels:history, channels:write, groups:read, groups:write, groups:history, chat:write and users:read.

Verify the scopes and migrate the app.

And confirm. The Appmixer Slack module since version 4.2.1 is already updated and ready for the new Slack apps.

Flows that use the SendPrivateChannelMessage component won't work. The component will appear unauthenticated. The reason is a different Oauth scope in the new Slack app version. In this case, the user has to authenticate the component again.

Screenshot API

Appmixer offers a built-in component for taking Screenshots.

Google

Domain verification

First, open Google developers console and your Appmixer project and follow instructions in the next picture.

Then copy&paste your ngrok URL.

Then you have to verify domain ownership.

Click 'Take me there' and then 'Add a property'.

Then download the HTML verification file.

After you download your HTML verification file, copy it into Appmixer engine.

After then, click the 'Verify' button and that's it. You don't need to restart the Appmixer engine.

Now, you can go back to developer console and add the ngrok domain once more.

But this time, because it's already verified, it will appear in the list of your domains.

People Tasks

Appmixer PeopleTask feature allows you to add human entry points in your flows.

Introduction

The Appmixer built-in PeopleTask module allows you to add human decision points in your flows. Each of these decision points create a new task that can either be approved, rejected or due (when the current time passes the "decision by" time configuration of the task). Once the decision is made, the flow continues. As an example, consider a flow for approving employee vacation requests. Your flow may look like this:

When an employee adds a new event in Google Calendar requesting a vacation, the person that's configured as an "approver" receives an email from the “RequestApprovalEmail” component that looks like this:

The flow for this particular request is blocked until the approver either approves or rejects this vacation request by clicking on the "Approve"/"Reject" links in their email. If the task is approved, our flow then continues to update the event in Google Calendar by adding a "CONFIRMED" text to the description of the event. If the request is rejected, the employee receives an email that their request was rejected. Both the approver and requester can visit their People Task dashboard that shows them an overview of all their approved/rejected/pending tasks:

PeopleTask Components

Appmixer provides two people task components that you can use in your flows: RequestApprovalEmail and RequestApproval. Both are part of the utils.tasks module. Note that both these components are template components and are assumed to be adjusted for your specific needs. For example, you might want to use your own email provider or use your own email template with your own branding and different wording. You might as well have your component send the task requests via SMS instead of email, etc. ...

The implementation of the sample componets is pretty simple since it leverages the built-in PeopleTask REST API. See blow for details.

PeopleTask REST API

Normally, you don't need to deal directly with the PeopleTask REST API unless you have some specific requirements, you need to update the tasks from an external system or if you want to implement your own custom components using the PeopleTask API.

Create a new task

You can create new task by sending an HTTP POST request to the people-task/tasks endpoint:

You should receive a JSON object back that looks like this:

Having the ID of the newly created task, you can register a webhook that will be called when the status of the task changes (i.e. from "pending" to e.g. "approved" or "rejected"):

Now when the status of the task changes, the Appmixer engine sends an HTTP POST request to the webhook url registered with the task. In our case, it would send a request to https://mywebhooks.myserver.com with data that looks like this (assuming the task was approved in the meantime):

As you can see, the task was approved (status is "approved") and we also receive the time the decision was made on (decisionMade).

Notice also the approverSecret and requesterSecret returned when you create a new task. These secrets allow you list and manipulate tasks of a user for which you have the secret instead of the currently signed-in user (i.e. identified by the token in the Authorization header).

Listing all tasks of a user

To get a list of all the tasks of a user (identified by the "Bearer" token in the "Authorization" header), you can issue a GET request to the /people-task/tasks endpoint:

The returned JSON may looks like this:

You can also get all the tasks of a user for which you have a secret (approverSecret or requesterSecret) instead of the currently signed-in user:

Approving and Rejecting Tasks

To approve or reject tasks, you can issue an HTTP PUT request to the /people-task/tasks/:id/approve or /people-task/tasks/:id/reject endpoints:

or to reject the task:

Appmixer engine then looks up all the webhooks registered for this task and calls these webhooks notifying the receiver about the task status change.

Note that the example above approves/rejects a task that belongs to the signed-in user (i.e. the user identified by the token in the "Authorization" header). However, you can approve/rejects tasks of any user if you have their "approverSecret" (just send a JSON to the same endpoint with { "secret": "MY_APPROVER_SECRET" } )

PeopleTasks SDK Component

The Appmixer SDK provides a UI widget that allows you to display the approver or requester dashboards. In order to display a dashboard of a user, you need to have the secret that you get when the task is created. This secret is a string that gives you the permission to display a dashboard of the user that the secret belongs to and approve or reject tasks of this user. Usually, this secret is used to construct the URLs that are part of an email - as hyperlinks - sent to the user and that allows the user to approve/reject a task with one single click:

Therefore, having the secret, you can display the user dashboard with:

And you can also approve/reject a task of any user (assuming you have their "approver" secret):

Service configuration.
Create new service configuration.
Auth section.
Adding service configuration key/values.
Adding clientId.
Adding clientSecret.
Logs panel on Designer UI
Log detail modal
Backoffice login page
here
When clicking on any variable, the Modifier Editor is opened, allowing you to apply one of more modifiers to the variable.

Download and install NodeJS: (version >8 is required)

First set the Appmixer API URL. This is the URL of your hosted Appmixer engine instance or your own custom URL where the self-managed engine is located. If you have a trial package or local installation, you can use .

Before you publish a component. Your user account has to have property vendor set to a string or an array of strings. The value depends on what vendor is used in component(s) you're about to publish. More about that in . In the examples in this section we use appmixer as a vendor, but you should use your own. If your company is called acme then the vendor property should be set to acme as well. You can use Backoffice for that as shown in the next picture.

Using Backoffice to set vendor property.
Custom component published
Both Flow Manager and Designer provides Share menu item.
Share dialog with a list of users the flow is shared with.
Share Dialog

Using Appmixer ACL feature you can control access to certain components. All of that can be configured from Backoffice or through the .

When you open components section you can see default ACL rules. Users with scope admin, user and tester can use all non-private components. Component can be hidden using property, then you can make these private components visible to certain users using ACL.

All appmixer components allowed to be used by all users.
Only utils, google and slack components available.

You can define ACLs to restrict access to API. The default setting is similar to the one for Components. All roles can access all actions on flows resource.

With this setting, any request to any endpoint will result in 403 response code. The following example will show you how to limit access to API for user scope to read only operations.

Read only access to /flows endpoint.

The next example is not supported in the Backoffice UI, but can be implemented directly using API. Flows may contain . You can use ACL to restrict access to those metadata. We can create a flow with custom metadata like this:

Unfortunately, since August 20, 2018 no longer accepts signups.

Got to and create a new app.

Some components (NewChannelMessageRT) use Slack Events API (). In order to use this component(s) in Appmixer, you have to register Appmixer's Slack URI in Slack first.

Go to then choose your Appmixer app.

Go to your app settings ()

In Appmixer 4.2 the Slack module was upgraded. We did remove all the legacy API and switched to their newest API. For some time, it was possible to use the Appmixer Slack module with both legacy Slack apps and the new Slack apps. But the legacy apps are deprecated and will retire in August 2021 (). If you use the legacy Slack app, you either have to create a new Slack app or migrate the existing one.

It uses API. If you want to have this module on the platform you have to create your own Screenshot account and set the API token in the Backoffice.

In order to use Google API Webhooks you have to verify your domain ownership. More about domain verification can be found in . When Appmixer is running on your servers, you can use records to verify your domain ownership, but if you want to use components that use Google API Webhooks (appmixer.google.drive.NewFile for example) in an Appmixer instance running on your localhost (typical for testing the trial or when developing new components) you need something else. When running Appmixer on localhost, you usually use tools like to create public tunnel to the Appmixer API. In order to verify ngrok domain ownership you can use the .

https://nodejs.org
http://localhost:2200
Flows
/flows
/flows
here
private
API
ACL
custom metadata
DeepAI
Slack
docker cp google52658022a92d779c.html appmixer-401_engine_1:/usr/src/appmixer/gridd/public
$ curl -XPOST https://api.appmixer.com/people-task/tasks -H 'Authorization: Bearer TOKEN' -H 'Content-Type: application/json' -d '{ "approver": "david@client.io", "decisionBy": "2020-03-01 19:00:00", "description": "Example description", "requester": "requester@example.com", "title": "My Task title" }'
{
    "approver": "david@client.io",
    "decisionBy": "2020-03-01T18:00:00.000Z",
    "description": "Example description",
    "requester": "requester@example.com",
    "title": "My Task title",
    "status": "pending",
    "approverSecret": "75a69470a129597b9de7b97829a3501f8fb8ae43e8f818fcae4191552eb70a66",
    "requesterSecret": "440197b197b9743b457172d37bcf98db3e006644657f9e19192efcc428125aae",
    "created":"2020-02-23T14:39:14.796Z",
    "id": "5e528e92095eeb0008b2aa40"
}
$ curl -XPOST https://api.appmixer.com/people-task/webhooks -H 'Authorization: Bearer TOKEN' -H 'Content-Type: application/json' -d '{ "url": "https://mywebhooks.myserver.com", "taskId": "5e528e92095eeb0008b2aa40" }'
{
    "id": "5e528e92095eeb0008b2aa40",
    "title": "My Task title",
    "description": "Example description",
    "status": "approved",
    "approver": "david@client.io",
    "requester": "requester@example.com",
    "decisionBy": "2020-03-01T18:00:00.000Z",
    "decisionMade": "2020-02-23T14:46:39.175Z",
    "created": "2020-02-23T14:45:22.669Z",
    "mtime": "2020-02-23T14:45:22.669Z"
}
$ curl -XGET https://api.appmixer.com/people-task/tasks -H 'Authorization: Bearer TOKEN'
[
  {
    "id": "5e528de61fb74b0008d524e8",
    "title": "My Task title",
    "description": "Example description",
    "status": "pending",
    "approver": "david@client.io",
    "requester": "requester@example.com",
    "decisionBy": "2020-03-01T18:00:00.000Z",
    "created": "2020-02-23T14:36:22.730Z",
    "mtime": "2020-02-23T14:36:22.732Z",
    "isApprover": true
  },
  {
    "id": "5e529002095eeb0008b2aa41",
    "title": "My Task title",
    "description": "Example description",
    "status": "approved",
    "approver": "david@client.io",
    "requester": "daviddurman@gmail.com",
    "decisionBy": "2020-03-01T18:00:00.000Z",
    "decisionMade": "2020-02-23T14:46:55.290Z",
    "created": "2020-02-23T14:45:22.669Z",
    "mtime": "2020-02-23T14:46:55.293Z",
    "isApprover": true
  }
]
$ curl -XGET "https://api.appmixer.com/people-task/tasks?secret=75a69470a129597b9de7b97829a3501f8fb8ae43e8f818fcae4191552eb70a66"
$ curl -XPUT https://api.qa.appmixer.com/people-task/tasks/5e528de61fb74b0008d524e8/approve -H 'Authorization: Bearer TOKEN'
$ curl -XPUT https://api.qa.appmixer.com/people-task/tasks/5e528de61fb74b0008d524e8/reject -H 'Authorization: Bearer TOKEN'
$ curl -XPUT https://api.qa.appmixer.com/people-task/tasks/5e528de61fb74b0008d524e8/approve -H 'Authorization: Bearer TOKEN' -H 'Content-Type: application/json' -d '{ "secret": "75a69470a129597b9de7b97829a3501f8fb8ae43e8f818fcae4191552eb70a66" }'
var peopleTasks = appmixer.ui.PeopleTasks({ el: '#your-people-tasks' });
peopleTasks.set('secret', '7crn14y8ew7a2c45b413ed7a0788175e764c4a7d11d44289bd2706e09ea4318f');
peopleTasks.open();
appmixer.api.approveTask('5e529002095eeb0008b2aa41' /*task ID*/, {
    secret: '7crn14y8ew7a2c45b413ed7a0788175e764c4a7d11d44289bd2706e09ea4318f'
});
// or 
appmixer.api.rejectTask('5e529002095eeb0008b2aa41' /*task ID*/, {
    secret: '7crn14y8ew7a2c45b413ed7a0788175e764c4a7d11d44289bd2706e09ea4318f'
});
Highrise
https://api.slack.com/apps
https://api.slack.com/events-api
https://api.slack.com/apps
https://api.slack.com/apps/{your-app-id}/event-subscriptions
https://api.slack.com/legacy/workspace-apps
https://screenshotapi.net/
here
CNAME
HTML file method
ngrok
Change user agent.
Setting the redirect URL
Activate Public Distribution
Slack Even Subscription.
Ignore bot scopes
User scopes
Setting Screenshot API token in the Backoffice.
Domain verification
Download HTML verification file
Employee vacation requests approvals
Request Approval Email
People Task Dasbhoard
Direct links as part of an email to approve/reject a task or visit a user task dashboard