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

5.0

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

Loading...

Appmixer hosted

Loading...

Loading...

Loading...

Loading...

Loading...

Appmixer Self-Managed

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

API

Loading...

Loading...

Loading...

Loading...

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

Loading...

Loading...

Appmixer Backoffice

Loading...

Loading...

End User Guide

Knowledge base

Please visit the Appmixer Knowledge base for end-user tutorials.

icon

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

{
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV..."
}

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.

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: https://en.wikipedia.org/wiki/Data_URI_scheme. 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:

Beta badge
{
    "marker": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL..."
}

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.

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" }

quota

Configuration of the quota manager 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:

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

author

The author of the component. Example:

{
    "author": "David Durman <[email protected]>"
}
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

    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ář <[email protected]>",
        "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

      Name of the component.

    • label

      Label that will be used instead of name.

    • Component icon.

    • Component badge icon giving users extra context.

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

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

    Migration from 4.5

    Breaking changes

    Node 18

    Appmixer 5.0 is running on Node 18. Connectors that are published in Appmixer will be automatically rebuilt when they are needed.

    API

    GET /auth/flow/{flowId}

    The response object structure has changed. This is an undocumented API endpoint. The new structure contains components and services properties.

    Before:

    After:

    GET /flows API

    When used with the pattern query options it searches through the flowId as well (not just name as before).

    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, ...):

    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

    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:

    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:

    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:

    state

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

    {
        "fb0dfbf0-aa5a-44c6-8c5f-bb55c73ed877": {
            "603cf987982065000842bfc9": {
                "accessTokenValid": true,
                "accountId": "603cf987982065000842bfc9",
                "tokenId": "603cf987982065000842bfca",
                "componentAssigned": false,
                ...
            }
        },
        ...
    }
    {
        "components": {
            "fb0dfbf0-aa5a-44c6-8c5f-bb55c73ed877": {
                "603cf987982065000842bfc9": {
                    "accessTokenValid": true,
                    "accountId": "603cf987982065000842bfc9",
                    "tokenId": "603cf987982065000842bfca",
                    "componentAssigned": false,
                    ...
                }
            }
        },
        "services": {
            "appmixer:google": {
                "connectAccountButton": {
                    "image": "data:image/png;base64,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"
                }
            }
        }
    }
    
    icon
    marker
    author
    description
    auth
    quota
    properties
    inPorts
    outPorts
    firePatterns
    tick
    private
    webhook
    state
    context.state

    Installation GCP

    Installation of Appmixer Self-Managed Package on Google Cloud Platform

    Follow instructions in this document: https://docs.google.com/document/d/e/2PACX-1vRNIciiW_uIQnKIlpFrhsRNqAyHmY5fueyJXAGt2ARgUPkkdwZVMnquKt33Z3nPt1jjJ3xwsltdlT1R/pub

    Google modules

    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:

    Twilio service

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

    Modules as separate apps.
    A single app type of service.

    Service Manifest File

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

    Available fields are:

    Field

    Description

    name

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

    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.

    Service manifest fields meaning.

    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:

    Field

    Description

    name

    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.

    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.

    Services, modules and components hierarchy
    {
        "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...."
    }    
    ), 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.

    {
        "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]}}}"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    Flow

    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:

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

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

    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 "[email protected]" and "abc321" with your own user's username and password):

    curl -XPOST "http://localhost:2200/user/auth" -d '{"username": "[email protected]", "password": "abc321"}' -H 'Content-Type: application/json'

    You should see a response that looks like this:

    {"user":{"id":"5c88c7cc04a917256c726c3d","username":"[email protected]","isActive":false,"email":"[email protected]","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 API section to learn more about all the endpoints that you use.

    description

    Description of your app.

    icon

    App icon in the Data URI format.

    description

    Description of your app.

    icon

    App icon in the Data URI format.

    Enabling Users to Publish Custom Components
    Enabling Users to Publish Custom Components

    Getting Started

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

    Sign-in page

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

    Sign-up page

    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):

    FlowManager page

    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:

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

    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:

    Configuration

    Appmixer Engine

    APP_NAME

    The authentication popup windows do contain the Appmixer title by default. If you want to change that, set the APP_NAME env variable in the Appmixer engine.

    Example from the docker-compose.yml

    LOG_LEVEL

    By default set to info. Can be changed to error, warn or debug.

    LOG_COMPONENT_DATA_MESSAGE

    When set to false, the component's input/output messages won't be logged into Elasticsearch.

    Important! Appmixer Insights and Designer log messages won't contain any items if logging data messages are turned off.

    APPMIXER_HTTPS_PROXY and APPMIXER_HTTP_PROXY

    Setup proxy using these ENV variables. All HTTP(S) requests from Appmixer will be redirected to the proxy URL.

    Custom Auth Popups

    You can fully customize the authentication popups. There are new variables that you can set in the Backoffice:

    • AUTH_SERVICE_HTML

    • AUTH_POPUP_SUCCESS_HTML

    • AUTH_POPUP_FAIL_HTML

    • AUTH_PRE_HTML

    The AUTH_SERVICE_HTML is the popup for API Key and Password authentication., The AUTH_POPUP_SUCCESS_HTML is displayed when the authentication is successful, the AUTH_POPUP_FAIL_HTML is displayed if the authentication fails. And the AUTH_PRE_HTML is used for connectors that have the pre field defined in the auth.js file (to gather additional information before the authentication).

    To modify the popup, go to the System section in the Backoffice and add a new Key. Let's say we want to modify the popup for Api Key connectors.

    At the bottom of this page, you will find the default HTML files. You can use them and modify them, or you can create your own.

    We are going to change the text to:

    Then you paste the whole HTML into the Backoffice.

    And this is the result:

    Default HTML files

    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:

    # Download the Appmixer SDK:
    $ wget http://my.[acme].appmixer.cloud/appmixer/appmixer.js
    # Download the demo page:
    $ wget http://my.[acme].appmixer.cloud/appmixer/demo.html
    # Download the example theme object:
    $ wget http://my.[acme].appmixer.cloud/appmixer/theme.js
    $ open demo.html

    Notice that this won't work yet since we haven't configured the basic required variables. First, we need to edit the demo HTML file to add the base URL of our Appmixer engine REST API ( and our user credentials. Open the 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>';

    Replace <your-base-url> with https://api.[acme].appmixer.cloud and <your-username> and <your-password> with the user credentials. You can use the credentials that you received after your hosted environment was created. Or you can create a new user at http://my.[acme].appmixer.cloud. Now you can open the demo.html file in your browser. You should see something like this:

    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:

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

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

    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:

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

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

    Introduction

    Appmixer SKD is a toolkit to integrate workflow automation engine and embed white-labeled user interface widgets into your products. Gain a whole new set of comprehensive features with ease.

    Creating Custom Components

    In order to successfully follow the next tutorials, you need few URLs that you should have already received.

    Appmixer Backend API URL. Looks like https://api.[acme].appmixer.cloud

    Appmixer Backoffice URL. Looks like https://backoffice.[acme].appmixer.cloud

    Appmixer Frontend URL. Looks like https://my.[acme].appmixer.cloud

    In order to publish your custom components into the Appmixer, you need an account with a certain permission. You need your admin account, visit the and set it up. More information can be found .

    Customers tend to write their own components with the appmixer prefix. Something like appmixer.[acme].crm.CreateCustomer. This is not recommended. The first part of the module/component ID should not be appmixer, but your own vendor ID. You can use your company name, or the name of the product you're trying to build with Appmixer. Something like [acme].crm.customers.CreateCustomer could be the ID of your custom component.

    There are a couple of tutorials you can follow in order to learn how to create and publish your own components.

    First, you're gonna need to understand what a component is and how does a module with components look like. .

    Note that the HelloAppmixer tutorial, mentioned in the next paragraph, is written for the Appmixer running on a local machine. You can see a http://localhost:2200 URL mentioned a couple of times there. In the hosted version of Appmixer, you have your own Appmixer Backend API URL which you will use instead of the localhost:2200.

    Then you can write your own component. Before doing so, it is a good time to learn something about our . You will use that tool to create packages of your components and publish them into your hosted Appmixer. is a simple HelloAppmixer tutorial on how to build your first module with a component.

    Another can be found in our Knowledge base.

    Installation

    Appmixer SDK package includes two types of modules: basic UMD and advanced ESM.

    Basic Usage

    Load appmixer.js UMD module in your HTML file:

    <script src="https://my.YOURTENANT.appmixer.cloud/appmixer/appmixer.js"></script>
    
    <script type="module">
    const appmixer = new Appmixer(/* ... */)
    </script>

    Advanced Usage

    Download appmixer.es.js ES module and include the files in your project:

    Choose Appmixer UI widgets to include:

    Insights Chart Editor

    Create charts to visualize logs of messages that passed through flows.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    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.

    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.

    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.

    wget https://my.YOURTENANT.appmixer.cloud/appmixer/package/appmixer.es.js
    wget https://my.YOURTENANT.appmixer.cloud/appmixer/package/appmixer.css
    import { Appmixer } from './appmixer.es.js'
    import './appmixer.css'
    
    const appmixer = new Appmixer(/* ... */)
    import { Designer, FlowManager } from './appmixer.es.js'
    
    appmixer.ui('Designer', Designer)
    appmixer.ui('FlowManager', FlowManager)
    
    const designer = appmixer.ui.Designer(/* ... */)
    const flowManager = appmixer.ui.FlowManager(/* ... */)
    Backoffice
    here
    This is a good place to start
    CLI
    Here
    tutorial
    2KB
    auth-service.html.zip
    archive
    Default auth-service.html
    1KB
    login-success.html.zip
    archive
    1KB
    login-fail.html.zip
    archive
    3KB
    auth-pre.html.zip
    archive
    Default auth-pre.html
    Changing the authentication popup.

    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:

    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 });

    The list of API methods can be found here.

    An example how to redefine the flow update request.

    /* Create "Designer". */
    var designer = appmixer.ui.Designer({
        el: '#your-designer',
        options: designerOptions(),
        api: {
            // extending the updateFlow request
            updateFlow(flowId, update) {
                // at this place you can call your own API every time the flow 
                // gets updated
                // carefully catch errors, timeouts ... so calling your 
                // external API does not affect the Designer behaviour
                console.log('Calling your own API.');
                console.log(JSON.parse(JSON.stringify(update)));
    
                // in order to update the flow in Appmixer, call the Flow API
                return this._request({
                    url: `${this.get('baseUrl')}/flows/${flowId}`,
                    method: 'PUT',
                    data: update
                });
            }
        }
    });
    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    close

    Close the editor.

    Example

    const insightsChartEditor = appmixer.ui.InsightsChartEditor(config)
    
    insightsChartEditor.set(key, value)
    insightsChartEditor.get(key)
    Insights Chart Editor
    insightsChartEditor.state(name, value)
    insightsChartEditor.on(event, handler)
    insightsChartEditor.on('close', () => {/* ... */})
    const insightsChartEditor = appmixer.ui.InsightsChartEditor({
        el: '#insights-chart-editor'
    })
    
    insightsChartEditor.open()

    The OAuth variables can be set through the Backoffice.

    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:

    It will set both the APPMIXER_API_URL and the GRIDD_URL to the https://247bb870a6f4.ngrok.io.

    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:

    here
    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
        ...
      engine:
        ...
        environment:
          - APP_NAME=ACME
        ...
    appmixer.api.authenticateUser(USERNAME, PASSWORD).then((auth) => {
        appmixer.set('accessToken', auth.token);
        start();
    });
    <div id="your-flow-manager"></div>
    var flowManager = appmixer.ui.FlowManager({ el: '#your-flow-manager' });
    flowManager.open();
    flowManager.on('flow:open', (flowId) => {
        designer.set('flowId', flowId);
        flowManager.close();
        designer.open();
    });​
    appmixer.set('accessToken', null);
    Appmixer SDK
    Appmixer SDK Demo
    Insights page in Appmixer SDK Demo
    http://openweathermap.org
    Inspector panel
    Inspector panel
    Running flow

    Variables

    GET flow 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

    Query Parameters

    Name
    Type
    Description

    Public Files

    Returns a list of the public files

    GET /public-files

    The list returned does not contain the contents of the files.

    Upload a public file

    POST /public-files

    Request Body

    Name
    Type
    Description

    Removes a public file

    DELETE /public-files/:filename

    Path Parameters

    Name
    Type
    Description

    Getting Started

    Appmixer Backoffice is an administration UI for Appmixer. You need an admin user account in order to log into Backoffice. More about that in here.

    Appmixer Backoffice runs on port 8081 by default. After you install the Appmixer package you can open http://localhost:8081.

    Backoffice login page

    And sign in with your Appmixer user account.

    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 an outPorts definition can look like this:

    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:

    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:

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

    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:

    Replace

    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": "[email protected]", "password": "abc321" }'

    Services

    Open Appmixer Backoffice and click the Services item in the main menu.

    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 stored in Appmixer.

    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.

    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.

    componentId

    String

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

    flowId*

    String

    srcComponentOut

    String

    Name of the output port of the source component.

    srcComponentId

    String

    ID of the source (connected) component ID.

    tgtComponentId

    String

    ID of the target component ID.

    tgtComponentIn

    String

    Name of the input port of the target component.

    {
        // Response
    }

    filename*

    String

    The name for the file

    file*

    File

    The file to be uploaded

    filename*

    String

    The name of the file you want to remove

    { "ok": true }
    [
      {
        "filename": "test.txt"
      }
    ]
    { "ok": true }
    JSON Schema

    We support full schema definition for each option, so you can specify the structure of the data that is coming out from your component. You can add a schema property to each option, which contains a JSON Schema definition. For example:

    As you can see, compared to the first example, we replaced the last 3 options with a single one, which is actually an array of items with three properties. Each of these items has title which determines the label that will be visible in the UI. Note that the type of these inner properties could be an object or array, and have their own nested schemas.

    If the option is defined as an array and you want to work with that array using modifiers:

    You will see the item properties among other variables.

    And if you use the Each connector, you will see the item properties in the Variables picker.

    Alternatively, you can define a schema at the top level instead of using the options property. For example:

    When you define the structure of the data coming out from your component, the users of your component will have an easier time working with it, as they will be able to do things like selecting nested properties directly, selecting properties on iteration modifiers, and getting properties paths in modifiers. You can find more details about this in this section.

    outPort.source

    The definition is similar to the source source of properties. When used for the output port definition, it allows defining the output port schema dynamically.

    There is one difference though. When defined in the output port, the source definition can reference both component properties and input fields, while the properties source definition can only hold references to other properties' values.

    An example is a Google Spreadsheet component UpdatedRow. The output port options of this component consist of the column names in the spreadsheet. But that is specific to the selected Spreadsheet/Worksheet combination. Therefore it has to be defined dynamically.

    Dynamic output port options.

    Here is an example of the UpdatedRow output port definition.

    outPort.maxConnections

    Set the maximum number of outgoing links that can exist from the output port. The 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.

    {
        "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" }
                ]
            }
        ]
    }
    Variables Picker
    {
        "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 data", 
                        "value": "weather", 
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "description": { "type": "string", "title": "Weather description" },
                                    "icon": { "type": "string", "title": "Weather icon code" },
                                    "iconUrl": { "type": "string", "title": "Weather icon URL" }
                                }    
                            }
                        }
                    }
                ]
            }
        ]
    }
    {
        "outPorts": [
            {
                "name": "weather",
                "schema": {
                    "type": "object",
                    "properties": {
                        { "title": "Temperature", "value": "main.temp" },
                        { "title": "Pressure", "value": "main.pressure" },
                        { "title": "Humidity", "value": "main.humidity" },
                        { "title": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
                        { "title": "Sunset time (unix, UTC)", "value": "sys.sunset" },
                        { "title": "City name", "value": "name" },
                        { 
                            "label": "Weather data", 
                            "value": "weather", 
                            "schema": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "description": { "type": "string", "title": "Weather description" },
                                        "icon": { "type": "string", "title": "Weather icon code" },
                                        "iconUrl": { "type": "string", "title": "Weather icon URL" }
                                    }
                                }                            
                            }
                        }
                    }
                }
            }
        ]
    }
    {
        "outPorts": [
            {
                "name": "out",
                "source": {
                    // We will call another component to construct the output port
                    // options, in this case, the GetRows component
                    "url": "/component/appmixer/google/spreadsheets/GetRows?outPort=out",
                    // Every Appmixer component can have 'properties' and input ports,
                    // the 'data' sections is used to create the input data object 
                    // for the component
                    "data": {
                        // in this particular case, the GetRows component has an
                        // optional property called 'generateOutputPortOptions', we
                        // will pass that property with the value 'true'. The GetRows
                        // component will use this property to change its return value
                        // and instead of returning rows from Worksheet, it will
                        // return the 'options' array.
                        "properties": {
                            "generateOutputPortOptions": true
                        },
                        // the GetRows component expects the Spreadsheet ID and
                        // Worksheet ID as part of the message at its input port
                        // called 'in'. The UpdatedRow component is a trigger, it
                        // does not have an input port, but it has the same options like
                        // 'allAtOnce', 'withHeaders', ... and since it does not have
                        // an input port, it has these options defined in the
                        // 'properties' section. The next 'messages' section is used
                        // to construct an input port object for the GetRows component.
                        // It copies the user defined properties from the UpdatedRow.
                        // Appmixer will replace these with the actual values before
                        // calling the GetRows component.
                        "messages": {
                            "in/sheetId": "properties/sheetId",
                            "in/worksheetId": "properties/worksheetId",
                            "in/allAtOnce": "properties/allAtOnce",
                            "in/withHeaders": "properties/withHeaders",
                            "in/rowFormat": "properties/rowFormat"
                        }
                    }
                }
            }
        ]
    }
    
    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 198 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.

    var appmixer = new Appmixer({ baseUrl: BASE_URL });
    appmixer.set('strings', STRINGS);
    appmixer.set('strings', {
        ui: {
            flowManager: {
                search: 'Search flows',
                header: {
                    buttonCreateFlow: 'Create new Flow'
                }
            }
        }
    });
    wget https://my.appmixer.com/appmixer/package/strings-en.json
    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', {
        ui: {
            flowManager: {
                pagination: '{{range}} of {{total}} flow|{{range}} of {{total}} flows'
            }
        }
    });
    <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

    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:

    Insights page in Appmixer SDK Demo

    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:

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

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

    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:

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

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

    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:

    $ cd appmixer/frontend/appmixer/
    $ open demo.html
    # 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
        <script>
            var BASE_URL = '<your-base-url>';
            var USERNAME = '<your-username>';
            var PASSWORD = '<your-password>';
    appmixer.api.authenticateUser(USERNAME, PASSWORD).then((auth) => {
        appmixer.set('accessToken', auth.token);
        start();
    });
    <div id="your-flow-manager"></div>
    var flowManager = appmixer.ui.FlowManager({ el: '#your-flow-manager' });
    flowManager.open();
    flowManager.on('flow:open', (flowId) => {
        designer.set('flowId', flowId);
        flowManager.close();                        
        designer.open();
    });
    
    appmixer.set('accessToken', null);
    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

    Request Body

    Name
    Type
    Description

    password*

    string

    Password.

    username*

    string

    Username, has to have an email format.

    {
        "user": {
            "id": "5c88c7cc04a917256c726c3d",
            "username":"[email protected]",
            "isActive": false,
            "email": "[email protected]", 
    

    Create User

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

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

    Request Body

    Name
    Type
    Description

    password*

    string

    Password.

    email

    string

    Email address.

    username

    string

    Email address.

    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }

    Get User Information

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

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

    {
      "id": "58593f07c3ee4f239dc69ff7",
      "username": "[email protected]",
      "isActive": true,
      "email": "[email protected]",
      "scope": [
        "user"
      ],
      "plan": "beta"
    }
    curl "https://api.appmixer.com/apps" -H "Authorization: Bearer [ACCESS_TOKEN]"

    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.

    Adding service configuration key/values.

    Then add clientId (has to be clientId, not clientID or any other combination) key with your client Id.

    Adding clientId.

    And then the clientSecret.

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

    You can use this configuration even for components that do not require user authentication into 3rd party applications. DeepAI 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 here.

    secrets
    Service configuration.
    Create new service configuration.
    Auth section.
    Developer mode tools

    Currently developer mode expose a show/hide logs button on the Designer UI:

    Logs panel on 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:

    Log detail modal
    // Create new instance with developer mode enabled
    var appmixer = new Appmixer({
        devMode: true
    })
    // Enable developer mode at runtime
    appmixer.set('devMode', true)

    Appmixer Architecture

    High-Level Architecture

    Appmixer High-Level Architecture

    Appmixer System Components/Applications

    Appmixer Engine

    The main system that manages flows, accounts, users, 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

    JavaScript HTML 5 SDK that allows to seamlessly embed any of the Appmixer UI widgets (including the drag&drop workflow designer) to any 3rd party web page. The SDK communicates with the engine via REST API. Appmixer JavaScript SDK offers heavy customization capabilities, including changing of the look&feel (via themes), localization of all the text in the UI and even implementing custom component configuration Inspector fields.

    Appmixer Backoffice

    A standalone admin panel providing overview of all the flows and users in Appmixer together with system and modules configuration, ACL lists, system webhooks and more.

    Appmixer CLI

    Command line tool for development, testing and management of custom components. It can also be used to manage, export and import flows.

    Supporting Technologies

    RabbitMQ

    Appmixer Engine uses RabbitMQ as a message broker for all data messages passing through the running flows. Also, RabbitMQ serves as an intermediary for logs before entering ElasticSearch.

    MongoDB

    Storage for all static data such as:

    • Flows

    • Users

    • System Configuration

    • Accounts

    Redis

    Key-value store that holds the state of the cluster, provides caching and synchronization (distributed locks).

    ElasticSearch

    Search & analytics engine that stores all the activity of all the running flows, system logs & errors. Logs can be retrieved by using the Appmixer REST API (Insights). Alternatively, standard tools such as Kibana can be used for in-depth analysis, technical support and maintenance.

    Flow Design Phase Architecture

    Clients (Appmixer Studio, Appmixer JavaScript SDK) provide a drag&drop No-code designer for workflow automations. In design phase, the user is adding components to a flow, configures components, assigns accounts and interconnects components to form a data flow pipeline, possibly with logic and loops.When configuring components, the user can use variables to reference data that will be available at the flow runtime. These variables are outputs of components back in the chain (collection of all the outputs of all the ancestors of the configuring component).Variables can be modified at the flow runtime by applying modifiers. These modifiers are configurable and are represented as parametrized JavaScript functions that manipulate a single input. Modifiers can be chained to produce more complex expressions.The result of the design phase is a new or updated flow descriptor (JSON representation of the flow graph) and metadata, together with account assignment (externally linked) and modifiers (externally linked). Note that the flow descriptor in itself is not complete, the engine assumes the linked components and modifiers exist in the system. Therefore, copying a flow descriptor to another tenant can only work if the linked components and modifiers also exist in the target tenant.

    Flow Running Phase Architecture

    Flow is in the running phase when its configuration has been completed and the flow has been started. From this point, the flow does not require any user interaction and is executed and maintained by the engine in the background. During the flow running phase, the engine is responsible for passing external inputs to the associated trigger components (e.g. webhooks), scheduling polling type of triggers, orchestrating message passing between connected components, handling errors, retries, making sure defined quota limits are honored and logging all activity. The engine reports all unexpected events via system defined webhooks. This allows the customer to define custom actions in case e.g. the flow can’t run since a component in it lost authentication tokens (e.g. a 3rd party connected account was externally revoked). The engine has a built-in support for the most common authentication mechanisms (OAuth 1, OAuth 2, API keys, Basic auth, …) and can be extended with new ones. It automatically handles token refreshing for relevant authentication types. Components within a flow (and in general, when defined) don’t know about each other. They are black boxes that can be arbitrarily interconnected to produce different automations.If a message cannot be processed (e.g. a component tried to call an API which keeps failing multiple times - configurable) the engine stores such a message in a dead-letter collection in MongoDB and notifies a system configured webhook. This allows the customer to further inspect such messages, retry their processing and/or notify the end-user of such errors. The engine provides built-in scheduling mechanism, offering component developers an intuitive way for defining different types of timers, schedulers and throttles.

    UI & Widgets

    Appmixer UI is a tool for building user interfaces with component-based widgets.

    Configuration

    Widgets are included in appmixer.ui instances made with Appmixer constructor:

    const appmixer = new Appmixer(/* ... */)
    const widget = appmixer.ui.FlowManager(config)

    config.el

    Type: String|Element | Default: null

    HTML DOM element to serve as a container of the widget.

    config.theme

    Type: Object | Default: DefaultTheme

    Custom theme definition.

    config.l10n

    Type: Object | Default: DefaultL10N

    Custom localization texts.

    config.lang

    Type: String | Default: en

    Language code for localization of components.

    config.api

    Type: Object | Default: DefaultAPI

    Custom API methods.

    Instance

    widget.open

    Mount the widget instance and render it inside the el container.

    widget.close

    Unmount the widget instance and hide the el container.

    widget.reload

    Reload the entire widget.

    widget.reset

    Reset the state of the widget to defaults.

    widget.state

    Use state for properties that may change at any time when the widget is active.

    Example

    widget.set

    Set config property.

    widget.get

    Get config property.

    widget.on

    Add a new event listener and disable the default handler of the event.

    widget.off

    Remove an event listener and enable the default handler of the event.

    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

    Quick Start

    Let's start with a simple user interface to browse and manage your flows.

    Create the html demo below and follow the steps ahead to learn the essentials.

    1. Get Appmixer constructor from appmixer.js file and create a new instance:

    2. Set baseUrl

    "plan":"free"
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    widget.open()
    widget.close()
    widget.reload()
    widget.reset()
    widget.state(path, value) // setter
    widget.state(path) // getter
    {
      "foo": false,
      "bar": { "counter": 1 }
    }
    // set properties by key or path
    widget.state('foo', true)
    widget.state('bar', { counter: 2 })
    widget.state('bar/counter', 3)
    
    // get properties by key or path
    widget.state('foo') // true
    widget.state('bar') // { counter: 3 }
    widget.state('bar/counter') // 3
    
    // get the entire state
    widget.state() // { foo: true, bar: { counter: 3 } }
    
    // reset the state to defaults
    widget.reset() // { foo: false, bar: { counter: 1 } }
    widget.set(key, value)
    widget.get(key, value)
    widget.on(name, handler)
    widget.off(name)
    to connect the API module to the REST API of your engine:

    3. Create a new user and set accessToken to authorize your appmixer instance:

    Change USERNAME(e.g. [email protected]) and PASSWORD(e.g. 12345678) parameters to valid credentials for registration of a new user. Or use an authentication method if the user already exists:

    Thetoken is used by Appmixer to identify the user. Store the token and credentials in a database of your product. This way, you can always associate your users with the (virtual) ones created for Appmixer.

    4. Create new instances of Flow Manager and Designer widgets:

    5. Open Flow Manager to browse the flows and switch to Designer when a flow is selected:

    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <script src="https://my.YOURTENANT.appmixer.cloud/appmixer/appmixer.js"></script>
    </head>
    <body>
      <div id="flow-manager"></div>
      <div id="designer"></div>
    
      <script async type="module">
        const appmixer = new Appmixer()
    
        appmixer.set('baseUrl', BASE_URL)
    
        try {
            const auth = await appmixer.api.signupUser(USERNAME, PASSWORD)
            appmixer.set('accessToken', auth.token)
        } catch (error) {
            alert(error)
        }
    
        const flowManager = appmixer.ui.FlowManager({ el: '#flow-manager' })
        const designer = appmixer.ui.Designer({ el: '#designer' })
    
        flowManager.on('flow:open', flowId => {
            flowManager.close()
            designer.set('flowId', flowId)
            designer.open()
        })
    
        flowManager.open()
      </script>
    </body>
    </html>
      <script src="https://my.YOURTENANT.appmixer.cloud/appmixer/appmixer.js"></script>
    const appmixer = new Appmixer()
    Flow Manager
    Designer
    appmixer.set('baseUrl', BASE_URL)
    const auth = await appmixer.api.signupUser(USERNAME, PASSWORD)
    appmixer.set('accessToken', auth.token)
    const auth = await appmixer.api.authenticateUser(USERNAME, PASSWORD)
    <div id="flow-manager"></div>
    <div id="designer"></div>
    const flowManager = appmixer.ui.FlowManager({ el: '#flow-manager' })
    const designer = appmixer.ui.Designer({ el: '#designer' })
    flowManager.open()
    
    flowManager.on('flow:open', flowId => {
        flowManager.close()
        designer.set('flowId', flowId)
        designer.open()
    })

    Component States

  • Dead-letter collection

  • Component Behaviour (code)

  • Modifiers (code)

  • Files (GridFS)

  • Data Stores

  • Telemetry

  • Appmixer JavaScript SDK
    Appmixer Backoffice
    Appmixer Command Line Interface
    Flow Design Phase Architecture
    Flow Running Phase Architecture
    Docker Compose
  • NodeJS v18.15.0 (only for Appmixer CLI)

  • Installation

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

    Install and Start Appmixer

    Stop Appmixer

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

    Stop and Clean Up

    Stop Appmixer and remove all containers and images:

    Using Webhook Components

    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 ngrok utility:

    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:

    with the URL from ngrok:

    Now restart Appmixer:

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

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

    The information about automatic setting the GRIDD_URL is valid only when our docker-componse.yml file is used. When you run Appmixer without it, the GRIDD_URL has to be set. This variable affects the OAuth redirect URL.

    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:

    Replace the "[email protected]" 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:

    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 http://localhost:8081 using the email address and password of your admin user. If you visit the Users page, you should see something like this:

    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 "[email protected]" user will be able to publish new custom components using the appmixer CLI tool.

    Docker
    docker-compose --project-name appmixer up
    docker-compose --project-name appmixer stop
    docker-compose --project-name appmixer down --volumes --remove-orphans --rmi all
    $ 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
      engine:
        ...
        environment:
          - APPMIXER_API_URL=${APPMIXER_API_URL:-http://localhost:2200}
        ...
    engine:
        ...
        environment:
          - APPMIXER_API_URL=https://568284c4.ngrok.io
        ...
    docker-compose --project-name appmixer down # or kill existing with Ctrl-c
    docker-compose --project-name appmixer up
    APPMIXER_API_URL=https://568284c4.ngrok.io docker-compose --project-name appmixer up
    docker-compose -p appmixer exec mongodb mongo appmixer --quiet --eval 'db.users.update({"email": "[email protected]"}, { $set: {"scope": ["user", "admin"]}})'
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

    Custom Inspector Fields

    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.

    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.

    Defining Custom Inspector Fields

    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 Vue JS component. 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:

    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:

    • Template and Render function: https://vuejs.org/v2/api/#template, https://vuejs.org/v2/api/#render

    • Data objects: , , ,

    • Livecycle hooks:

    • 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):

    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):

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

    Properties section of the Component Manifest
    Custom Inspector Field for Polygon Selection in Google Maps
    Custom Inspector Field with Rich Text Editor
    Custom Inspector Field with Price Calculator
    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>
        `
    });
    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);
            },
        },
    });
    
    .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;
    }
    {
        "categories": {
            "object": {
                "label": "Object",
                "index": 1
            },
            "list": {
                "label": "List",
                "index": 2
            },
            ...
        },
        "modifiers": {
            "g_stringify": {
                "name": "stringify",
                "label": "Stringify",
                "category": [
                    "object",
                    "list"
                ],
    

    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.

    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\""
    }

    Designer

    Build, edit and inspect individual flows in a comprehensive editor.

    Designer

    Configuration

    Set up a new instance with config parameters and set/get methods:

    const designer = appmixer.ui.Designer(config)
    
    designer.set(key, value)
    designer.get(key)

    config.el ...

    Learn about widget config .

    config.flowId

    Type: String | Default: null

    ID of a flow that is opened in the editor.

    config.componentId

    Type: String | Default: null

    ID of a component that is opened in the editor.

    config.shareTypes

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    config.sharePermissions

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    config.options.showHeader

    Type: Boolean | Default: true

    Toggle visibility of the header.

    config.options.menu

    Type: Object[] | Default: []

    Add a dropdown menu input to trigger built-in and custom events:

    The optional icon property is a URL of an image or a base64 string.

    config.options.toolbar

    Type: Array[] | Default: []

    Add a toolbar with groups of built-in and custom buttons:

    Specify Vue under widget to create a custom toolbar button.

    Instance

    Learn about widget instance .

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    flow:start

    Toggle stage button to start the flow.

    flow:stop

    Toggle stage button to stop the flow.

    flow:share

    Click menu item to open sharing of the flow.

    flow:rename

    Click menu item to rename the flow.

    flow:export-svg

    Click menu item to export diagram of the flow to SVG.

    flow:export-png

    Click menu item to export diagram of the flow to PNG.

    flow:print

    Click menu item to print diagram of the flow.

    flow:wizard-builder

    Click menu item to open a wizard builder dialog.

    component:add

    Add a new component to the flow.

    component:open

    Open component inspector.

    component:close

    Close component inspector.

    component:rename

    Rename a component.

    component:update-type

    Use selection input to change component type.

    navigate:validation

    Click a button to show validation errors.

    Example

    "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 }"
    },
    ...
    }
    }
    {
        "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            }"
            },
            ...
        }
        
    }
    appmixer.ui.Designer({
      /* ... */
      options: {
          menu: [
            { event: 'flow:rename', label: 'Rename', icon: 'data:image/svg+xml;base64,...' },
            { event: 'flow:share', label: 'Share', icon: 'https://www.example.com/images/image.jpg' },
            { event: 'flow:wizard-builder', label: 'Wizard' },
            { event: 'flow:export-svg', label: 'Export SVG' },
            { event: 'flow:export-png', label: 'Export PNG' },
            { event: 'flow:print', label: 'Print' }
        ]
      }
    }
    const designer = appmixer.ui.Designer({
      /* ... */
      options: {
          toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['logs'],
            [{
              tooltip: 'Reload',
              widget: {
                  template: (
                      `<div @click="onClick" style="border: solid 1px gray; border-radius: 3px;">
                          <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px">
                            <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
                          </svg>
                      </div>`
                  ),
                  methods: {
                      onClick() {
                        designer.reload()
                      }
                  }
              }
            }]
        ]
      }
    }
    designer.state(name, value)
    designer.on(event, handler)
    designer.on('flow:start', flow => {/* ... */})
    designer.on('flow:stop', flow => {/* ... */})
    designer.on('flow:share', flow => {/* ... */})
    designer.on('flow:rename', flow => {/* ... */})
    designer.on('flow:export-svg', flow => {/* ... */})
    designer.on('flow:export-png', flow => {/* ... */})
    designer.on('flow:print', flow => {/* ... */})
    designer.on('flow:wizard-builder', flow => {/* ... */})
    designer.on('component:add', ({ data, next }) => {/* ... */})
    designer.on('component:open', ({ data, next }) => {/* ... */})
    designer.on('component:close', ({ data, next }) => {/* ... */})
    designer.on('component:rename', ({ data, next }) => {/* ... */})
    designer.on('component:update-type', ({ data, next }) => {/* ... */})
    designer.on('navigate:validation', (flowId) => {/* ... */})
    const designer = appmixer.ui.Designer({
        el: '#designer',
        options: {
            menu: [
              { event: 'flow:rename', label: 'Rename' },
              { event: 'flow:share', label: 'Share' },
              { event: 'flow:wizard-builder', label: 'Wizard' },
              { event: 'flow:export-svg', label: 'Export SVG' },
              { event: 'flow:export-png', label: 'Export PNG' },
              { event: 'flow:print', label: 'Print' }
            ],
            toolbar: [
              ['undo', 'redo'],
              ['zoom-to-fit', 'zoom-in', 'zoom-out'],
              ['logs']
            ]
        }
    })
    
    const flowId = await appmixer.api.createFlow('New flow')
    designer.set('flowId', flowId)
    designer.open()
    here
    ComponentOptions
    here
    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

    Custom Theme

    Customize UI widgets. You can change the colors, the typography, and much more.

    Basic usage

    To customize the widgets, you need to specify a theme JSON object. However, this is optional; the UI comes with a default light theme. Create a new Appmixer instance with the theme option:

    Or/and use the option with individual widgets:

    Flow Manager

    Browse and manipulate flows that are accessible to the current user.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    Usage with multiple themes

    If you wish to switch between themes, use the set("theme") method, this will re-render the UI:

    Variables

    Change the overall styling with a few global CSS properties. Here is a complete example with defaults:

    Colors

    The numbers in the names of colors refer to a foreground opacity of the color over the base background color:

    • neutral96 is a foreground color with 96% opacity over the background neutral00.

    • Some colors need a negative color NG on top. For example, a white text on a blue button.

    Font

    The numbers in size of the font refer to the defaults in pixels: size13 variable default is 13px.

    Shapes

    Shapes of connectors in diagrams are customizable by choosing a preset in your theme.

    Change the values of the entries to switch between presets. Here are built-ins per shape type:

    action

    action-vertical

    action-dark

    action-dark-vertical

    trigger

    trigger-vertical

    trigger-dark

    trigger-dark-vertical

    selection

    selection-vertical

    selection-dark

    selection-dark-vertical

    Use Custom Shapes to create new presets or override the defaults.

    Charts

    Charts are customizable by a unique set of non-CSS properties. The values default to the current theme variables, except for colorway. The colorway option specifies the dynamic colors automatically picked by charts.

    Selectors

    The theme JSON object references the entire Appmixer SDK UI in a complex tree of selectors. Elements use a hash symbol (#) prefix and dynamic states use the at sign (@). Each branch in the tree may hold nested selectors and any valid CSS properties for the element. The selectors are available for advanced customizations, but the structure may change between the Appmixer versions.

    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:

    FlowManager Dark Theme
    Designer Dark Theme
    Insights Logs Dark Theme
    Insights Chart Editor
    const appmixer = new Appmixer({
        theme: {
            variables: {
                font: {
                    family: “sans-serif”
                },
                colors: {
                    neutral00: “orange”
                }
            }
        }
    });
    const flowManager = appmixer.ui.FlowManager({
        el: "#my-flow-manager",
        theme: {
            variables: {
                colors: {
                    neutral00: 'purple'
                }
            }
        }
    });
    // change the theme of all widgets
    appmixer.set('theme', {
        variables: {
            font: {
                family: 'serif'
            },
        }
    })
    
    // or/and change the theme of a single widget
    widget.set('theme', {
        variables: {
            colors: {
                neutral00: 'green'
            }
        }
    });
    appmixer.set('theme', {
        variables: {
            font: {
                family: '\'SF Pro Text\', \'Helvetica Neue\', \'Helvetica\', \'Arial\', sans-serif',
                familyMono: '\'SF Mono\', \'ui-monospace\', Menlo, monospace',
                weightRegular: 400,
                weightMedium: 500,
                weightSemibold: 600,
                size: 14
            },
            colors: {
                base: '#FFFFFF',
                neutral: '#131314',
                focus: '#3688EB',
                error: '#DE3123',
                warning: '#B56C09',
                success: '#09CD96',
                modifier: '#C558CF',
                highlighter: '#FFA500'
            },
            shadows: {
                backdrop: 'rgba(0 0 0 / 6%)',
                popover: '0 3px 9px rgba(0 0 0 / 12%)',
                icon: '0 1px 3px rgb(0 0 0 / 6%)'
            },
            corners: {
                radiusSmall: '3px',
                radiusMedium: '6px',
                radiusLarge: '9px'
            },
            dividers: {
                regular: '1px',
                medium: '2px',
                semibold: '3px',
                bold: '6px',
                extrabold: '9px'
            }
        }
    })
    appmixer.set('theme', {
        ui: {
            shapes: {
                action: "action",
                trigger: "trigger",
                selection: "selection"
            }
        }
    })
    appmixer.set('theme', {
        ui: {
            charts: {
                legendFontSize: 12px,
                legendFontFamily: "sans-serif",
                legendFontColor: "black",
                tickFontSize: "black",
                tickFontFamily: "monospaced",
                tickFontColor: "black",
                gridColor: "lightgray",
                colorway: [
                    '#1452cc',
                    '#8a47c4',
                    '#c636b0',
                    '#ef2c94',
                    '#ff3d74',
                    '#ff5e52',
                    '#ff8230',
                    '#ffa600'
                ]
            }
        }
    })
    appmixer.ui.FlowManager({
        el: '#app',
        theme: {
            ui: {
                '#FlowManager': {
                    background: 'lightblue',
                    '#header': {
                        padding: '0 0 24px 0',
                        '#buttonCreateFlow': {
                            color: 'yellow',
                            '@hovered': {
                                color: 'white'
                            }
                        }
                    }
                }
            }
        }
    });
    wget  https://my.appmixer.com/appmixer/package/theme-light.json
    wget  https://my.appmixer.com/appmixer/package/theme-dark.json
    config.el ...

    Learn about widget config here.

    config.options

    Type: Object | Default: DefaultOptions

    config.options.menu

    Type: Object[] | Default: []

    Add a dropdown menu input to each flows to trigger built-in and custom events:

    The optional icon property is a URL of an image or a base64 string.

    Flow Manager Menu

    config.options.shareTypes

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    config.options.sharePermissions

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    config.options.filters

    Type: Object[] | Default: []

    Create dropdown inputs with built-in query filters:

    Flow Manager Filters

    config.options.customFilter

    Type: Object | Default: {}

    Filter the flows with additional parameters:

    This is especially useful in connection with customFields metadata to display multiple different Flow Managers each listing a different category of flows:

    config.options.sorting

    Type: Object[] | Default: []

    Create dropdown inputs with built-in sorting:

    Flow Manager Sorting

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    layout

    Type: String | Default: grid

    Change layout of the widget.

    query

    Type: Object | Default: DefaultQuery

    Set custom query parameters.

    Events

    flow:open

    Select a flow to open in Designer widget.

    flow:create

    Click Create Flow button.

    flow:start

    Toggle flow stage button.

    flow:stop

    Toggle flow stage button.

    flow:clone

    Click menu item to clone a flow.

    flow:share

    Click menu item to open sharing of a flow.

    flow:rename

    Click menu item to rename flow.

    flow:remove

    Click menu item to remove a flow.

    Sharing

    Add menu item with flow:share event for a configurable flow sharing dialog:

    Flow Manager Sharing

    Example

    const flowManager = appmixer.ui.FlowManager(config)
    
    flowManager.set(key, value)
    flowManager.get(key)
    Flow Manager
    appmixer.ui.FlowManager({
      /* ... */
      options: {
          menu: [
            { event: 'flow:open', label: 'Open', icon: 'data:image/svg+xml;base64,...' },
            { event: 'flow:rename', label: 'Rename', icon: 'https://www.example.com/images/image.jpg' },
            { event: 'flow:insights', label: 'Insights' },
            { event: 'flow:clone', label: 'Clone' },
            { event: 'flow:share', label: 'Share' },
            { event: 'flow:remove', label: 'Remove' },
            { event: 'my-custom-event', label: 'Custom Event' }
        ]
      }
    }
    appmixer.ui.FlowManager({
      /* ... */
      options: {
        filters: [
          { property: 'stage', value: 'running', label: 'Running flows' },
          { property: 'stage', value: 'stopped', label: 'Stopped flows' },
          { property: 'sharedWith', value: 'myFlows', label: 'My flows' },
          { property: 'sharedWith', value: 'sharedWithOthers', label: 'Shared with others' },
          { property: 'sharedWith', value: 'sharedWithMe', label: 'Shared with me' }
        ]
      }
    }
    appmixer.ui.FlowManager({
      /* ... */
      options: {
        customFilter: {
          stage: 'running'
        }
      }
    }
    appmixer.ui.FlowManager({
      /* ... */
      options: {
        customFilter: {
          'customFields.category': 'healthcare',
          'customFields.template': true
        }
      }
    }
    appmixer.ui.FlowManager({
      /* ... */
      options: {
        sorting: [
          { label: 'Last Modified', property: 'mtime', value: -1 },
          { label: 'Last Created', property: 'btime', value: -1 }
        ]
      }
    }
    flowManager.state(name, value)
    flowManager.on(event, handler)
    flowManager.on('flow:open', flowId => {/* ... */})
    flowManager.on('flow:create', () => {/* ... */})
    flowManager.on('flow:start', flowId => {/* ... */})
    flowManager.on('flow:stop', flowId => {/* ... */})
    flowManager.on('flow:clone', flowId => {/* ... */})
    flowManager.on('flow:share', flowId => {/* ... */})
    flowManager.on('flow:rename', flowId => {/* ... */})
    flowManager.on('flow:remove', flowId => {/* ... */})
    appmixer.ui.FlowManager({
      /* ... */
      options: {
        menu: [{ event: 'flow:share', label: 'Share' }],
        // specify custom types and scopes
        shareTypes: [
          { value: 'email', label: 'Email', placeholder: 'Enter an email' },
          { value: 'scope', label: 'Scope', placeholder: 'Enter a scope' },
          { value: 'domain', label: 'Domain', placeholder: 'Enter a domain' }
        ],
        // override default permissions
        sharePermissions: [
          { label: 'Read', value: 'read' },
          { label: 'Start', value: 'start' },
          { label: 'Stop', value: 'stop' }
        ]
      }
    }
    // create a new widget
    const flowManager = appmixer.ui.FlowManager({
      el: '#flow-manager',
      options: {
        menu: [
          { event: 'flow:open', label: 'Open' },
          { event: 'custom-event', label: 'Custom' },
          { event: 'flow:rename', label: 'Rename' },
          { event: 'flow:insights', label: 'Insights' },
          { event: 'flow:clone', label: 'Clone' },
          { event: 'flow:share', label: 'Share' },
          { event: 'flow:remove', label: 'Remove' }
        ],
        filters: [
          { property: 'stage', value: 'running', label: 'Running flows' },
          { property: 'stage', value: 'stopped', label: 'Stopped flows' },
          { property: 'sharedWith', value: 'myFlows', label: 'My flows' },
          { property: 'sharedWith', value: 'sharedWithOthers', label: 'Shared with others' },
          { property: 'sharedWith', value: 'sharedWithMe', label: 'Shared with me' }
        ],
        sorting: [
          { label: 'Last Modified', property: 'mtime', value: -1 },
          { label: 'Last Created', property: 'btime', value: -1 }
        ]
      }
    })
    
    // change default layout
    flowManager.state('layout', 'list')
    
    // override a built-in event
    flowManager.on('flow:create', () => {
      flowManager.state('error', 'Creating a new flow overridden by a custom event handler.')
    })
    
    // load flow details with a custom event
    flowManager.on('custom-event', async flowId => {
      try {
        flowManager.state('loader', true)
        const flow = await appmixer.api.getFlow(flowId)
        alert(`Flow ${flow.name} has ${Object.keys(flow.flow).length} component(s).`)
      } catch (error) {
        flowManager.state('error', 'Loading flow failed.')
      } finally {
        flowManager.state('loader', false)
      }
    })
    
    // open the widget
    flowManager.open()

    System Webhooks

    Appmixer Engine Events.

    Component Errors

    Components might throw exceptions. Such exceptions/errors are logged and users can see them in the Insights. 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.

    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:

    Quota Limit Errors

    If you have defined quota limits for the component and the limit has been reached then a Quota Error is posted to the error notification webhook. You can read more about Quotas and Limits . Below is the example of the posted quota error which is raised when the weather API request quota limit has been reached. The type property will contain quota value. Appmixer will try to process such a message again. But too many of these notifications may indicate, that some flows are generating too many messages.

    Single quota error does not necessarily mean something is wrong. Let's say a flow is writing rows into a Google Spreadsheet. And the flow is about to write 200 rows. There is a Google quota, a maximum of 60 requests at the same time. After writing 60 rows, the component will reach the quota limit and generate one of these notifications. But after some time, the quota will be available again and the component will write the remaining 140 rows.

    Forgot Password

    Appmixer engine has an to reset the forgotten passwords. The Forgot Password webhook is triggered whenever a user in the system requests for forgot password API. You can configure a webhook by setting the environment variable WEBHOOK_USER_FORGOT_PASSWORD to URL where the Appmixer engine will submit POST payload as follows.

    If forgot password webhook is configured as explained above, the Appmixer engine will not send the email to the user and trigger the webhook instead.

    User

    API for users

    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": "[email protected]", "password": "abc321" }'

    Request Body

    Name
    Type
    Description

    Create User

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

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

    Request Body

    Name
    Type
    Description

    Get User Information

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

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

    Get User Information

    GET https://api.appmixer.com/users/:userId

    Admin token required.

    Get all users

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

    Admin token required.

    Examples:

    Get the first 30 users with a scope "acme1":

    curl -XGET "https://api.appmixer.com/users?filter=scope:acme1&sort=created:-1&limit=30&offset=0" -H 'Authorization: Bearer [ADMIN_TOKEN]'

    Get all users who's username includes a pattern:

    curl -XGET "https://api.appmixer.com/users?pattern=joe" -H 'Authorization: Beader [ADMIN_TOKEN]'

    Get number of users

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

    Admin token required

    Update user

    PUT https://api.appmixer.com/users/:userId

    Admin token required.

    Request Body

    Name
    Type
    Description

    Delete user

    DELETE https://api.appmixer.com/users/:userId

    Admin token required. This operation stops all running flows and deletes all the user's data from the system - logs, accounts, tokens ... The response is a ticket, the operation may take a long time. You can use the ticket and poll for the result with the next API method.

    GET https://api.appmixer.com/users/:userId/delete-status/:ticket

    Change user password

    POST https://api.appmixer.com/user/change-password

    User token required.

    Request Body

    Name
    Type
    Description

    Reset user password

    POST https://api.appmixer.com/user/reset-password

    Admin token required.

    Request Body

    Name
    Type
    Description

    Forgot Password

    POST https://api.appmixer.com/user/forgot-password

    See the configuration for more details.

    Request Body

    Name
    Type
    Description

    Reset forgotten password

    POST https://api.appmixer.com/user/forgot-password/reset

    Reset user password by providing unique code generated via POST /user/forgot-password API.

    Request Body

    Name
    Type
    Description

    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]"

    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

    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

    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

    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

    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

    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

    Request Body

    Name
    Type
    Description

    Create a new Store Item

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

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

    Path Parameters

    Name
    Type
    Description

    Request Body

    Name
    Type
    Description

    Update key or value of an existing store item

    PATCH https://api.appmixer.com/store/:id/:key

    Use this endpoint to rename a key or update the value against the key. Updates are passed in the body payload.

    curl --location --request PATCH 'https://api.appmixer.com/store/623632fb3eb18366c82aa9fd/existingKey' --header 'Authorization: Bearer [ACCESS TOKEN]' --header 'Content-Type: application/json' --data-raw '{ "key": "newKey", "value": "newValue" }'

    Path Parameters

    Name
    Type
    Description

    Request Body

    Name
    Type
    Description

    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

    Download the content of a Data Store

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

    The endpoint downloads the entire store either as a CSV or JSON file.

    Query Parameters

    Name
    Type
    Description

    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

    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

    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

    Request Body

    Name
    Type
    Description

    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

    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]"

    allowedPrivateComponents

    Array

    Array of component types.

    vendor

    String|Array

    One or more vendors.

    [
        {
            "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...
        }
    ]

    password*

    string

    Password.

    username*

    string

    Username, has to have an email format.

    password*

    string

    Password.

    email

    string

    Email address.

    username

    string

    Email address.

    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    {
      "id": "58593f07c3ee4f239dc69ff7",
      "username": "[email protected]",
      "isActive": true,
      "email": "[email protected]",
      "scope": [
        "user"
      ],
      "plan": "beta"
    }

    username

    String

    Username

    email

    String

    Email

    password

    String

    Password

    scope

    Array

    Array of scopes.

    {
        "ticket": "830639e3-c53a-42d6-ad43-0276674236b4"
    }
    {
        "status": "in-progress | completed | failed | cancelled",
        "stepsDone": 4,       // can be used to display a progress bar
        "stepsTotal": 10      // can be used to display a progress bar
    }

    oldPassword*

    String

    Old password

    newPassword*

    String

    New password

    email*

    String

    User email address

    password*

    String

    New password

    email*

    String

    Email address

    password*

    String

    New password. The minimum length of the password is five characters.

    code*

    String

    Code generated via forgot-password.

    {}
    Forgot Password Service
    {
        "user": {
            "id": "5c88c7cc04a917256c726c3d",
            "username":"[email protected]",
            "isActive": false,
            "email": "[email protected]", 
            "plan":"free"
        },
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    [{
        "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"
    }]

    id

    string

    Store ID.

    storeId

    string

    Store ID.

    {
        "count": 681
    }

    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.

    name

    string

    Name of the store.

    {
        "storeId": "5c7f9bfe51dbaf0007f08db0"
    }

    id

    string

    Store ID.

    id

    string

    Store ID.

    name

    string

    New name of the store.

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

    key

    string

    Key under which the posted value will be stored.

    id

    string

    Store ID.

    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"
    }

    id*

    String

    Store ID

    key*

    String

    Key under which the updates are required

    key

    String

    New key

    value

    String

    New Value

    {
        "key": "New Key",
        "value": "New Value"
        "createdAt": "2021-09-01T11:34:00.258+0000",
        "updatedAt": "2021-09-01T11:34:00.258+0000",
    }

    items

    array

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

    {
        "deletedCount": 1
    }

    format

    String

    Default value 'json'. The other option is 'csv'.

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

    webhook

    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 Behaviour section, especially the context.getWebhookUrl() for details and example.

    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.

    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
       },
       {
    
    
    {
      "_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"
      }
    }

    size

    number

    Maximum number of logs returned. Useful for pagination.

    from

    number

    Index of the first log returned. Useful for pagination.

    {
        "aggregations": {
            "avg_price": {
                "value": 10
            },
            "sum_income": {
                "value": 2000
            }
        },
        "hits": [
            { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
            { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
            ...
        ]
    }
    {
      "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"
      ]
    }

    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

    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.

    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.

    aggs

    object

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

    to

    string

    To date.

    from

    string

    From date.

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

    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

    Name
    Type
    Description

    app

    string

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

    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

    Name
    Type
    Description

    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"
    ]

    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

    {
        "ticket":"a194d145-3768-4a8a-84a4-4f1e4e08c4ad"
    }

    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

    Name
    Type
    Description

    ticket

    string

    Ticket that you got from the POST /component request.

    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

    Name
    Type
    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).

      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)"
        },
        "type": "component",
        "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"
        },
        "type": "component",
        "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": "[email protected]",
                                "type": "to"
                            }
                        ],
                        "from_email": "[email protected]"
                    },
                    "scope": {
                        "bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
                            "out": {
                                "started": "2020-12-14T11:20:41.858Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-12-14T11:20:41.858Z"
                    }
                }
            ]
        },
        "accessTokenId": null
    }
    {
        "err": {
            "message": "Ran out of quota for appmixer:utils:weather",
            "error":429,
            "data":429,
            "name":"QuotaError",
            "stack":"QuotaError: Ran out of quota for appmixer:utils:weather\\n    at Request._callback (/Driver.js:67:31)\"
        },
        "type": "quota",
        "flowId": "b7db896e-0e5c-4ee1-9cb3-1099c9f43b62",
        "flowName": "Daily Rainy Day Alert",
        "userId": "60ef4fd533d6143ba09c400f",
        "componentId": "13ad74e3-3354-463c-bf30-ea6e0c157ad8",
        "componentType": "appmixer.utils.weather.GetCurrentWeather",
        "inputMessages": {
            "location": [
                {
                    "properties": {
                        "correlationId": '4dc33f4c-dbd0-4e97-ab05-381d785851f3',
                        "gridInstanceId": null,
            ...
                }
            ]
        }
    }
    // WEBHOOK_USER_FORGOT_PASSWORD = 'http://hosted-url.com/events/forgot-password';
    
    {
      'code': 'unique code generated for identifying forgot password request',
      'email': 'email address of user requesting for forgot password',
      'userId': 'USER id of user',
      'created': 'Date when a user requested for forgot password',
      'link': 'Link to access forgot password page on frontend'
    }
    here
    API
    Log viewer in Designer.

    API Module

    The Appmixer SDK uses this API module internally to connect to the REST API.

    Name
    Description

    People Task

    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.

    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

    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:

    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.

    Dependencies

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

    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 .

    "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"
    }
    ]
    }
    ,
    "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"
    }
    }
    [
        {
            "name": "appmixer.twilio.sms.SendSMS",
            "author": "David Durman <[email protected]>",
            "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 <[email protected]>",
            "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
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    ]
    // 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'"
        }
      ]
    }
    {
        "name": "appmixer.twilio.sms.SendSMS",
        "version": "1.0.0",
        "private": true,
        "main": "SendSMS.js",
        "author": "David Durman <[email protected]>",
        "dependencies": {
            "twilio": "^2.11.0"
        }
    }
    https://docs.npmjs.com/files/package.json

    Using Appmixer API

    The Appmixer API allows you to access all the features that the UI works with via a REST API. You should have already received some user credentials when your hosted environment was created. Or you could have already tried the sign-up page (https://my.[acme].appmixer.cloud) and created other users. 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 "[email protected]" and "abc321" with your own user's username and password):

    curl -XPOST "https://api.[acme].appmixer.cloud/user/auth" -d '{"username": "[email protected]", "password": "abc321"}' -H 'Content-Type: application/json'

    You should see a response that looks like this:

    {"user":{"id":"5c88c7cc04a917256c726c3d","username":"[email protected]","isActive":false,"email":"[email protected]","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 "https://api.[acme].appmixer.cloud/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 API section to learn more about all the endpoints that you use.

    Configuration

    Set up a new api instance with config parameters and set/get methods:

    config.baseUrl

    Type: String | Default: null

    Base URL of your Appmixer engine REST API.

    config.accessToken

    Type: String | Default: null

    Access token of an authorized user.

    Methods

    api.authenticateUser

    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 a 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 a 403 status code (i.e. err.response.status === 403), the user does not exist in Appmixer.

    api.signupUser

    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.

    api.createFlow

    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.

    api.deleteFlow

    Delete an existing flow identified by flowId.

    api.getFlow

    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 Data URI format).

    api.getFlows

    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:

    api.getFlowsCount

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

    api.updateFlow

    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.

    api.startFlow

    Start a flow.

    api.stopFlow

    Stop a flow.

    api.cloneFlow

    Create a copy of an existing flow. The returned promise resolves to the ID of the newly created flow.

    api.getUser

    Get current user. The returned promise resolves to an object with username.

    api.getStores

    Get all the data stores. The returned promise resolves to an array of stores each an object with name and storeId properties.

    api.getStore

    Get one store. The returned promise resolves to an object with name and storeId properties.

    api.getStoreRecordsCount

    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.

    api.getStoreRecords

    Get store records. query is an object with storeId, pattern (string to search for in keys/values), limit , offset and sort properties. Example:

    api.createStore

    Create a new store. The returned promise resolves to the ID of the newly created store.

    api.deleteStore

    Delete a store.

    api.renameStore

    Rename an existing store.

    api.createStoreItem

    Create a new record in a store.

    api.deleteStoreItems

    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.

    api.createAccount

    Create a custom account.

    api.getAccounts

    Get a list of connected accounts of the user. filter is a custom query string (see the GET /accounts for an example). The returned promise resolves to an array of objects of the form { accountId, name, displayName, service, icon, profileInfo }.

    api.getComponentAccounts

    Get a list of accounts connected to a specific component.

    api.getAccountFlows

    Get a list of flows this account is used in. The returned promise resolves to an array of objects with flowId and name properties.

    api.setAccountName

    Rename a connected account. Note that this name is displayed in the Accounts widget and also in the Inspector UI of the Designer.

    api.getLogs

    Get logs. The query is an object of the form { from, size, sort, query }:

    Get logs of a specific flow:

    api.getLog

    Get one log. logId and index are values returned from getLogs().

    api.getPeopleTasks

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

    api.getPeopleTasksCount

    Returns the number of tasks based on the query. See getPeopleTasks(query) for more info.

    api.getPeopleTask

    Return one task identified by id.

    api.approveTask

    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.

    api.rejectTask

    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.

    api.getCharts

    Returns all the Insights charts of the user.

    api.getChart

    Return one Insights chart identified by chartId.

    api.deleteChart

    Delete an Insights chart identified by chartId.

    api.getFlowAuthentication

    This request will return an object with all the components in the flow that have auth section with all the available accounts.

    Events

    error

    The event is triggered when a request fails with an error or when the access token is invalid.

    warning

    The event is triggered when API validation fails with a warning.

    api.set(name, value)

    Set configuration property.

    api.get(name)

    Get configuration property.

    api.on(event, handler)

    Add event listener.

    api.off(event, handler)

    Remove event listener.

    const appmixer = new Appmixer(config)
    
    appmixer.api.set(key, value)
    appmixer.api.get(key)
    await appmixer.api.authenticateUser(username, password)
    await appmixer.api.signupUser(username, password)
    await appmixer.api.createFlow(name, [descriptor], [properties])
    await appmixer.api.deleteFlow(flowId)
    await appmixer.api.getFlow(flowId)
    await appmixer.api.getFlows(query)
    {
      limit: 20,
      offset: 0,
      pattern: "slack",
      projection: "-flow,-thumbnail",
      sort: "mtime:-1",
      sharedWithPermission: "read",
      filter: "userId:423jfdsalfjl4234fdsa"
    }
    await appmixer.api.getFlowsCount(query)
    await appmixer.api.updateFlow(flowId, update)
    await appmixer.api.startFlow(flowId)
    await appmixer.api.stopFlow(flowId)
    await appmixer.api.cloneFlow(flowId)
    await appmixer.api.getUser()
    await appmixer.api.getStores()
    await appmixer.api.getStore(storeId)
    await appmixer.api.getStoreRecordsCount(query)
    await appmixer.api.getStoreRecords(query)
    {
      limit: 30,
      offset: 0,
      pattern: "foo",
      sort: "updatedAt:-1",
      storeId: “5c6d643f4849f447eba55c1d"
    }
    await appmixer.api.createStore(name)
    await appmixer.api.deleteStore(storeId)
    await appmixer.api.renameStore(storeId, newName)
    await appmixer.api.createStoreItem(storeId, key, value)
    await appmixer.api.deleteStoreItems(items)
    await appmixer.api.createAccount(params, data)
    await appmixer.api.getAccounts(filter)
    await appmixer.api.getComponentAccounts(componentType, componentId)
    await appmixer.api.getAccountFlows(accountId)
    await appmixer.api.setAccountName(accountId, newName)
    await appmixer.api.getLogs(query)
    {
      from: 0,
      size: 30,
      sort: "@timestamp:desc",
      query: "@timestamp:[2018-01-01 TO 2018-01-01]"
    }
    {
      from: 0,
      size: 30,
      sort: "@timestamp:desc",
      query: "@timestamp:[2018-01-01 TO 2018-01-01] AND +flowId:FLOW_ID"
    }
    await appmixer.api.getLog(logId, index)
    await appmixer.api.getPeopleTasks(query)
    await appmixer.api.getPeopleTasksCount(query)
    await appmixer.api.getPeopleTask(id)
    await appmixer.api.approveTask(id, [params])
    await appmixer.api.rejectTask(id, [params])
    await appmixer.api.getCharts()
    await appmixer.api.getChart(chartId)
    await appmixer.api.deleteChart(chartId)
    await appmixer.api.getFlowAuthentication(flowId)
    appmixer.api.on(event, handler)
    appmixer.api.on('error', error => {
      if (error.code === 401) {
        /* A request failed because the current access token is invalid ... */
      }
    }
    appmixer.api.on('warning', warning => { ... }

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

    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": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "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.

    {
        "approver": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "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.

    {
        "approver": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "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": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "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": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "status": "rejected",
        "title": "Example title"
    }
    appmixer.ui.PeopleTask UI SDK
    People Tasks tutorial
    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
    {
        "description": "This action gets the current weather conditions for a location."
    }
    Component Description

    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.

    Flow

    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 https://openweathermap.org 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.

    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.

    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"
            ]
        }
    }

    The auth.service identifies the authentication module 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.

    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.

    Connected Accounts

    Accounts

    Manage accounts authorized by the current user.

    Accounts

    Configuration

    Set up a new instance with config parameters and set/get methods:

    const accounts = appmixer.ui.Accounts(config)
    
    accounts.set(key, value)
    accounts.get(key)

    config.el ...

    Learn about widget config .

    Instance

    Learn about widget instance .

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    flow:open

    Select a flow to open in Designer widget.

    Example

    Insights Logs

    Browse logs of messages that passed through flows.

    Insights Logs

    Configuration

    Set up a new instance with config parameters and set/get methods:

    const insightsLogs = appmixer.ui.InsightsLogs(config)
    
    insightsLogs.set(key, value)
    insightsLogs.get(key)

    config.el ...

    Learn about widget config .

    config.flowId

    ID of a flow to filter the logs by.

    Instance

    Learn about widget instance .

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Example

    Connectors

    Browse apps and components that are accessible to the current user inside flows.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    const connectors = appmixer.ui.Connectors(config)
    
    connectors.set(key, value)
    connectors.get(key)

    config.el ...

    Learn about widget config .

    Instance

    Learn about widget instance .

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Example

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

    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.

    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.

    Config

    System configuration

    Get current user-defined configuration values

    GET /config

    Only returns the values that have been stored by the user through the API

    authConfig

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

    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 .

    People Tasks

    Manage tasks created by utility components of flows.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    Getting started

    You can start with our or .

    connectors.state(name, value)
    const connectors = appmixer.ui.Connectors({
        el: '#connectors'
    })
    
    connectors.open()
    here
    here
    {
    	"authConfig": {
    		"service": "appmixer:deepai"
    	}
    }
    Service Configuration
    deepai
    auth
    here
    SDK Demo: Workflow Automation
    SDK Demo: Integration Templates
    accounts.state(name, value)
    accounts.on(event, handler)
    accounts.on('flow:open', flowId => {/* ... */})
    const accounts = appmixer.ui.Accounts({
        el: '#accounts'
    })
    
    accounts.open()
    here
    here
    insightsLogs.state(name, value)
    const insightsLogs = appmixer.ui.InsightsLogs({
        el: '#insights-logs'
    })
    
    insightsLogs.open()
    here
    here

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

    requester

    string

    Requester's email address.

    approver

    string

    Approver's email address.

    requester

    string

    Requester's email address.

    approver

    string

    Approver's email address.

     [{
        "approver": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "status": "pending",
        "title": "Example title",
        "id": "5da9ed9ff29cd51c5fa27380"
    }, {
        "approver": "[email protected]",
        "decisionBy": "2020-01-01 19:00:00",
        "description": "Example description",
        "requester": "[email protected]",
        "status": "pending",
        "title": "Example title",
        "id": "5da9ed9ff29cd51c5fa27380"
    }]
    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Example

    const peopleTasks = appmixer.ui.PeopleTasks(config)
    
    peopleTasks.set(key, value)
    peopleTasks.get(key)
    People Tasks
    peopleTasks.state(name, value)
    const peopleTasks = appmixer.ui.PeopleTasks({
        el: '#people-tasks'
    })
    
    peopleTasks.open()
    The OAuth variables can be set through the Backoffice.
    here
    [
      {
        "key": "JWTSecret",
        "value": "OQekJ3DH4pRnWFl4wlN0hzhc5UIjdihEwFnwYLYUdXGXk+/f5JieT/1VLPUJnvALIGK014md41rUuarqYZscl2T5azHQmFhQmUKj8dEuoIELWB45wlkxDKcojCQi9Otk76itnmvKrbm/ZokDJxePNv2Edgc7/mLrTHG7l54w44c="
      },
      {
        "key": "WEBHOOK_FLOW_COMPONENT_ERROR",
        "value"
    

    Create a configuration key/value pair

    POST /config

    Request Body

    Name
    Type
    Description

    key*

    String

    Configuration key

    value*

    Any

    Configuration value

    {
      "key": "myConfigKey",
      "value": "My Custom Value"
    }

    Removes a configuration entry

    DELETE /config/:key

    Path Parameters

    Name
    Type
    Description

    key*

    String

    The key of the configuration to be removed

    { "ok": true }
    Inspector panel

    Wizard

    Manage a flow that is used as an integration instance.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    :
    "https://example.com/webhook"
    }
    ]
    config.el ...

    Learn about widgetconfig here.

    config.flowId

    Type: String | Default: null

    The ID of a flow that is opened in the wizard.

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    flow:start

    Submit the form and start the flow.

    flow:validation

    Flow validation errors changed.

    cancel

    Click a button to close the form.

    close

    Submit the form and wait for the flow to start.

    Example

    const wizard = appmixer.ui.Wizard(config)
    
    wizard.set(key, value)
    wizard.get(key)
    Wizard
    wizard.state(name, value)
    wizard.on(event, handler)
    wizard.on('flow:start', flowId => {/* ... */})
    wizard.on('flow:validation', errors => {/* ... */})
    wizard.on('cancel', () => {/* ... */})
    wizard.on('close', () => {/* ... */})
    const wizard = appmixer.ui.Wizard({
        el: '#wizard',
        flowId: 'your-integration-id'
    })
    
    wizard.on('flow:start', async flowId => {
        await appmixer.api.startFlow(flowId)
        wizard.close()
    })
    
    wizard.open()

    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.

    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

    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

    Request Body

    Name
    Type
    Description

    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

    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

    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

    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:

    {
        "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": [
        ...
    }

    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.

    Constructor

    Appmixer Constructor lays a foundation for building user interfaces with widgets.

    Configuration

    Set up a new appmixer instance with config parameters and set/get methods:

    const appmixer = new Appmixer({/* [name]: value */})
    
    appmixer.set(name, value)
    appmixer.get(name)

    config.baseUrl

    Type: String | Default: null

    Base URL of your Appmixer engine REST API.

    config.accessToken

    Type: String | Default: null

    Access token of an authorized user.

    config.debug

    Type: Boolean | Default: false

    Enable debugger for development purposes.

    config.theme

    Type: Object | Default: DefaultTheme

    config.l10n

    Type: Object | Default: DefaultL10N

    Define custom localization texts.

    config.lang

    Type: String | Default: en

    Specify a language code for the localization of components.

    config.api

    Type: Object | Default: {}

    Set custom API methods.

    Instance

    appmixer.ui

    Register and create UI Widgets.

    appmixer.api

    Use methods of built-in API Module.

    appmixer.set

    Set configuration property.

    appmixer.get

    Get configuration property.

    appmixer.registerCustomComponentShape

    Register a custom Designer component shape.

    appmixer.registerInspectorField

    Register a custom Designer inspector field.

    Basic Usage

    Connect to Appmixer engine REST API and render user interfaces with a built-in widget:

    Change USERNAME and PASSWORD parameters to valid credentials for registration of a new user.

    Learn more about the basic usage with the Quick Start example.

    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:

    The

    localization

    An optional object containing localization strings. For example:

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

    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]/quota.js , [vendor/[service]/[module]/quota.jsor [vendor/[service]/[module]/[component]/quota.js.

    An example of a quota module:

    The quota definition above tells the engine to throttle the receive()

    Insights Dashboard

    Browse and manipulate charts created by the current user.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    Storage

    Manage records associated with data storage utility components of flows.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <[email protected]>",
        "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"
           }
       }
    }
    Custom Component Strings
    appmixer.ui('Widget', {/* ... */})
    appmixer.ui.Widget({/* ... */})
    appmixer.set(key, value)
    appmixer.get(key, value)
    appmixer.registerCustomComponentShape(name, shape)
    appmixer.registerInspectorField(type, Field, options)
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <script src="https://my.YOURTENANT.appmixer.cloud/appmixer/appmixer.js"></script>
    </head>
    <body>
      <div id="your-widget"></div>
    
      <script async type="module">
        const appmixer = new Appmixer({ baseUrl: BASE_URL })
    
        const auth = await appmixer.api.signupUser(USERNAME, PASSWORD)
        appmixer.set('accessToken', auth.token)
    
        appmixer.ui.FlowManager({ el: '#your-widget' }).open()
      </script>
    </body>
    </html>
    Flow Manager
    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.

    You can also configure system webhook to receive the quota errors when raised. Read more about it here.

    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'
        }]
    };
    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    chart:clone

    Clone chart.

    chart:remove

    Remove chart.

    chart:open

    Open chart in Chart Editor.

    Example

    const insightsDashboard = appmixer.ui.InsightsDashboard(config)
    
    insightsDashboard.set(key, value)
    insightsDashboard.get(key)
    Insights Dashboard
    insightsDashboard.state(name, value)
    insightsDashboard.on(event, handler)
    insightsDashboard.on('chart:clone', chartId => {/* ... */})
    insightsDashboard.on('chart:remove', chartId => {/* ... */})
    insightsDashboard.on('chart:open', chartId => {/* ... */})
    const insightsDashboard = appmixer.ui.InsightsDashboard({
        el: '#insights-dashboard'
    })
    
    insightsDashboard.open()
    config.el ...

    Learn about widget config here.

    config.storeId

    Type: String | Default: []

    ID of a store to open within the storage.

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Example

    const storage = appmixer.ui.Storage(config)
    
    storage.set(key, value)
    storage.get(key)
    Storage
    storage.state(name, value)
    const storage = appmixer.ui.Storage({
        el: '#storage'
    })
    
    storage.open()
    [
        {
            "role": "admin",
            "resource": "*",
            "action": [
                "*"
            ],
            "attributes": [
                "non-private"
            ]
        },
        {
            "role": "user",
            "resource": "*",
            "action": [
                "*"
            ],
            "attributes": [
                "non-private"
            ]
        },
    

    type

    string

    components | routes

    type

    string

    components | routes

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

    type

    string

    components | routes

    ['*', 'flows']

    type

    string

    components | routes

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

    type

    string

    components | routes

    resource

    string

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

    [
        "routes",
        "components"
    ]
    message
    from the example looks like this in the raw form:

    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

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

    inPort.inspector

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

    inPort.source

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

    An example of how the source property can be used to generate the input port Inspector dynamically for the appmixer.google.spreadsheets.CreateRow component. When showing the Inspector for the CreateRow, we need to know the structure (columns) of the Worksheet, the Inspector input fields will copy the columns in the Worksheet

    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 components. This might not be desirable in some cases. One can set scopeDepth to a number that represents the depth (levels back 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:

    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.

    {
        "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
                        }
                    }
                }
            }
        ]
    }
    Input Port Configuration using Variables
    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]}}}
    {
    ...
        "inPorts": [
            {
                "name": "in",
                "schema": {
                    "type": "object"
                },
                "source": {
                    // The ListColumns component can return an array of columns in a
                    // Worksheet.
                    "url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
                    "data": {
                        // The ListColumns component needs two properties in order
                        // to get the list of columns, the Spreasheet Id and the
                        // Worksheet Id. Both will be taken from properties of the
                        // CreateRow component (the caller).
                        "properties": {
                            // Appmixer will replace 'properties/sheetId' with
                            // the actual value before making the call
                            "sheetId": "properties/sheetId",
                            "worksheetId": "properties/worksheetId"
                        },
                        // A transformer function 'columnsToInspector' from the 
                        // ListColumns.js will be executed in order to transform a list
                        // of columns to the Appmixer Inspector.
                        "transform": "./ListColumns#columnsToInspector"
                    }
                }
            }
        ]
    }
    {
        "variablesPipeline": {
            "scopeDepth": 1,
            "rawValue": true
        }
    }
    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": [
            {
       ...
    }

    Appmixer Deployment Models

    Appmixer Deployment Models

    Appmixer supports all cloud deployment models:

    • Public cloud

    • Private cloud

    • VPC

    • Hybrid cloud

    All 3rd party managed services and integrations are optional and customizable. For example, you can choose for which of the supporting technologies you prefer to use a 3rd party managed alternative. You can also choose what integrations you would like to offer to your end/internal-users and configure your own notifications (e.g. email) via customizable webhooks.

    Appmixer Supporting Technologies

    Appmixer uses multiple supporting technologies to function: MongoDB, RabbitMQ, Redis and ElasticSearch. All the supporting technologies are open-source.

    Managed Services

    Supporting technologies can be either installed and managed on an “as-is” basis or 3rd party managed (e.g. CloudAMQP’s RabbitMQ as a Service can be used).

    For help with deploying Appmixer with full compatibility with AWS managed services for all the supporting technologies (Amazon MQ, ElasticCache, DocumentDB, OpenSearch), please contact our customer support ([email protected]).

    Appmixer Kubernetes Deployment

    Appmixer self-managed package includes Docker and Kubernetes configuration files for easy deployment.

    Appmixer engine (together with Appmixer Backoffice and Appmixer Studio) images are available via Appmixer Docker Registry.

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

    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

    Get message

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

    Get a single message.

    Path Parameters

    Name
    Type
    Description

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

    Delete a message.

    Path Parameters

    Name
    Type
    Description

    Retry a message

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

    Put the message back into Appmixer engine.

    Path Parameters

    Name
    Type
    Description

    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

    Get Service Configuration

    GET /service-config/:serviceId

    Get the configuration stored for the given service.

    Path Parameters

    Name
    Type
    Description

    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

    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

    Request Body

    Name
    Type
    Description

    Delete Service Configuration

    DELETE /service-config/:serviceId

    Removes the configuration from the given service.

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

    string

    The service id. Example: appmixer:google

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

    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"
    }

    serviceId

    string

    The service id. Example

    whatever-key

    string

    Any value you need

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

    serviceId

    string

    The service id. Example: appmixer:google

    {}
    [
    	{
    		"serviceId": "appmixer:google",
    		"clientID": "my-global-client-id",
    		"clientSecret": "my-global-client-secret"
    	},
    	{
    		"serviceId": "appmixer:evernote",
    		"sandbox": true,
    	}
    ]
    [
        {
            "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"
    
    {
            "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...
        }

    string

    messageId

    string

    messageId

    string

    {}

    messageId

    string

    {}

    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

    Integrations

    Manage flows used as integration templates and instances.

    Configuration

    Set up a new instance with config parameters and set/get methods:

    },
    "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...
    }
    ]
    config.el ...

    Learn about widget config here.

    config.options

    Type: Object | Default: {}

    config.options.customFilter

    Type: Object | Default: {}

    Filter the integrations with additional parameters:

    Instance

    Learn about widget instance here.

    State

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message.

    Events

    integration:create

    Click a button to to create a new integration from template.

    integration:edit

    Click a button to edit integration.

    integration:remove

    Click a button to remove integration.

    integration:start

    Click a button to start integration.

    integration:stop

    Click a button to stop integration.

    Example

    const integrations = appmixer.ui.Integrations(config)
    
    integrations.set(key, value)
    integrations.get(key)
    Integrations
    appmixer.ui.Integrations({
      /* ... */
      options: {
        customFilter: {
          // displaying Integrations with a certain customField
          'customFields.category': 'healthcare',
          // that are shared with someone
          'sharedWith': '![]'
          // and in this example, that are shared throught the 'domain'
          // options with a 'testdomain.com'
          'sharedWith.domain': 'testdomain.com'
          // or we could filter only Integrations that are shared with
          // scope 'user'
          // 'sharedWith.scope': 'user',
        }
      }
    }
    integrations.state(name, value)
    integrations.on(event, handler)
    integrations.on('integration:create', integrationId => {/* ... */})
    integrations.on('integration:edit', integrationId => {/* ... */})
    integrations.on('integration:remove', integrationId => {/* ... */})
    integrations.on('integration:start', integrationId => {/* ... */})
    integrations.on('integration:stop', integrationId => {/* ... */})
    const integrations = appmixer.ui.Integrations({
        el: '#integrations'
    })
    
    integrations.open()
    
    integrations.on('integration:create', async templateId => {
        // Create integration flow as a clone of the template. Disconnect
        // accounts because they might not be shared with the end user.
        const integrationId = await appmixer.api.cloneFlow(templateId, { connectAccounts: false })
        await appmixer.api.updateFlow(integrationId, { templateId })
    
        const wizard = appmixer.ui.Wizard({
            el: '#your-wizard-div',
            flowId: integrationId,
        });
    
        wizard.open()
        integrations.reload()
    })
    
    integrations.on('integration:edit', function(integrationId) {
        var wizard = appmixer.ui.Wizard({
            el: '#wizard',
            flowId: integrationId
        });
        wizard.open()
    });
    
    integrations.open()
    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:

    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 JSON path syntax.

    You can customize the component's label and description. You can customize the 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:

    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 JSON path syntax. For more information about the Strings Object refer to the Custom Strings section.

    service.json and module.json localization

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

    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:

    Application groups localization

    Labels of groups of application modules can be localized/changed without rewriting a single module.json file.

    Use the localization strings to do so:

    Strings resolving

    When rendering the component's inspector, the strings are resolved with the following priority:

    1. Localization object in the manifest (component.json).

    2. Strings object components namespace.

    3. Property in the 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.

    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <[email protected]>",
        "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ů."
    }
    appmixer.set('strings', {
        ui: {
            designer: {
                stencil: {
                    groups: {
                        applications: 'Connectors',
                        utilities: 'Tools'
                    }
                }
            }
        }
    });

    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.

    Component Configuration

    Configuration properties are defined using two objects schema and inspector.

    properties.schema

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

    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

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

    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.

    Inspector built-in types:

    text

    A single line input field.

    textarea

    A multi-line text input field.

    number

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

    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.

    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:

    toggle

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

    color-palette

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

    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.

    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.

    The value of this field has the following structure:

    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

    The expand option in source. The select inputs in the expression can have dynamic values retrieved with the configuration (just like an ordinary select input). Sometimes you may want to define different dynamic values for every expression box based on another field(s) in that box:

    This is how it is done:

    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.

    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.

    You can use googlepicker to pick folders instead of files:

    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.

    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:

    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

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

    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:

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

    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:

    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:

    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.source.data.transform

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

    Example:

    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.

    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

    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

    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]"

    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

    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

    Request Body

    Name
    Type
    Description

    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

    Clone Flow

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

    Clone a flow

    Path Parameters

    Name
    Type
    Description

    Request Body

    Name
    Type
    Description

    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

    Request Body

    Name
    Type
    Description

    Send GET request to a component

    GET https://api.appmixer.com/flows/:flowId/components/:componentId

    Send POST request to a component

    POST https://api.appmixer.com/flows/:flowId/components/:componentId

    Send PUT request to a component

    PUT https://api.appmixer.com/flows/:flowId/components/:componentId

    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.

    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.

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

    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.

    field inside the second group, only
    variableB
    will be available, because
    variableA
    is already been used
    :
    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.

  • {
        "properties": {
            "schema": {
                "properties": {
                    "interval": {
                        "type": "integer",
                        "minimum": 5,
                        "maximum": 35000
                    }
                },
                "required": [
                    "interval"
                ]
            }
    }
    {
        "properties: {
            "inspector": {
                "inputs": {
                    "interval": {
                        "type": "number",
                        "group": "config",
                        "label": "Interval (in minutes, min 5, max 35000)"
                    }
                },
                "groups": {
                    "config": {
                        "label": "Configuration",
                        "index": 1
                    }
                }
            }
        }
    }
    {
        "type": "text",
        "label": "Text message."
    }
    {
        "type": "textarea",
        "label": "A multi-line text message."
    }
    {
        "type": "number",
        "label": "A numerical input.",
        "min": 1,
        "max": 10,
        "step": 1
    }
    {
        "type": "multiselect",
        "options": [
            { "content": "one", "value": 1 },
            { "content": "two", "value": 2 },
            { "content": "three", "value": 3 }
        ],
        "placeholder": "-- Select something --",
        "label": "Multi Select box"
    }

    Option

    Description

    format

    String representing the format of the date/time. Please see the moment.js library documentation for all the available tokens: https://momentjs.com/docs/#/parsing/string-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).

    {
        "type": "date-time",
        "label": "Date",
        "config": {
            "enableTime": true
        }
    }
    {
        "type": "toggle",
        "label": "Toggle field"
    }
    {
        "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" }
        ]
    }
    {
        "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..." }
        ]
    }
    {
          "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
              }
          ]
    }
    {
        "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" }
                ]
            }
        ]
    }
    {
    ...
        "inspector": {
            "inputs": {
                "expressionWithSource": {
                    "type": "expression",
                    "label": "Dynamic Expression",
                    "tooltip": "Dynamic Expression with a <b>source</b> call.",
                    "exclusiveFields": [ "select" ],
                    "index": 1,
                    "levels": [ "AND", "OR" ],
                    "fields": {
                        "text": {
                            "type": "text",
                            "label": "Text",
                            "tooltip": "A plain text field with <b>required: true</b>. The <b>value</b> of this field will be part of the variables in the select box <b>Dynamic Select</b>. If the <b>value</b> is <b>break</b> the component will throw an error that has to be visible in the UI.",
                            "required": true,
                            "index": 1
                        },
                        "select": {
                            "type": "select",
                            "label": "Dynamic Select",
                            "tooltip": "Dynamic Select options have to be available.",
                            "index": 2,
                            "source": {
                                "url": "/component/test/test/source/ExpressionWithExpand?outPort=out",
                                // The "expand" value is actually a path to an array
                                // in the flow JSON, that array is generated by this
                                // "expression", the Appmixer engine will then expand
                                // this array and call the "source" for each item in it.
                                // If you have different "levels" in your expression, 
                                // then you have to use yours here.
                                "expand": "$.expressionWithSource.AND.OR",
                                "data": {
                                    "properties": {
                                        "generateOptions": "select",
                                        // it will find the correct "text" input value
                                        // in the same "box" and use it in the "source"
                                        // call
                                        "text": "./text"
                                    },
                                    "transform": "./ExpressionWithExpand#fieldsToSelectArray"
                                }
                            }
                        }
                    }
                }
            }
        }
    ...
    }
    
    "inputs": {
        "fileId": {
            "type": "filepicker",
            "label": "Select file",
            "index": 1,
            "tooltip": "Pick a CSV file to import into the flow"
        }
    }
    "inputs": {
        "file": {
            "type": "googlepicker",
            "index": 1,
            "label": "File",
            "placeholder": "Choose a file...",
            "tooltip": "Choose a file to export."
        }
    }
    "inputs": {
        "file": {
            "type": "googlepicker",
            "index": 1,
            "label": "Folder",
            "placeholder": "Choose a folder...",
            "tooltip": "Choose a folder.",
            "view": "FOLDERS"
        }
    }
    "input": {
        "folder": {
            "type": "onedrivepicker",
            "index": 1,
            "label": "Folder",
            "placeholder": "Choose a folder...",
            "tooltip": "Choose a folder to upload the file to.",
            "view": "folders"
        }
    }
    "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
        }
    }
    {
          "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
              }
          ]
    }
    {
           "source": {
               "url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
               "data": {
                   "messages": {
                       "in": 1
                   },
                   "properties": {
                       "sheetId": "properties/sheetId",
                       "worksheet": "properties/worksheet"
                   },
                   "transform": "./transformers#columnsToInspector"
               }
           }
    }
    {
        inputs: { ... },
        groups: { ... }
    }
    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;
    };
    "/component/appmixer/google/spreadsheets/ListColumns?outPort=out"
    {
        "properties": {
            "targetComponentProperty": "properties/myProperty"
        }
    }
    {
        inputs: { ... },
        groups: { ... }
    }
    {
        "transform": "./transformers#columnsToInspector"
    }
    http://json-schema.org
    Rappid Inspector definition format
    CSS color formats
    source
    Invalid Inspector field
    Configuration Overview
    source with expand

    mode

    pattern

    string

    A term to filter flows containing pattern in their name or flowId property.

    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",
        
    
    {
      "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": "[email protected]",
                      "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": "[email protected]",
                      "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"
    }

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

    id

    string

    {
        "count": 29
    }    

    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"
    }

    id

    string

    Flow ID.

    object

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

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

    id

    string

    Flow ID.

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

    id*

    String

    Flow ID

    prefix

    String

    Prefix for flow clone name. The original flow name will be used with this prefix as a name for the new flow.

    projection

    String

    Properties to be filtered from the flow model.

    Example: "-thumbnail,-sharedWith". With this projection string, the thumbnail and sharedWith property will not be cloned.

    connectAccounts

    Boolean

    If user accounts (like Gmail account for SendEmail component) connected to the source flow components should also be connected to cloned flow components. Default is false. Accounts can be connected only if the owner of the cloned flow is the same as the owner of the original flow.

    isTemplateInstance

    Boolean

    If source flow is an instance of template (related to Integrations). Default is false

    {
      "cloneId": "cloned-flow-id"
    }

    id

    string

    Flow ID.

    command

    string

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

    {
        "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
    }
    {
        // Response
    }
    {
        // Response
    }
    {
        // Response
    }
    {
        // Response
    }
    {
        // Response
    }
    {
        // Response
    }
    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.

    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"
    }

    Get user files

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

    Example: https://api.appmixer.com/files?limit=10&filter=filename:~invoice&sort=filename:1

    The example will return 10 files with 'invoice' in the filename.

    Query Parameters

    Name
    Type
    Description

    limit

    number

    offset

    number

    sort

    String

    projection

    String

    [
        {
            "length": 2366210,
            "chunkSize": 261120,
            "uploadDate": "2022-09-14T15:25:33.281Z",
            "filename": "3mb.pdf",
            "md5": "69604bdd54ff681ecd0bf166544c0854",
            "metadata": {
                "userId": "6123bbeb34598f20b676833b"
            },
            "contentType": "application/pdf",
            "fileId": "7b917904-41b8-4c66-9f5f-0c1bf897eab6"
        }
    ]

    Get number of files

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

    Used for paging.

    Query Parameters

    Name
    Type
    Description

    includeComponentSourceFiles

    Boolean

    By default, the endpoint returns only files (that are used in flows). Custom component source codes are stored as files as well, if the user owns some, they can be returned as well.

    filter

    String

    {
        count: 2
    }

    Remove file.

    DELETE https://api.appmixer.com/files/:fileId

    {
        // Response
    }

    Delete all user files.

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

    {
        // Response
    }

    Request Body
    Name
    Type
    Description

    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.

    {
        chartId: '5defb3901f17d98d974fbb00'
    }

    Update Chart

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

    The same properties as in Create Chart API endpoint.

    Path Parameters

    Name
    Type
    Description

    string

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

    Get Charts

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

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

    Query Parameters

    Name
    Type
    Description

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

    Get One Chart

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

    Path Parameters

    Name
    Type
    Description

    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"
    }

    Delete a Chart

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

    Path Parameters

    Name
    Type
    Description

    id

    string

    ID of a chart.

    {
        "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":[]
                    }
                }
            }
        }
    }
    "btime"
    :
    "2018-03-29T19:24:08.950Z"
    ,
    "mtime": "2018-04-05T12:50:15.952Z",
    "sharedWith": [{
    "email": "[email protected]",
    "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": "[email protected]",
    "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": "[email protected]",
    "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": "[email protected]",
    "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"
    }
    ]

    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

    includeComponentSourceFiles

    Boolean

    By default, the endpoint returns only files (that are used in flows). Custom component source codes are stored as files as well, if the user owns some, they can be returned as well.

    filter

    String

    type

    string

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

    name

    string

    Name of the chart.

    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"
        }
    ]

    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:

    You can just download the component and publish it with:

    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:

    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

    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.

    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:

    myservice/service.json

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

    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:

    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:

    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.

    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:

    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):

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

    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:

    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:

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

    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.

    # 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 [email protected]
    $ appmixer publish appmixer.myservice.zip
    $ tree myservice/
    myservice/
    ├── mymodule
    │   └── HelloAppmixer
    │       ├── component.json
    │       └── HelloAppmixer.js
    └── service.json
    
    2 directories, 3 files
    module.exports = {};
    {
        "name": "appmixer.myservice.mymodule.HelloAppmixer",
        "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg=="
    }
    {
        "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 [email protected]
    
    # Now we can pack and publish our component:
    $ appmixer pack myservice
    $ appmixer publish appmixer.myservice.zip
    {
        "name": "appmixer.myservice.mymodule.HelloAppmixer",
        "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEeCRYRYwe3nwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAXSURBVAjXY2BgYPj//z8WErsoBAw+HQBdc1+hmBJKIwAAAABJRU5ErkJggg==",
        "inPorts": [
            {
                "name": "in"
            }
        ]
    }
    {
        "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"
                        }
                    }
                }
            }
        ]
    }
    $ appmixer pack myservice
    $ appmixer publish myservice.zip
    {
        "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
    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');
                });
        }
    };
    2KB
    appmixer.myservice.zip
    archive
    receive()
    Appmixer CLI tool
    receive()
    Axios
    Node package manager
    Custom App and Component
    Custom Component Inspector panel
    Custom Component Inspector panel
    Tweet received from Appmixer Custom Component
    Insights page

    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

    Query Parameters

    Name
    Type
    Description

    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

    Example of filtering certain accounts:

    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

    Request Body

    Name
    Type
    Description

    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

    Request Body

    Name
    Type
    Description

    Below is an example of a request to create s Slack (Oauth2) account.

    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.

    Below is another example, this time for Google (Oauth2) account with access token expiration:

    One more example, this time an API Key account:

    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

    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

    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

    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]"

    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

    Query Parameters

    Name
    Type
    Description

    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

    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

    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
      "componentType": "appmixer.slack.list.SendChannelMessage",
      "auth": {
        "accounts": {
          "5a6e21f3b266224186ac7d03": {
            "accessTokenValid": true,
            "accountId": "5a6e21f3b266224186ac7d03",
    
    [
      {
        "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": "[email protected]",
        "displayName": null,
        "service": "appmixer:pipedrive",
        "userId": "58593f07c3ee4f239dc69ff7",
        "profileInfo": {
          "name": "tomas",
          "email": "[email protected]"
        },
        "icon": "data:image/png;base64,...rkJggg==",
        "label": "Pipedrive"
      }
    ]  

    profileInfo

    object

    Can be provided directly. If not, requestProfileInfo from the authentication module will be called.

    componentType

    string

    Component Type.

    componentId

    string

    Component ID.

    filter

    string

    You can filter 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]'

    accountId

    string

    The ID of the account to update.

    string

    Human-readable name of the account.

    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.

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

    {
        "accountId": "5f841f3a43f477a9fa8fa4e9",
        "name": "[Name of the account]",
        "displayName": null,
        "service": "[vendor:service]",
        "userId": "5f804b96ea48ec47a8c444a7",
        "profileInfo": {
            
        },
        "pre": {},
        "revoked": false
    }
    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]"
        }
    }'
    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"
            ]
        }
    }'
    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]"
        }
    }'

    accountId

    string

    Account ID.

    { "5a6e21f3b266224186ac7d04": "valid" }

    accountId

    string

    Account ID.

    { "accountId": "5abcd0ddc4c335326198c1b2" }

    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"
      }
    ]
    { "ticket": "58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" }

    ticket

    string

    Authentication ticket.

    componentType

    string

    Component type.

    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"
    }

    ticket

    string

    Authentication session ticket.

    componentType

    string

    Component type.

    {
        "service": "appmixer:slack"
    }

    componentId

    string

    Component ID.

    { "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5" }

    accountId

    string

    Account ID.

    componentId

    string

    Component ID.

    {
        "accountId":"5a6e21f3b266224186ac7d03",
        "componentId":"e25dc901-f92a-46a2-8d29-2573d4ad65e5"
    }
    requestProfileInfo

    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)

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

    Variables dynamically populated at design time with available Twilio phone numbers.

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

    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 <[email protected]>",
        "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 <[email protected]>",
        "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,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"
    }
    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 <[email protected]>",
        "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 <[email protected]>",
        "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;
    };

    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

    Special selectors

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

    Special attributes

    Link attributes, see:

    link.tools

    Link attributes, see:

    "Cut" button tooltip.

    element-halo-remove-tooltip

    title

    "Remove" button tooltip.

    Key

    Description

    options.updateCallback

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

    attributes

    Element attributes, see: jointjs.dia.Cell.define

    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 attributes of joint.dia.Element.ports.interface

    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

    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

    Key

    Value

    Description

    event

    "remove"

    The entry acts as a remove button.

    var customComponentShape = {
        attributes: {
            size: {
                height: 72,
                width: 72
            },
            markup: [
                {
                    tagName: 'rect',
                    selector: 'body'
                },
                {
                    tagName: 'text',
                    selector: 'label'
                },
                {
                    tagName: 'image',
                    selector: 'icon'
                },
                {
                    tagName: 'g',
                    selector: 'element-halo',
                    children: [
                        {
                            tagName: 'image',
                            selector: 'element-halo-copy',
                            children: [
                                {
                                    tagName: 'title',
                                    selector: 'element-halo-copy-tooltip'
                                }
                            ]
                        },
                        {
                            tagName: 'image',
                            selector: 'element-halo-cut',
                            children: [
                                {
                                    tagName: 'title',
                                    selector: 'element-halo-cut-tooltip'
                                }
                            ]
                        },
                        {
                            tagName: 'image',
                            selector: 'element-halo-remove',
                            children: [
                                {
                                    tagName: 'title',
                                    selector: 'element-halo-remove-tooltip'
                                }
                            ]
                        }
                    ]
                }
            ],
            attrs: {
                body: {
                    fill: 'white',
                    stroke: 'black',
                    strokeWidth: 1,
                    refWidth: 1,
                    refHeight: 1
                },
                icon: {
                    ref: 'body',
                    refX: 0.5,
                    refY: 0.5,
                    xAlignment: 'middle',
                    yAlignment: 'middle',
                    width: 24,
                    height: 24,
                    clipPath: 'url(#icon-clip)'
                },
                label: {
                    ref: 'body',
                    textAnchor: 'middle',
                    refX: 0.5,
                    refY: '100%',
                    refY2: 12,
                    xAlignment: 'middle',
                    fontFamily: 'sans-serif',
                    fontWeight: 'bold',
                    fontSize: 14,
                    fill: 'black'
                },
                'element-halo': {
                    display: 'none',
                    ref: 'body',
                    width: 68,
                    height: 44,
                    refX: 0.5,
                    refY: 0,
                    refY2: -24,
                    xAlignment: 'middle'
                },
                'element-halo-copy': {
                    event: 'element-copy',
                    x: 0,
                    y: 0,
                    width: 20,
                    height: 20,
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii01IC0yIDI0IDI0IiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIiBjbGFzcz0iaWNvbl9faWNvbiI+PHBhdGggZD0iTTUgMnYyaDRWMkg1em02IDBoMWEyIDIgMCAwIDEgMiAydjE0YTIgMiAwIDAgMS0yIDJIMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmgxYTIgMiAwIDAgMSAyLTJoNGEyIDIgMCAwIDEgMiAyem0wIDJhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJIMnYxNGgxMFY0aC0xek00IDhoNmExIDEgMCAwIDEgMCAySDRhMSAxIDAgMSAxIDAtMnptMCA1aDZhMSAxIDAgMCAxIDAgMkg0YTEgMSAwIDAgMSAwLTJ6Ij48L3BhdGg+PC9zdmc+',
                    cursor: 'pointer'
                },
                'element-halo-cut': {
                    event: 'element-cut',
                    x: 24,
                    y: 0,
                    width: 20,
                    height: 20,
                    cursor: 'pointer',
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yLjUgLTIuNSAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiIgY2xhc3M9Imljb25fX2ljb24iPjxwYXRoIGQ9Ik05LjEwNyAxMC41NTRMNy4zNjMgMTIuNjNhMy45OTMgMy45OTMgMCAwIDEtLjE5MiA0Ljg5IDQuMDA0IDQuMDA0IDAgMCAxLTUuNjM1LjQ5MiAzLjk5MSAzLjk5MSAwIDAgMS0uNDkzLTUuNjI4IDQuMDA1IDQuMDA1IDAgMCAxIDQuNzg4LTEuMDM4TDcuODAxIDkgMi40MTMgMi41ODdhLjk5OC45OTggMCAwIDEgLjEyMy0xLjQwNyAxIDEgMCAwIDEgMS40MDkuMTIzbDUuMTYyIDYuMTQ0IDUuMTYxLTYuMTQ0YTEgMSAwIDAgMSAxLjQxLS4xMjMuOTk4Ljk5OCAwIDAgMSAuMTIzIDEuNDA3TDEwLjQxMiA5bDEuOTcgMi4zNDVhNC4wMDUgNC4wMDUgMCAwIDEgNC43ODggMS4wMzggMy45OTEgMy45OTEgMCAwIDEtLjQ5MyA1LjYyOCA0LjAwNCA0LjAwNCAwIDAgMS01LjYzNS0uNDkyIDMuOTkzIDMuOTkzIDAgMCAxLS4xOTItNC44OWwtMS43NDMtMi4wNzV6bS02LjI4NSA1LjkyN2EyIDIgMCAwIDAgMi41NzEtMy4wNiAyLjAwMiAyLjAwMiAwIDAgMC0yLjgxOC4yNDZjLS43MS44NDUtLjYgMi4xMDUuMjQ3IDIuODE0em0xMi41NyAwYTEuOTk2IDEuOTk2IDAgMCAwIC4yNDYtMi44MTQgMi4wMDIgMi4wMDIgMCAwIDAtMi44MTctLjI0NiAxLjk5NiAxLjk5NiAwIDAgMC0uMjQ3IDIuODE0Yy43MS44NDUgMS45NzIuOTU1IDIuODE4LjI0NnoiPjwvcGF0aD48L3N2Zz4'
                },
                'element-halo-remove': {
                    event: 'element-remove',
                    x: 48,
                    y: 0,
                    width: 20,
                    height: 20,
                    cursor: 'pointer',
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0zIC0yIDI0IDI0IiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIiBjbGFzcz0iaWNvbl9faWNvbiI+PHBhdGggZD0iTTYgMlYxYTEgMSAwIDAgMSAxLTFoNGExIDEgMCAwIDEgMSAxdjFoNGEyIDIgMCAwIDEgMiAydjFhMiAyIDAgMCAxLTIgMmgtLjEzM2wtLjY4IDEwLjJhMyAzIDAgMCAxLTIuOTkzIDIuOEg1LjgyNmEzIDMgMCAwIDEtMi45OTMtMi43OTZMMi4xMzcgN0gyYTIgMiAwIDAgMS0yLTJWNGEyIDIgMCAwIDEgMi0yaDR6bTEwIDJIMnYxaDE0VjR6TTQuMTQxIDdsLjY4NyAxMC4wNjhhMSAxIDAgMCAwIC45OTguOTMyaDYuMzY4YTEgMSAwIDAgMCAuOTk4LS45MzRMMTMuODYyIDdoLTkuNzJ6TTcgOGExIDEgMCAwIDEgMSAxdjdhMSAxIDAgMCAxLTIgMFY5YTEgMSAwIDAgMSAxLTF6bTQgMGExIDEgMCAwIDEgMSAxdjdhMSAxIDAgMCAxLTIgMFY5YTEgMSAwIDAgMSAxLTF6Ij48L3BhdGg+PC9zdmc+'
                },
                'element-halo-copy-tooltip': {
                    fontFamily: 'nunitosans-regular, Helvetica, Arial, sans-serif',
                    fontSize: 13
                },
                'element-halo-cut-tooltip': {
                    fontFamily: 'nunitosans-regular, Helvetica, Arial, sans-serif',
                    fontSize: 13
                },
                'element-halo-remove-tooltip': {
                    fontFamily: 'nunitosans-regular, Helvetica, Arial, sans-serif',
                    fontSize: 13
                }
            }
        },
        ports: {
            attributes: {
                in: {
                    position: {
                        name: 'left'
                    },
                    attrs: {
                        '.port-body': {
                            r: 5,
                            strokeWidth: 2,
                            stroke: 'black',
                            fill: 'white'
                        },
                        'connection-port-label': {
                            fill: 'black',
                            fontFamily: 'sans-serif',
                            fontSize: 12
                        }
                    },
                    label: {
                        position: {
                            name: 'left',
                            args: {
                                x: -8,
                                y: 14
                            }
                        },
                        markup: [
                            {
                                tagName: 'text',
                                selector: 'connection-port-label'
                            }
                        ]
                    }
                },
                out: {
                    position: {
                        name: 'right'
                    },
                    attrs: {
                        '.port-body': {
                            r: 5,
                            strokeWidth: 2,
                            stroke: 'black',
                            fill: 'white'
                        },
                        'connection-port-label': {
                            fill: 'black',
                            fontFamily: 'sans-serif',
                            fontSize: 12
                        }
                    },
                    label: {
                        position: {
                            name: 'right',
                            args: {
                                x: 8,
                                y: 14
                            }
                        },
                        markup: [
                            {
                                tagName: 'text',
                                selector: 'connection-port-label'
                            }
                        ]
                    }
                }
            }
        },
        link: {
            attributes: {
                router: {
                    name: 'metro'
                },
                connector: {
                    name: 'rounded',
                    args: {
                        radius: 8
                    }
                },
                markup: [
                    {
                        tagName: 'path',
                        selector: 'wrapper'
                    },
                    {
                        tagName: 'path',
                        selector: 'line'
                    }
                ],
                tools: [
                    {
                        z: 100,
                        distance: '50%',
                        event: 'link-remove',
                        markup: [
                            {
                                tagName: 'circle',
                                selector: 'button',
                                attributes: {
                                    cx: -0.5,
                                    cy: -0.5,
                                    r: 10,
                                    fill: config.colors.bodyStrokeActive,
                                    cursor: 'pointer'
                                }
                            },
                            {
                                tagName: 'path',
                                selector: 'icon',
                                attributes: {
                                    pointerEvents: 'none',
                                    transform: 'translate(-6.4 -7)',
                                    d: 'M8,1.333h3.333A.667.667,0,0,1,12,2V3.333A.667.667,0,0,1,11.333,4H.667A.667.667,0,0,1,0,3.333V2a.667.667,0,0,1,.667-.667H4V.667A.667.667,0,0,1,4.667,0H7.333A.667.667,0,0,1,8,.667Zm2.533,4-.409,6.133a2,2,0,0,1-2,1.867H3.884a2,2,0,0,1-2-1.864L1.47,5.333ZM4.667,6A.667.667,0,0,0,4,6.667v4.667a.667.667,0,0,0,1.333,0V6.667A.667.667,0,0,0,4.667,6ZM7.333,6a.667.667,0,0,0-.667.667v4.667a.667.667,0,0,0,1.333,0V6.667A.667.667,0,0,0,7.333,6Z',
                                    fill: 'white'
                                }
                            }
                        ]
                    }
                ],
                attrs: {
                    line: {
                        pointerEvents: 'none',
                        connection: true,
                        stroke: 'black',
                        strokeWidth: 1.5,
                        strokeDasharray: '4 4',
                        fill: 'transparent'
                    },
                    wrapper: {
                        connection: true,
                        cursor: 'pointer',
                        stroke: 'transparent',
                        strokeWidth: 24,
                        fill: 'transparent'
                    }
                }
            }
        },
        states: {
            '@active': {
                attributes: {
                    attrs: {
                        body: {
                            stroke: 'blue',
                            strokeWidth: 3
                        },
                        label: {
                            fill: 'black'
                        },
                        'element-halo': {
                            display: 'initial'
                        }
                    }
                },
                ports: {
                    attributes: {
                        in: {
                            attrs: {
                                '.port-body': {
                                    stroke: 'blue'
                                }
                            }
                        },
                        out: {
                            attrs: {
                                '.port-body': {
                                    stroke: 'blue'
                                }
                            }
                        }
                    }
                },
                link: {
                    attributes: {
                        attrs: {
                            line: {
                                strokeDasharray: 0,
                                stroke: 'blue'
                            }
                        }
                    }
                }
            },
            '@invalid': {
                attributes: {
                    attrs: {
                        body: {
                            stroke: 'red',
                            strokeWidth: 2
                        },
                        label: {
                            fill: 'red'
                        }
                    }
                },
                ports: {
                    attributes: {
                        in: {
                            attrs: {
                                '.port-body': {
                                    stroke: 'red'
                                }
                            }
                        },
                        out: {
                            attrs: {
                                '.port-body': {
                                    stroke: 'red'
                                }
                            }
                        }
                    }
                }
            }
        }
    };
    
    var customSelectionShape = {
        attributes: {
            markup: [
                {
                    tagName: 'rect',
                    selector: 'body'
                },
                {
                    tagName: 'g',
                    selector: 'halo',
                    children: [
                        {
                            tagName: 'image',
                            selector: 'halo-copy'
                        },
                        {
                            tagName: 'image',
                            selector: 'halo-cut'
                        },
                        {
                            tagName: 'image',
                            selector: 'halo-remove'
                        }
                    ]
                }
            ],
            attrs: {
                body: {
                    pointerEvents: 'none',
                    fill: 'rgba(0, 0, 0, 0.1)',
                    stroke: 'black',
                    strokeWidth: 1
                },
                halo: {
                    ref: 'body',
                    width: 68,
                    height: 44,
                    refX: 0.5,
                    refY: 0,
                    refY2: -24,
                    xAlignment: 'middle'
                },
                'halo-copy': {
                    event: 'element-copy',
                    x: 0,
                    y: 0,
                    width: 20,
                    height: 20,
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii01IC0yIDI0IDI0IiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIiBjbGFzcz0iaWNvbl9faWNvbiI+PHBhdGggZD0iTTUgMnYyaDRWMkg1em02IDBoMWEyIDIgMCAwIDEgMiAydjE0YTIgMiAwIDAgMS0yIDJIMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmgxYTIgMiAwIDAgMSAyLTJoNGEyIDIgMCAwIDEgMiAyem0wIDJhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJIMnYxNGgxMFY0aC0xek00IDhoNmExIDEgMCAwIDEgMCAySDRhMSAxIDAgMSAxIDAtMnptMCA1aDZhMSAxIDAgMCAxIDAgMkg0YTEgMSAwIDAgMSAwLTJ6Ij48L3BhdGg+PC9zdmc+',
                    cursor: 'pointer'
                },
                'halo-cut': {
                    event: 'element-cut',
                    x: 24,
                    y: 0,
                    width: 20,
                    height: 20,
                    cursor: 'pointer',
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yLjUgLTIuNSAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiIgY2xhc3M9Imljb25fX2ljb24iPjxwYXRoIGQ9Ik05LjEwNyAxMC41NTRMNy4zNjMgMTIuNjNhMy45OTMgMy45OTMgMCAwIDEtLjE5MiA0Ljg5IDQuMDA0IDQuMDA0IDAgMCAxLTUuNjM1LjQ5MiAzLjk5MSAzLjk5MSAwIDAgMS0uNDkzLTUuNjI4IDQuMDA1IDQuMDA1IDAgMCAxIDQuNzg4LTEuMDM4TDcuODAxIDkgMi40MTMgMi41ODdhLjk5OC45OTggMCAwIDEgLjEyMy0xLjQwNyAxIDEgMCAwIDEgMS40MDkuMTIzbDUuMTYyIDYuMTQ0IDUuMTYxLTYuMTQ0YTEgMSAwIDAgMSAxLjQxLS4xMjMuOTk4Ljk5OCAwIDAgMSAuMTIzIDEuNDA3TDEwLjQxMiA5bDEuOTcgMi4zNDVhNC4wMDUgNC4wMDUgMCAwIDEgNC43ODggMS4wMzggMy45OTEgMy45OTEgMCAwIDEtLjQ5MyA1LjYyOCA0LjAwNCA0LjAwNCAwIDAgMS01LjYzNS0uNDkyIDMuOTkzIDMuOTkzIDAgMCAxLS4xOTItNC44OWwtMS43NDMtMi4wNzV6bS02LjI4NSA1LjkyN2EyIDIgMCAwIDAgMi41NzEtMy4wNiAyLjAwMiAyLjAwMiAwIDAgMC0yLjgxOC4yNDZjLS43MS44NDUtLjYgMi4xMDUuMjQ3IDIuODE0em0xMi41NyAwYTEuOTk2IDEuOTk2IDAgMCAwIC4yNDYtMi44MTQgMi4wMDIgMi4wMDIgMCAwIDAtMi44MTctLjI0NiAxLjk5NiAxLjk5NiAwIDAgMC0uMjQ3IDIuODE0Yy43MS44NDUgMS45NzIuOTU1IDIuODE4LjI0NnoiPjwvcGF0aD48L3N2Zz4'
                },
                'halo-remove': {
                    event: 'element-remove',
                    x: 48,
                    y: 0,
                    width: 20,
                    height: 20,
                    cursor: 'pointer',
                    xlinkHref: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0zIC0yIDI0IDI0IiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIiBjbGFzcz0iaWNvbl9faWNvbiI+PHBhdGggZD0iTTYgMlYxYTEgMSAwIDAgMSAxLTFoNGExIDEgMCAwIDEgMSAxdjFoNGEyIDIgMCAwIDEgMiAydjFhMiAyIDAgMCAxLTIgMmgtLjEzM2wtLjY4IDEwLjJhMyAzIDAgMCAxLTIuOTkzIDIuOEg1LjgyNmEzIDMgMCAwIDEtMi45OTMtMi43OTZMMi4xMzcgN0gyYTIgMiAwIDAgMS0yLTJWNGEyIDIgMCAwIDEgMi0yaDR6bTEwIDJIMnYxaDE0VjR6TTQuMTQxIDdsLjY4NyAxMC4wNjhhMSAxIDAgMCAwIC45OTguOTMyaDYuMzY4YTEgMSAwIDAgMCAuOTk4LS45MzRMMTMuODYyIDdoLTkuNzJ6TTcgOGExIDEgMCAwIDEgMSAxdjdhMSAxIDAgMCAxLTIgMFY5YTEgMSAwIDAgMSAxLTF6bTQgMGExIDEgMCAwIDEgMSAxdjdhMSAxIDAgMCAxLTIgMFY5YTEgMSAwIDAgMSAxLTF6Ij48L3BhdGg+PC9zdmc+'
                }
            }
        }
    };
    
    new Appmixer({
        componentShapes: {
            action: customComponentShape,
            trigger: customComponentShape,
            selection: customSelectionShape        
        }
    });

    link.attributes

    title

    Manifest
    jointjs.dia.Link
    jointjs.linkTools

    Authentication

    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 four types of authentication mechanisms that are common for today's APIs: API key, Password, OAuth 1, and OAuth 2.

    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, pwd, 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 authentication mechanism. Any of apiKey, pwd, oauth and oauth2.

    definition

    The definition of the authentication mechanism is 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, in that case, it has one argument context. It is an object that contains either consumerKey, username and password, 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 was mentioned in the beginning, Appmixer authentication supports four mechanisms: API Key, Password, 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.

    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:

    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 render a form like this:

    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)

    While this field is optional, is recommended for a better UX. This field can be a function that returns a promise or object. 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. It is used together with the accountNameFromProfileInfo that will pick one property from this object.

    accountNameFromProfileInfo ()

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

    We want to use the email to identify the Freshdesk accounts in Appmixer, 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.

    validate ()

    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 the object passed into the axios library. In that case, it will look like this:

    If the validate function throws an exception and the exception contains message property, the message will be shown in the Connecting Account Failed page.

    The validate can be specified as an object and the Appmixer engine will perform such a request. In this case, the response error can vary from API to API. Appmixer will try to find the error message in the response. If the error message is not shown on the Connecting Account Failed page, then the validate response can always be parsed and a proper exception thrown in the auth.js module using the validateErrCallback function:

    Password

    The password-based authentication is almost similar to key-based authentication as explained in the above section. The only difference is that you will use username and password inputs. The auth property inside definition will need to have two inputs as shown in the below-given code snippet. The validate method is used to validate the input values provided. You can make API call by using the context.username and context.password properties. If the API you are validating returns a token, it will have to be returned from the validate method as shown below.

    HTTP Basic Authentication

    The pwd type can be used for the HTTP Basic Authentication. Here's a sample code:

    Then the username and password is available in the components:

    OAuth 1

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

    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 the definition object:

    accountNameFromProfileInfo ()

    Works exactly the same way as described in the 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 shown in 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 shown in 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 the 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.

    OAuth 2

    The latest OAuth protocol and industry-standard, OAuth 2.0 improved many things from the first version in terms of 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:

    Notice that there is no requestRequestToken method in OAuth 2, but 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's properties:

    accountNameFromProfileInfo ()

    Works exactly the same way as described in the section.

    authUrl ()

    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:

    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.

    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 the access tokens for the application. Inside this function you have access to context properties you need: clientId, clientSecret, callbackUrl and authorizationCode.

    requestProfileInfo () (optional)

    Works exactly the same way as described in the section.

    refreshAccessToken ()

    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 (and refreshToken if needed) properties, as shown in the example. You have access to context properties clientId, clientSecret, callbackUrl and refreshToken.

    validateAccessToken ()

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

    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 the Dropbox auth.js module:

    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 the Microsoft authentication module with such features:

    scope

    String or an array of strings.

    scopeDelimiter

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

    refreshBeforeExp

    By default, the Appmixer engine will try to refresh the access token five minutes before its expiration. This is fine for most of the OAuth2 implementations, where an access token is usually valid for hours or days. However, there are OAuth2 implementations with stricter rules. With this property, you can define how many minutes (default, see refreshBeforeExpUnits) before the access token expiration should Appmixer refresh the token. Appmixer will not try to refresh the token before this value.

    refreshBeforeExpUnits

    Works in cooperation with the refreshBeforeExp property. Useful if you need to go down to seconds. See the example above.

    OAuth 2 redirect URI

    When you're developing an OAuth 2 application, at some point you have to register an app in the 3rd party system. For that, you need the redirect URI that points to the Appmixer API. The format of the redirect URI is https://[appmixer-url]/auth/[service]/callback.

    So if the service you're developing is called myService then the redirect URI will be https://[appmixer-url]/auth/myService/callback.

    Context

    Context properties are different for each authentication type. But some of them are common for all types.

    async context.httpRequest

    Wrapper around the library.

    Custom "Connect account" button

    There is a possibility to redefine the Connect Account button in the Designer and Integration Wizards.

    This is done with an optional connecAccountButton: { image: 'data uri' } property. Example from the Google auth.js:

    Setting OAuth 1,2 secrets

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

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

    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.

    // or any library you want to perform API requests
    const request = require('request-promise');
    
    module.exports = {
    
        // there will be some other properties based on the authentication 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' (https://www.npmjs.com/package/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}}'
            // if you define 'requestAccessToken' property in an Oauth2 authentication
            // module with a string that contains a URL, then a POST request will be \
            // sent to that URL. In other words, what will happen, when a property 
            // is defined as a string, depending on the context (Oauth vs ApiKey ...).
        }
    }
    
    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 context.httpRequest({
                    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 context.httpRequest({
                    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;
            }
        }
    };
    {
        contact: {
            email: '[email protected]',
            name: 'Appmixer example',
            // More properties here...
        }
        // There can be more properties here as well...
    }
    '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.'
                },
                cert: {
                    // just to show, that a 'textarea' input is supported as well
                    type: 'textarea',
                    name: 'TLS CA',
                    tooltip: 'Paste text content of <code>.crt</code> file' 
                }
            },
    
            // In the validate request we need the appId and apiKey specified by the 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}}'
                }
            }
        }
    };
    
    validate: {
        'method': 'GET',
        'uri': 'https://api.apify.com/v2/users/{{apiUserId}}?token={{apiToken}}'
    },
    
     validateErrCallback: err => {
    
        if (err?.someNestedObject?.message) {
            throw new Error(err.someNestedObject.message);  // message for the UI
        }
        throw err;
    }
    'use strict';
    
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            // As opposed to the 'apiKey' module, the 'auth' property inside the 
            // definition object is optional for the 'pwd' type. If omitted, it will 
            // have the following structure. If you want to change name of the field,
            // change the content of the tooltip or add another field, you can do
            // that here. 
            auth: {
                username: {
                    type: 'text',
                    name: 'Username',
                    tooltip: 'Username'
                },
                password: {
                    type: 'password',
                    name: 'Password',
                    tooltip: 'Password'
                }
            },
    
            // the only mandatory property for the 'pwd' authentication type
            validate: async context => {
            
                const { username, password } = context
                const { data } = await context.httpRequest.post(
                    // the URL for the username/password authentication
                    'https://service-server-api-url/auth',
                    { username, password }
                );
                
                // verify authentication works
                if (!data.isValid) {
                    throw new Error("Invalid username/password combination.");
                }
                
                // if the API does not return a token for exchange, then return here.
                // in such case, the components using this authentication module
                // will have to use the username/password to perform each request.
                // There will be properties context.auth.username and 
                // context.auth.password available in the context object in a component.
                
                // if the API returns a token and expiration then return them using 
                // the following way - object with 'token' and 'expires' properties.
                const { token, expiresIn } = data;
                
                // the token and expiration (if provided) will be available in a
                // component's context object under context.auth.token and
                // context.auth.expires
                return {
                    token,
                    // expires can be a Date - token expiration date (timestamp),
                    // or number/string with seconds representing a token lifetime.
                    // Appmixer will use this to request a new token (using stored
                    // username and password) when the current one is about to expire
                    expires: expiresIn
                };
                
                // if the API returns a token that does not expire, just return
                // that token 
                // return { token };
            }
        }
    };
    
    'use strict';
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            validate: async context => {
    
                await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                    auth: {
                        username: context.username,
                        password: context.password
                    }
                });
            }
        }
    }
    'use strict';
    module.exports = {
    
        async start(context) {
    
            await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                auth: {
                    username: context.auth.username,
                    password: context.auth.password
                }
            });
        }
    }
    '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.');
                    });
                }
            };
        }
    };
    
    '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,
                            // Some services return a new refresh token, if so, it can
                            // be returned here and Appmixer will replace the old one.
                            refreshToken: result['refresh_token'] 
                        };
                    });
                },
    
                validateAccessToken: {
                    method: 'GET',
                    url: 'https://app.asana.com/api/1.0/users/me',
                    auth: {
                        bearer: '{{accessToken}}'
                    }
                }
            };
        }
    };
    
    authUrl: 'https://www.dropbox.com/oauth2/authorize'
    '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}}'
                }
            }
        }
    };
    
    '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}}'
                }
            }
        }
    };
    
    module.exports = {
    
        type: 'oauth2',
    
        definition: () => {
    
            // telling Appmixer it cannot refresh it sooner than 30 seconds 
            // before access token expiration
            refreshBeforeExp: 30,
            refreshBeforeExpUnits: 'seconds',   // 'minutes' by default
            
            // the rest is business as usual
            authUrl: 'https://www.acme.com/oaut2/authorize'
            
            ...
        }
    }
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            // the only mandatory property for the 'pwd' authentication type
            validate: async context => {
            
                const { username, password } = context;
                const { data } = await context.httpRequest.post(
                    // the URL for the username/password authentication
                    'https://service-server-api-url/auth',
                    { username, password }
                );
                
                // verify authentication works
                if (!data.isValid) {
                    throw new Error("Invalid username/password combination.");
                }            
                return true;
            }
        }
    };
    'use strict';
    const GoogleApi = require('googleapis');
    const Promise = require('bluebird');
    
    module.exports = {
    
        type: 'oauth2',
    
        definition: initData => {
    
            return {
    
                // The auth.js 'definition' object can have an optional 
                // 'connectAccountButton' property, which has to contain the 'image'.
                connectAccountButton: { image: 'data:image/png;base64,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' },
    
                scope: ['profile', 'email'],
    
                accountNameFromProfileInfo: function(context) {
    
                    return context.profileInfo.email;
                },
                
        ...
    }
    
    context
    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 or object
    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
    axios
    Backoffice
    Authentication form for Freshdesk, defined by auth object
    Custom "Connect account" button in the Designer.
    Custom "Connect account" button in the Wizard.
    credentials.json

    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.

    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
            });
        }
    };

    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.

    async context.sendArray(arrayOfObjects, outPort, options)

    A method for sending an array of objects to an output port.

    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:

    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:

    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: { persistent: 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.

    async context.stateAddToSet(key, value)

    Add value into set under key.

    async context.stateRemoveFromSet(key, value)

    Remove value from set under key.

    async context.stateInc(key, value = 1, returnOriginal = false)

    Increment value under key. The second parameter is optional and can be used to set the increment value. The function return by default the new value (after incremented), if returnOriginal is set to true, it will return the value before the increment.

    Flow State

    Similar to the component state, this state is available to all components in the flow.

    async context.flow.loadState()

    Load the state from the DB.

    async context.flow.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.flow.stateGet(key)

    Get a state value stored under key.

    async context.flow.stateUnset(key)

    Remove a value under key.

    async context.flow.stateClear()

    Clears the entire state.

    async context.flow.stateAddToSet(key, value)

    Add value into a Set stored under key.

    async context.flow.stateRemoveFromSet(key, value)

    Remove value from Set stored under key.

    async context.flow.stateInc(key, value = 1, returnOriginal = false)

    Increment value under key. The second parameter is optional and can be used to set the increment value. The function return by default the new value (after incremented), if returnOriginal is set to true, it will return the value before the increment.

    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.

    async context.service.stateInc(key, value = 1, returnOriginal = false)

    Increment value under key. The second parameter is optional and can be used to set the increment value. The function return by default the new value (after incremented), if returnOriginal is set to true, it will return the value before the increment.

    Files

    async context.saveFile(fileName, mimeType, buffer)

    This method has been deprecated. Use context.saveFileStream instead. Save a file to the Appmixer file storage. This function returns a promise that when resolved gives you a 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 ({ fileId }). This is a more efficient and recommended version of context.saveFile(name, mimeType, buffer).

    Return object:

    async context.replaceFileStream(fileId, stream)

    Replaces the content of the file. Returns a Promise with the ID of the file { fileId }. The fileId remains the same.

    Return object:

    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.

    HTTP

    async context.httpRequest

    Components do often trigger HTTP requests. You don't have to install any library for that, you can use context.httpRequest. It is a wrapper around the library.

    Store

    async context.store.listStores()

    Get the list of user's Data Stores.

    async context.store.get(storeId, key)

    Get value from the Data Store, stored under the key.

    async context.store.set(storeId, key, value)

    Set value to the Data Store under the key.

    async context.store.remove(storeId, key)

    Remove the key from the Data Store.

    async context.store.clear(storeId)

    Clear all data from the Data Store.

    async context.store.find(storeId, query)

    Find items in the Data Store.

    async context.store.getCursor(storeId, query, options)

    Get cursor.

    async context.store.registerWebhook(storeId, events = undefined)

    Register Data Store webhook. If no events are specified, then the component will get all events from the Data Store. Possible events are insert, update and delete.

    And the same functionality with registering only for the insert events.

    async context.store.unregisterWebhook(storeId);

    Unregister webhook.

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

    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.

    The return value from this context method is a timeout Id (a UUID string). Each timeout has its own unique identifier. That can be used to clear that timeout.

    async context.clearTimeout(timeoutId)

    Will clear (cancel) scheduled timeout.

    async context.callAppmixer(request)

    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:

    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

    Flow property is available here.

    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)

    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.

    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.

    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.

    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 Promise = require('bluebird');
    
    module.exports = {
        async receive(context) {
     
            // this is the old way, without context.sendArray function
            const arrayOfObjects = [
                { name: 'John', surname: 'Doe' },
                { name: 'Martin', surname: 'Tester' }
            ];   
            await Promise.map(arrayOfObjects, item => {
                return context.sendJson(item, 'out');
            });
            
            // and the new way
            await context.sendArray(arrayOfObjects, 'out');
            // by default it will be sending 5 items in parallel to the
            // output port. That default amount depends on Appmixer
            // configuration
            
            // the concurrency can be changed
            await context.sendArray(arrayOfObjects, 'out', {
                // sending one by one
                concurrency: 1  
            });
        }
    }
    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');
                });
        }
    }
    // Example
    {
      length: 7,
      chunkSize: 261120,
      uploadDate: "2023-03-22T10:34:07.751Z",
      filename: "test",
      md5: "3e47b75000b0924b6c9ba5759a7cf15d",
      metadata: {
        userId: "638f734271b34799e665c9b9",
        fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2",
        originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
      },
      fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2"
    }
    // Example
    {
      length: 7,
      chunkSize: 261120,
      uploadDate: "2023-03-22T10:34:07.765Z",
      filename: "something.txt",
      md5: "3e47b75000b0924b6c9ba5759a7cf15d",
      metadata: {
        userId: "638f734271b34799e665c9b9",
        fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030",
        originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
      },
      fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030"
    }
    'use strict';
    
    module.exports = {
    
        receive(context) {
    
            const { fileId, content } = context.messages.in.content;
            return context.replaceFileStream(
                fileId,
                content
            ).then(savedFile => {
                return context.sendJson(savedFile, '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.

    module.exports = {
      async receive(context) {
        const { data } = await context.httpRequest({
          url: "https://some-url.com/api",
          method: "POST",
          data: {
            username: "someuser",
            password: "somepass",
          },
          headers: { Authentication: "some"}        
        });
            
        // it is also possible to execute dedicated http methods
        // see here https://axios-http.com/docs/api_intro
        // for example
        // context.httpRequest.get(url[, config])
        const { data } = await context.httpRequest.get("https://url", { params: { name: "some" }, headers: {}});
        // context.httpRequest.post(url[, data[, config]])
        const { data } = await context.httpRequest.post("https://url", { username: "some" }, headers: {});
            
        return context.response(data);
      }
    }
    await context.store.find(storeId, { query: { key: { $nin: rowIds } } })
    module.exports = {
    
        async receive(context) {
    
            // whenever there is an item added/updated/removed from the data
            // store, this component will get triggered with data on the
            // context.messages.webhook.content
            const data = context.messages.webhook.content.data.currentValue;
    
            if (context.messages.webhook.content.data.type === 'insert') {
                await context.sendJson({
                    key: data.key,
                    storeId: data.storeId,
                    value: data.value,
                    updatedAt: data.updatedAt,
                    createdAt: data.createdAt
                }, 'item');
            }
            return context.response('ok');
        },
    
        async start(context) {
    
            // register without specifying 'events'.
            await context.store.registerWebhook(context.properties.storeId);
        },
    
        async stop(context) {
    
            await context.store.unregisterWebhook(context.properties.storeId);
        }
    };
    module.exports = {
    
        async receive(context) {
    
            const data = context.messages.webhook.content.data.currentValue;
            await context.sendJson({
                key: data.key,
                storeId: data.storeId,
                value: data.value,
                updatedAt: data.updatedAt,
                createdAt: data.createdAt
            }, 'item');
            return context.response('ok');
        },
    
        async start(context) {
    
            // register only the 'insert' events on the data store.
            await context.store.registerWebhook(context.properties.storeId, ['insert']);
        },
    
        async stop(context) {
    
            await context.store.unregisterWebhook(context.properties.storeId);
        }
    };
    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: '[email protected]',
            approver: '[email protected]',
            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;
            }
        }
    };
    
    https://openweathermap.org/api
    axios
    API section
    customFields
    unprocessedMessages
    Variables

    correlationId