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

6.0

Loading...

Getting Started

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

Building Connectors

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

Loading...

Appmixer UI SDK

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Customizing Embedded UI

Loading...

Loading...

Loading...

Appmixer Backoffice

Loading...

Loading...

Appmixer CLI

Loading...

Loading...

Appmixer Self-Managed

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Monitor & Troubleshoot

Being equipped to troubleshoot your customers' integrations is crucial, as there are myriad reasons why integrations might encounter issues. The most frequent problems stem from integration misconfigurations, your end-users revoking previously granted permissions, alterations in third-party apps leading to the use of outdated data, service outages, and various other factors. When your customers encounter difficulties, it's essential to have the necessary tools to investigate and understand the causes of these issues. This tutorial is designed to provide you with the knowledge and resources needed for effective troubleshooting.

Look up your users

To access information on your end-users, navigate to the "Admin" interface within the Appmixer Studio and proceed to the "Users" page. Please note, you must possess admin user privileges to access the "Admin" interface:

Inspect Flows (integrations or automations)

After locating the flow you intend to troubleshoot, proceed to examine the flow's logs and configuration. Click on the "Logs" link to view the flow's logs, and utilize the "Flow" link to inspect the flow's configuration in the Appmixer Designer.

As an Appmixer admin user, you have the capability to directly make minor adjustments to a flow from within the designer. However, it's important to recognize the boundaries of such modifications. For instance, re-authenticating to the applications within the flow is not possible, as authentication was initially performed by the end-user. Additionally, altering the flow's structure is discouraged, as it may necessitate new inputs from the end-user. Focus on implementing small, necessary corrections, and guide the end-user to undertake more significant changes by re-configuring their integrations according to your instructions.

Build and Run an Automation

This tutorial is designed to walk you through the process of building your first automation. Whereas integrations consist of predefined workflows that end-users can easily activate via a web form, automations are a powerful tool for enhancing your internal business operations. They allow you to link applications and services used within your organization or to launch bespoke business logic that meets your unique requirements.

Automation Overview

In this tutorial, we'll construct an automation designed to gather customer feedback via Typeform, relay each piece of feedback as a Slack notification, and automatically generate a GitHub issue based on the customer's response to the question: "What improvements or additional features would you like to see in future updates?". Utilizing OpenAI's ChatGPT, the generated ticket will be structured as a comprehensive user story, encompassing title, role, goal, reason, acceptance criteria, and test case.

Let's delve into the specifics of the automation flow:

Here's the first piece of customer feedback collected through our Typeform:

As a consequence, we received a notification in Slack:

Furthermore, a new GitHub Issue was created containing this content:

A Step-by-step Guide

The upcoming sections will walk you through the steps required to build the aforementioned automation from start to finish.

Select a trigger

Every automation begins with a trigger, which initiates the automation flow. Typically, triggers can capture data from external sources—like a Webhook trigger or a google.email.NewEmail trigger—or they can be based on time events, such as a Scheduler or Timer. This setup allows you to program your automations to run at predetermined times, for instance, "every day" or "each week on Monday at 2pm". A unique trigger type is the "OnStart" trigger, which activates immediately when your automation flow is started. This is particularly useful for batch operations that you intend to run only once or for debugging purposes.

To start building an automation, go to the Automations page and select "Create Automation". This will lead you to the Automation Designer. The first item you'll encounter there is the Trigger Selector. Look up "Typeform" and choose the "NewEntry" trigger.

In the Configuration Inspector on the right, authenticate with Typeform and select one of your forms:

Add Actions

Drag the Slack module from the left panel and choose the "SendChannelMessage" action in the Inspector panel on the right. Authenticate with your Slack account and select a Slack channel to direct your notifications:

Click the "+" button next to the Slack message field to include data placeholders for each Typeform question. These placeholders will be substituted with real data once the automation executes and retrieves customer feedback from Typeform:

Similarly, add the "OpenAI.CreateChatCompletion" action. Configure the model, set the response type to json_object, and define the prompt as follows:

The goal is to have ChatGPT generate a JSON comprising two fields: "title" and "content." This JSON will then be seamlessly integrated into the "Github.CreateIssue" action. The structure of our output JSON should resemble the following:

{
    "title": CHATGPT_INFERED_USER_STORY_TITLE,
    "content": CHATGPT_INFERED_USER_STORY_DESCRIPTION_IN_MARKDOWN
}

Create new variables and transform data with Modifiers

When your automation flow includes a component that outputs data you need to alter and then utilize the modified data across various configuration fields, employing the Control.SetVariable component proves beneficial. This component enables you to define custom variables, which can subsequently be referenced by name in other linked components. This approach is particularly advantageous when you aim to use transformed data in several locations, as it obviates the need to replicate the same modifications for a single variable across multiple fields.

In our scenario, we intend to capture the output of the ChatGPT completion (ChatGPT's response) in a variable named result. This allows us to conveniently reuse this data for both the title and description fields when we're ready to create our GitHub issue.

Given our interest in only the first choice, and considering that the message content of this choice is a JSON string (as specified in our prompt to ChatGPT), we need to manipulate the Choices variable to extract our desired JSON. To transform data within Appmixer, simply click on a variable to access the Modifiers panel and then sequentially apply the necessary modifiers, akin to how formulas are utilized in an Excel sheet. In our case, we will employ the sequence of modifiers: First Item -> JSON Path -> Parse. This sequence will allow us to select the first item from the Choices list, extract the message.content from this item, and finally parse the message.content into a JSON object for later use. It is crucial to note that ChatGPT outputs a JSON string, not a JSON object, necessitating the Parse modifier to convert the text into structured data.

Finalize our Workflow Automation

The final step involves creating a GitHub Issue using the title and content (in Markdown) generated by our AI. Navigate through the interface to locate the GitHub connector, then drag and drop it onto the canvas. Select the "CreateIssue" action and assign response.title and response.content (the JSON fields generated by ChatGPT) to the corresponding fields within the "Github.CreateIssue" action. As an additional step, choose the "enhancement" label from the selection box to categorize our GitHub issues as enhancements.

Start and Monitor Your Automation

Your automation is now set up and ready to be launched. Begin by giving it a distinctive name; this can be done by double-clicking on the default "New flow" title and renaming it to something more descriptive, like "Track Customer Feature Requests". To activate your automation, click the "Start flow" button located in the top right corner. You'll notice the log panel opens at the bottom displaying the initial log entry: "Flow started". As customer feedback starts coming in through our Typeform web form, the log panel will populate with entries detailing the actions taken within the flow. These logs provide insights into each component's input, the data transmitted between components, and the output generated, offering a comprehensive view of the automation's operation:

Clicking any log entry will reveal a panel that offers additional details.

Additionally, you can visit the Insights page to access enhanced filtering options and gain improved visibility into your logs. For instance, if you're specifically interested in logs pertaining to the CreateIssue component, you can utilize the Flows selector in the left panel. This allows you to select and view logs exclusively for the component of interest.

This action will produce a filtered list of logs:

Additionally, the Time range picker allows you to choose any date interval you're interested in for viewing the logs.

Lastly, you can search for specific strings using the Search filter to find particular entries in the logs.

In the "Users" section, you'll find a comprehensive list of all users within the system. The interface allows you to search for users based on multiple criteria, with email being the most common search parameter. Upon locating a user, you can click on the Flows icon () to view all the flows associated with that user, including integration instances and automations.

Access Appmixer REST API

Appmixer offers a REST API that mirrors the functionalities accessible through its UI. This API enables advanced customizations, additional automations, and scripting capabilities for enhanced integration flexibility.

The base URL for the Appmixer REST API varies according to your specific Appmixer tenant. The URL format is as follows:

https://api.YOUR_TENANT.appmixer.cloud

The majority of the Appmixer API endpoints require authentication using an access token associated with an Appmixer user account. Certain endpoints may require that this user to has the admin scope; specific requirements are detailed at each endpoint's documentation. To obtain a user's access token, sign in using the user's credentials:

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

On success, Appmixer returns a JSON object structured as follows:

{
    "user": {
        "id": "5c88c7cc04a917256c726c3d",
        "username":"abc@example.com",
        "email": "abc@example.com"
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cC..."
}

Use the token in the Authorization header to authenticate your user to the Appmixer API. For example:

$ curl "https://api.YOUR_TENANT.appmixer.cloud/flows" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cC..."

Find the full documentation to the REST API .

here

Connector Configuration

Appmixer includes a variety of ready-to-use connectors, with some being immediately operational while others necessitate user authentication with third-party systems (such as Slack, Gmail, Teams, and many others). This authentication process typically employs the OAuth 2 protocol.

Appmixer features an internal Authentication Hub, functioning as an authentication proxy, which simplifies the setup process by eliminating the need to register your own OAuth credentials with third-party services. This means all OAuth-based connectors offered by Appmixer are ready to use right out of the box. However, should you prefer to use your own OAuth credentials for enhanced customization or compliance reasons, you have the flexibility to do so. This can be done for all or selected connectors by specifying your OAuth credentials (clientId and clientSecret) as outlined below.

Custom OAuth Credentials

For detailed information on configuring specific connectors, please consult the App Registration section of our documentation. This section will guide you through the necessary steps and provide insights on setting up each connector according to your needs.

Appmixer offers several methods for configuring the OAuth credentials, with the Appmixer Backoffice being the simplest. For instance, if you're looking to use Slack components, you'll first need to register your application on the Slack developer portal, where you'll receive a clientId and clientSecret. After obtaining these, you can save them into Appmixer as follows:

For the Configuration ID, use appmixer:slack. You are now ready to add the Slack OAuth app clientId and clientSecret.

Add a key named clientId (it's crucial to use exactly clientId, not clientID or any other variation) and assign it the clientId you received from Slack.

Following that, add the clientSecret you received from Slack.

Once you've added the clientId and clientSecret, you're all set to utilize the Slack components within Appmixer.

It's possible to include any key/value pairs in this configuration. These pairs will be accessible within your component's code via the context.config object (or context.auth), and in the case of auth.js files, directly within the context object. This method proves particularly beneficial for API key-based authentications in your custom connectors when you prefer not to present your end-users with a form to enter their own API key. Instead, opting to use a single, global API key for all users allows them to freely use the connector without the necessity for individual authentication. This approach enhances user convenience by simplifying access and usage of the connector, ensuring a seamless experience without compromising on security or functionality. A prime example is the DeepAI component. For utilizing the Deep AI API, an API key is required. However, instead of having your users supply this API key individually, you might prefer to use a single API key across all users for simplicity and ease of management. This setup allows for a more streamlined user experience, with further details available in the documentation.

Additionally, for components using OAuth2 authentication, you have the option to redefine the default callbackUrl (redirect URL). This flexibility allows you to customize the authentication process to better fit your application's workflow.

Domain Verification

In scenarios where third-party developer consoles require the verification of domain ownership for domains used as callback URLs in OAuth-registered applications, you might face challenges (for example with configurations like the Google OAuth app). Given that your Appmixer tenant operates under a domain such as api.YOUR_TENANT.appmixer.cloud and you lack access to this domain's DNS settings, your options for domain verification can seem limited.

To upload a public file to the root location, navigate to the Appmixer Backoffice interface and select the "System -> Public Files" option from the left menu. On this page, you'll find the functionality to upload your files.

Once you've uploaded your files through the Appmixer Backoffice, they will be accessible via your Appmixer Tenant API URL. For instance, if you upload a file named google52658022a92d779c.html, it can be accessed at https://api.YOUR_TENANT.appmixer.cloud/google52658022a92d779c.html as well as at the well-known directory URL https://api.YOUR_TENANT.appmixer.cloud/.well-known/google52658022a92d779c.html. This accessibility ensures that third-party services can easily verify domain ownership by locating the specified file at your domain's root or the well-known directory.

For customers managing their own Appmixer installations, there's flexibility to adjust where these public files are hosted through the PUBLIC_FILES_PREFIX system configuration option. This setting allows you to tailor the file access paths to suit your infrastructure requirements, providing further control over how you manage domain verification and public file accessibility.

The Connector Configuration is available via the interface:

To facilitate this process, the includes a tool designed specifically for domain verification purposes. This tool enables you to upload a file that the third-party service has generated for verification. Once uploaded, this file becomes accessible at the root of your domain. The third-party service verifies domain ownership by checking for the presence of this file at the specified root location on your domain, thus confirming that you indeed control the domain. This public file method provides a straightforward solution for verifying your Appmixer tenant domain without the need for DNS access.

Appmixer Backoffice
Appmixer Backoffice

Use App Events

App Events provide the simplest method for sending data to Appmixer. To utilize App Events, select the OnAppEvent trigger from the Utilities category of connectors:

Note that another convenient way to trigger your automations is by using the Webhook component from the HTTP module. However, the Webhook component may not be as effective for building integration templates. This is because when integrations are activated by end-users, the Webhook component generates a new URL each time since each activated integration is a new instance derived from the template. Consequently, the Webhook URL displayed in the inspector panel (configuration panel) during template creation will not match the URLs in the actual integration instances.

In contrast, the OnAppEvent trigger creates a named webhook, allowing you to target a specific user with specific events and data by calling it with the actual end-user access token.

In summary, if you only care about automations where the webhook URL is known, you can use Webhook. If you build integration templates, use OnAppEvent instead.

Assign a meaningful name to your app event and consider providing sample data. Although including sample data is optional, it facilitates referencing the data from the app event in subsequent components.

Providing an Event Data Example populates all detected JSON fields as variables in subsequent connected components. This allows you to easily use and reference these data placeholders throughout your integration, ensuring that the necessary information is accessible and can be dynamically incorporated into various parts of your workflow. This practice enhances the configurability and functionality of your automations by clearly mapping out how data flows between components.

Triggering OnAppEvent using Appmixer JavaScript UI SDK

Once your automation or integration templates are built, published, and users begin activating your integrations, you can trigger app events by using the appmixer.api.sendAppEvent(EVENT, DATA) function provided by the client-side Appmixer SDK. This function enables you to programmatically send events and associated data through your application, facilitating real-time interaction and response within your integrations.

appmixer.api.sendAppEvent('contact-created', {
    email: 'david@example.com',
    fname: 'David',
    lname: 'Doe'
});

This triggers all the integrations or automations associated with the user who is authenticated with the access token used in the SDK, specifically targeting those that begin with the OnAppEvent configured with the Event Name set to contact-created.

Triggering OnAppEvent using HTTP requests

Alternatively, you can trigger the OnAppEvent from either client-side or backend-side code by sending an HTTP POST request to the endpoint https://YOUR_APPMIXER_TENANT_API_URL/plugins/appmixer/utils/appevents/events/EVENT. When making this request, include the event data in the payload. This method allows you to directly interact with the Appmixer system via HTTP, providing flexibility to trigger events from various parts of your application infrastructure.

curl -XPOST \
           -H 'Content-Type: application/json' \
           -H "Authorization: Bearer VIRTUAL_USER_ACCESS_TOKEN" \
           -d '{ "email": "david@example.com", "fname": "David", "lname": "Doe" }' \
           "https://APPMIXER_TENANT_API_URL/plugins/appmixer/utils/appevents/events/contact-created"

If you don't have the access token for a virtual user, you can obtain it by using their username and password to call the . This process involves submitting the necessary credentials to authenticate the user, after which the endpoint will provide an access token. This token can then be used to authorize subsequent actions or API calls under that user's identity.

End User Guide

Knowledge base

Embed into Your Application

You can natively embed Appmixer UI widgets into your application using the Appmixer UI JavaScript SDK. With just a few lines of code, you gain the ability to integrate not only the integration marketplace but also the fully-featured drag-and-drop automation designer. All widgets are customizable with your theme to align with your app's branding.

Install the Appmixer SDK

To incorporate the Appmixer SDK into your web page, follow these steps:

Initialize the Appmixer SDK by using the Appmixer API URL provided during the sign-up process. For hosted tenants, the URL follows the pattern: https://api.YOUR_TENANT.appmixer.cloud.

Authenticate your end-users

Before presenting integrations to your end-users, it's necessary to first identify them. Achieve this by dynamically creating "Appmixer virtual users" in the background. These virtual users should be linked with the users in your own user management system.

The username and usertoken serve as credentials to authenticate Appmixer virtual users. It is advisable to utilize an email address (which can be fictional) as the username and a secret token as the usertoken. For instance, you might use MY_USER_ID_IN_MY_APP@yourappdomain.com as the email format. Appmixer does not send emails autonomously to virtual users.

To create a usertoken, consider using the function provided below:

Please ensure that you securely store these credentials alongside your user records associated with the Appmixer virtual users. This practice enables seamless authentication of your users with Appmixer upon their subsequent visits to your application.

Embed Integration Marketplace

Finally, you can embed your integration marketplace to your application. Utilize the ui.Integrations and ui.Wizard widgets offered by the Appmixer SDK to display the list of available and user activated integrations together with the Wizard web form, facilitating end-users in setting up new integrations or modifying existing ones:

Embed Automation Designer

Embedding the Integration Marketplace into your application is just the beginning. You can also incorporate the fully-featured drag-and-drop Automation Designer, granting your end-users unparalleled flexibility in integration and automation. Typically, when integrating the ui.Designer, it's beneficial to include the ui.FlowManager as well. This UI widget presents a list of automations previously created by your users, complete with capabilities for filtering, searching, and managing these automations.

Custom Strings and Localization

The text within any UI widget embedded via the Appmixer SDK can be customized or translated into different languages. This is achieved by utilizing the strings object, which can be directly passed to the Appmixer SDK constructor or configured using the set('strings', CHANGES) method:

Custom Theme

The Appmixer SDK offers a straightforward method to customize colors, fonts, and other visual attributes of the rendered UIs. To apply your custom theme, pass your theme object to the Appmixer SDK constructor, or configure it by using the set('theme', YOUR_THEME) method:

Demo Applications

For additional code examples on integrating Appmixer into your application, please visit our public repositories:

Please visit the for end-user tutorials.

Note that YOUR_TENANTis the name of your tenant. For example, if the URL of your hosted version of Appmixer is , then YOUR_TENANTis eminent-emu-12345.

Please refer to our page for additional methods to integrate the Appmixer SDK into your page

Although creating virtual users directly from client-side code is feasible, we recommend enhancing the security of your Appmixer tenant by configuring the API_USER_CREATE_SCOPE system setting to the "admin" string. This ensures that only admin users have the capability to add new users. To create virtual users, you should use the POST /user API endpoint, accompanied by an access token from an Appmixer admin user, from your backend application code. For further details, please consult our documentation at .

For more information on customizing text strings and a comprehensive JSON object listing all the text placeholders available for customization, please refer to the page in our documentation.

For more detailed information, please refer to the page in the documentation.

Appmixer Knowledge base
<script src="https://my.YOUR_TENANT.appmixer.cloud/appmixer/appmixer.js"></script>
const appmixer = new Appmixer({ baseUrl: 'https://api.YOUR_TENANT.appmixer.cloud' });
let auth;
try {
    auth = await appmixer.api.authenticateUser(username, usertoken);
    appmixer.set('accessToken', auth.token);
} catch (err) {
    if (err.response && err.response.status === 403) {
        // Virtual user not yet created in Appmixer. Create one.
        try {
            auth = await appmixer.api.signupUser(username, usertoken);
            appmixer.set('accessToken', auth.token);
        } catch (err) {
            alert('Something went wrong creating a virtual user. ' + err.message);
        }
    } else {
        alert('Something went wrong authenticating a virtual user. '+ err.message);
    }
}
function generateSecureUsertoken(length = 22) {
  const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  return Array.from(crypto.getRandomValues(new Uint32Array(length)))
    .map((x) => charset[x % charset.length])
    .join('');
}

generateSecureUsertoken()  // ODQMwnwGeZQeXTV5sj3AsR
<div id="integrations-placeholder"></div>
const integrations = appmixer.ui.Integrations({
    el: '#integrations-placeholder',
    options: {
        showHeader: true
    }
});
const wizard = appmixer.ui.Wizard();

integrations.on('integration:create', templateId => {
    wizard.close();
    wizard.set('flowId', templateId);
    wizard.open();
});
integrations.on('integration:edit', integrationId => {
    wizard.close();
    wizard.set('flowId', integrationId);
    wizard.open();
});
wizard.on('flow:start-after', () => integrations.reload());
wizard.on('flow:remove-after', () => {
    integrations.reload();
    wizard.close();
});

integrations.open();
<div id="flow-manager-placeholder"></div>
<div id="designer-placeholder"></div>
const automations = appmixer.ui.FlowManager({
    el: '#flow-manager-placeholder',
    options: {
        menu: [{ event: 'flow:remove', label: 'Remove' }]
    }
});
const designer = appmixer.ui.Designer({
    el: '#designer-placeholder',
    options: {
        showButtonHome: true,
        menu: [
            { event: 'flow:rename', label: 'Rename' }
        ],
        toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['logs']
        ]
    }
});

automations.on('flow:open', flowId => {
   designer.close();
   designer.set('flowId', flowId);
   designer.open();
});
designer.on('navigate:flows', () => {
    designer.close();
    automations.reload();
});

automations.open();
appmixer.set('strings', {
    ui: {
        flowManager: {
            search: 'Search Automations',
            header: {
                title: 'Automations',
                buttonCreateFlow: 'Create Automation'
            }
        }
    }
});
appmixer.set('theme', {
    mode: 'light',
    ui: {
        shapes: {
            action: 'action-vertical',
            trigger: 'trigger-vertical'
        }
    },
    variables: {
        font: {
            family: 'serif',
            familyMono: 'monospace',
            size: 16
        },
        colors: {
            background: '#FFFFFF',
            surface: '#FFFDFC',
            separator: '#493843',
            neutral: '#493843',
            primary: '#493843',
            secondary: '#61988E',
            tertiary: '#EABDA8',
            error: '#B3261E',
            warning: '#B56C09',
            success: '#08B685',
            modifier: '#C558CF',
            highlighter: '#FFA500'
        },
        corners: {
            elementRadiusSmall: '0px',
            elementRadiusMedium: '0px',
            elementRadiusLarge: '0px',
            containerRadiusSmall: '0px',
            containerRadiusMedium: '0px',
            containerRadiusLarge: '0px'
        },
        dividers: {
            regular: '2px',
            medium: '4px',
            semibold: '6px',
            bold: '6px',
            extrabold: '9px'
        }
    }
});
Installation
Appmixer Virtual Users
Custom Strings
Custom Theme
Default - not yet themed - embedded Integration Marketplace
Appmixer Designer Embedded in your SaaS product
Appmixer Flow Manager Embedded in your SaaS product
Changed UI Strings in embedded Flow Manager
Select a trigger in your Automation
Authenticate and configure your trigger

Apps

This set of endpoints control the connectors that are installed in your Appmixer tenant. You can also publish new and uninstall existing connectors.

Get Apps

GET https://api.YOUR_TENANT.appmixer.cloud/apps

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

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

Get App Components

GET https://api.YOUR_TENANT.appmixer.cloud/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.

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

Get All Components

GET https://api.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/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.

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

Delete a Component/Module/Service

DELETE https://api.YOUR_TENANT.appmixer.cloud/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).

https://my.eminent-emu-12345.appmixer.cloud
https://github.com/clientIO/appmixer-demo-embedded-integrations
https://github.com/clientIO/appmixer-demo-embedded-designer
https://github.com/clientIO/appmixer-demo-firebase-vanilla

Introduction

Appmixer is an embedded iPaaS (Integration Platform as a Service) platform designed for SaaS vendors. It enables them to launch native integrations and no-code automations within their products. Appmixer significantly accelerates the time required to deliver integrations to customers.

There are three main use cases for Appmixer.

Embedded iPaaS

Instantly deliver an integration marketplace within your SaaS product using Appmixer Studio and its no-code drag-and-drop designer. Pre-build integrations that enable your end-users to connect your product with other third-party apps and APIs. This feature allows for seamless integration and enhances user experience by facilitating easy connections with external applications.

Embedded Workflow Automation

Enhance your product with embedded workflow automation that offers no-code capabilities. This feature empowers your end-users to visually automate their workflows, customize your product, and connect to any third-party application, facilitating a more versatile and user-friendly experience.

Internal Process Automation (iPaaS)

Leverage the Appmixer no-code automation designer to streamline processes within your company, all while eliminating the need to write any code.

Flows

Get Flows

GET https://api.YOUR_TENANT.appmixer.cloud/flows

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

Query Parameters

Get Flow

GET https://api.YOUR_TENANT.appmixer.cloud/flows/:id

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

Path Parameters

Get Flows Count

GET https://api.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/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

Update Flow

PUT https://api.YOUR_TENANT.appmixer.cloud/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

Query Parameters

Request Body

Delete Flow

DELETE https://api.YOUR_TENANT.appmixer.cloud/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

Clone Flow

POST https://api.YOUR_TENANT.appmixer.cloud/flows/:id/clone

Clone a flow

Path Parameters

Request Body

Start/Stop Flow

POST https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

Send GET request to a component

GET https://api.YOUR_TENANT.appmixer.cloud/flows/:flowId/components/:componentId

Query Parameters

Send POST request to a component

POST https://api.YOUR_TENANT.appmixer.cloud/flows/:flowId/components/:componentId

Query Parameters

Send PUT request to a component

PUT https://api.YOUR_TENANT.appmixer.cloud/flows/:flowId/components/:componentId

Query Parameters

Send DELETE request to a component

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

Query Parameters

Build a Custom Connector

Appmixer enables you to expand its default set of connectors with your own custom connectors. These connectors can interface with third-party APIs, your internal APIs, or provide utilities or business logic tailored to your product.

In Appmixer, connectors are implemented as Node.js modules, accompanied by JSON manifest files. These files contain metadata such as icons, descriptions, and input/output definitions.

Component Overview

In this guide, we will demonstrate how to implement a basic connector, offering just one action component. We will create a connector for the Bored API, a public API that requires no authentication and provides a single HTTP GET endpoint. This endpoint returns a random activity suggestion for when you're feeling bored.

Our component will feature one input port, allowing it to connect with other components within your automations and integrations. It will also have one output port for linking to subsequent actions that might utilize the data from our GetActivity component. Additionally, our component will offer an option for users to specify the type of activity they wish to receive—be it "education," "recreational," or "cooking."

Install Appmixer CLI

Please note that YOURTENANT refers to the tenant ID assigned to you during the signup process.

At this point, your CLI is initialized, and you are ready to start building, testing, and publishing your components.

Create your Component

To generate a boilerplate code for our connector, we will use the Appmixer CLI's generator tool:

In our example above, we used the tool to generate a component with the ID appmixer.boredapi.core.GetActivity. It's important to adhere to Appmixer's convention for fully qualified component names, which follows the pattern [VENDOR].[SERVICE].[MODULE].[COMPONENT]. The description is provided using the --description parameter. The author of the component is specified with the --author parameter, while --inPorts and --outPorts parameters are used to define comma-separated names of input and output ports, respectively (in this case, just one port for each type). The --iconUri parameter allows the passing of a URL for an icon that will be used for the component. Note also that the generator does more than just output the single component code and manifest file; it creates the necessary directory hierarchy and the service.json connector manifest file to complete our connector.

The resulting file structure should resemble the following:

Each component is comprised of two main files: a component.json manifest file that defines the metadata of the component, and a Node.js module (GetActivity.js in our case) that implements the behavior of our component, namely, how it processes inputs and produces outputs. In this guide, we will leave the package.json file as is, since our component does not require any dependencies (no third-party Node.js libraries are needed).

Define Input and Output

Simultaneously, we will define the output of our component, which corresponds to the response received from the HTTP GET request to the /api/activity endpoint. While defining the output is optional, omitting it means users utilizing our component in their automations cannot reference specific values directly when connecting our component's data with other components.

Originally, our generated component.json manifest file looked like this:

After incorporating our changes, which include adding a type input parameter and defining the output, the manifest file will be updated as follows:

Call 3rd Party HTTP API

The final piece of our component is its behavior—specifically, how it responds to inputs and generates outputs. This is achieved through a Node.js module that exports functions recognized by Appmixer. The most crucial function is the receive(context) method, which Appmixer triggers whenever the component receives an input. Within this method, you can implement any business logic or make calls to third-party or internal HTTP endpoints (other protocols are also supported, essentially anything that Node.js can handle). When your component is prepared to output data (typically by using the response object from an HTTP call), it should use the built-in context.sendJson(object, PORT) function. The final GetActivity.js file will appear as follows:

The receive() function initiates an HTTP GET request to our API, incorporating the type input as a query parameter with the same name. All user inputs are accessible through the context.messages.[INPUT_PORT_NAME].content object. We also utilize the built-in context.httpRequest() method to simplify the initiation of HTTP requests. While you're free to use any Node.js library of your choice for making HTTP requests, the httpRequest() method offers a convenient alternative that includes easy request sending and proper error logging.

It's important to note that our code does not explicitly handle errors. This is because Appmixer is designed to automatically manage errors on behalf of the component. If the receive() function encounters an error, such as an HTTP request failing, Appmixer will retry the function automatically at a later time, employing an exponential backoff strategy.

Test your Component

Although your component is now ready to be published to your Appmixer tenant, it is advisable to first conduct local testing. Continuously updating your component code, republishing it, and reconfiguring or restarting your test flow for every test can be cumbersome. Consequently, the Appmixer CLI includes a tool that facilitates local testing of your component. In our scenario, we aim to test our component by providing the type input, allowing the component to execute its action (make an HTTP request), and then observe the output:

Below is the console output from our test:

Publish your Component

Now, we are ready to publish the component to our Appmixer tenant. This will enable us to utilize it in our integration templates, internal automations, or make it available to our end-users through the embedded automation designer.

Finally, we can set up an automation that sends us an email every day at 17:00, providing ideas for cooking activities:

If you need to make changes to your connector, simply edit the files, re-pack, and re-publish. Your changes will then be reflected in your tenant.

Build and Publish an Integration

This tutorial will guide you through the process of creating, testing, and publishing your first integration. Specifically, you will learn how to:

  • Build an integration template using the Appmixer no-code Studio.

  • Parametrize your template by adding fields to gather information from your end-users through an easy-to-use web form (Wizard).

  • Validate your integration's functionality through testing.

  • Publish your integration to your end-users for immediate use.

  • Explore a demo of how your integrated marketplace could be presented to your end-users.

Integration Overview

In our tutorial, we will demonstrate an integration that processes newly created contacts as external events. This simulates a real-world scenario where you would send these events via HTTP requests from your application code. The integration examines each contact for the presence of a hotLead parameter. If this parameter is set to true, the contact is then forwarded to Slack.

Create an Integration Template

Configure the OnAppEvent trigger by setting the Event name to contact-created.Use { "first": "John", "last": "Doe", "hotLead": true } as the Event Data Example. This data sample will enable us to reference the specific data fields later in the integration workflow.

Next, add the Condition component to your flow. Do this by dragging the Controls module from the left palette to the canvas. Then, select the Condition options from the Action panel on the right:

Configure the Condition component to check whether the hotLead property from the OnAppEvent trigger is set to true. It's important to note that you can utilize the "+" button adjacent to any configuration field. This feature allows you to reference data originating from any component earlier in the integration flow, regardless of its depth in the workflow.

Next, drag the Slack module from the left palette to the canvas. Choose the SendChannelMessage action and connect it to the true output port of the Condition component:

Select Configuration Fields to Collect from your End-Users

Our integration template is nearly complete. The next step involves configuring the Slack.SendChannelMessage component. You may have noticed that this component includes three configuration fields: Slack account, Slack channel, and Message. Importantly, these fields should not be hardcoded by the template creator. Instead, they are intended to be customizable by the end-user. This means the end-user should have the ability to authenticate with their own Slack account, choose their desired Slack channel, and tailor the message to be sent.

You can enhance user convenience by setting default values for any field, facilitating quicker integration activation for your end-users. To do this, simply enter a value into the desired field. For example, we'll set a default value for the "Slack message" field:

Additionally, you have the option to apply data transformations to any data variable incorporated into a field. To do this, click on the desired variable, which will open the Modifiers panel. Here, you can define a sequence of modifiers that will be applied in the specified order once the integration is activated and the data becomes available. For our scenario, we aim to transform the last name of the incoming contact to uppercase.

Finally, it's important to assign a meaningful name to our integration:

Test your Integration

At this point, our integration is prepared for publishing. However, before proceeding with that, it's crucial to verify that the integration functions as anticipated. Initially, we should examine the appearance of the final configuration web form (Wizard) as it will be presented to our end-users. Additionally, for improved clarity, we recommend renaming the default "Channel" and "Message" fields within the wizard to "Slack Channel" and "Slack Message," respectively. To do this, click on the "Edit Wizard" button located at the top of the interface. This will allow you to view a live preview of the wizard and make adjustments to the fields via the left navigation panel.

Next, proceed with a live test of the integration. Click the "Start Test" button at the top of the page to initiate the integration within your user context (as the template creator). This action will launch the Wizard in its defined state, allowing you to configure the necessary fields. Once the test integration is activated, you'll have the opportunity to experience the integration's functionality firsthand, just as an end-user would.

With our test currently running, you have the ability to manually initiate the OnAppEvent trigger. This is achieved by sending a custom App event to the active test integration. To do this, click the "Test" button located at the top of the interface, which will open the Test menu. From here, choose "Send App Event." For the Event name, enter "contact-created," and for the Event Data, use an example such as { "first": "John", "last": "Smith", "hotLead": true }. After submitting the event, monitor your Slack channel to verify that the contact information has been successfully posted.

Additionally, you can view the activity of your running test integration by navigating to "Test" and then selecting "Insights" from the menu:

Publish your Integration to your End-Users

Now that your integration is ready and has been thoroughly tested, you are all set to publish it for your end-users.

Install and Update Connectors

Manage the modules available in the system.

Connector Details

Clicking on any connector will redirect you to that connector's detailed page, where you can find more information and manage settings specific to that connector.

On the connector's detail page, you will find a comprehensive description of the connector, along with options to download the connector as a zip file, and to install, update, or remove the connector from your Appmixer tenant. Additionally, the page provides a list of the connector's components, including actions and triggers, complete with their respective descriptions. This information aids in understanding the functionality and potential use cases of each component within your workflows.

Installing a connector

To install new connectors, simply click the "Install Connector" button found on the Connector details page.

Access Control

By default, all users within your Appmixer tenant instantly gain access to any installed connectors. If you prefer to modify this default setting, you can do so by removing the user * * ACL rule from the ACL configuration. To access this option, navigate to System -> ACL from the left menu. Removing this rule will revoke all users' access to the connectors. Subsequently, you can utilize the "Add User Rules for Installed Connectors" button, which automatically establishes specific access rules for all installed connectors. This method is particularly useful because when you install a new connector in the future, it won't be immediately available to your end-users. This delay provides you, the admin user, with ample opportunity to properly configure and test the new connector before making it accessible to your end-users.

For enhanced clarity, the details page of each connector includes an "Access Control" section. This section outlines all the ACL (Access Control List) rules applicable to that specific connector, helping you understand who has access to it. To view a comprehensive list of all Access Control rules across connectors, you can navigate to the System -> ACL page via the left menu. This centralized view facilitates easier management and oversight of permissions and access within your Appmixer tenant.

Removing a connector

You have the ability to uninstall any connector from your Appmixer tenant by clicking the "Remove Connector" button. This action will uninstall the connector and remove it from your Appmixer environment.

However, caution is advised when removing connectors. Any flows (whether integrations or automations) that include components from the connector you intend to remove will cease functioning. It's important to ensure that any such flows are halted before proceeding with the removal. The most straightforward method to verify this is to visit the Flows page in the Backoffice, applying filters by the connector name and running status, to identify and stop any active flows utilizing the connector in question.:

This process enables you to individually navigate to the Designer for each flow by clicking on the "Flows" link in the right menu for each listed flow, allowing you then to stop the flow directly from the Designer interface.

For more extensive connector cleanups or when manual management of flows is not feasible, you can leverage the Appmixer REST API. This allows you to retrieve flows using the connector with a GET request to https://api.YOUR_TENANT.appmixer.cloud/stats/component-usage?componentType=trello&stage=stopped&group=$flowId, and subsequently stop each flow individually with a POST request to https://api.YOUR_TENANT.appmixer.cloud/flows/:FLOW_ID, including a JSON body with { "command": "stop" } and setting "Content-Type": "application/json" in the header.

  • Trigger Components: These might continue to poll and fail. However, if the connector is re-installed, they can recover, allowing the flow to resume operation.

Installing Connectors from your filesystem

Using the Appmixer CLI to build, test, and publish your connectors is highly recommended as it offers the most control and flexibility. However, if you're looking to quickly test a connector provided by the Appmixer team or a colleague, the UI option to upload your connector can be a convenient alternative. This approach is particularly useful for expedited testing or evaluation of connectors without the need for extensive setup.

Upgrading a Connector to a New Version

The Appmixer team diligently focuses on enhancing connectors and ensuring they remain compatible with updates to third-party APIs. Whenever new versions of connectors become available, all Appmixer customers are notified, with the notification including a list of both newly added and updated connectors. To explore these updates, you can visit the Connectors page in the Backoffice and use the "update status" filter. This will display a list of connectors that have updates ready for installation. This proactive approach helps maintain the efficiency and reliability of your integrations, keeping them aligned with the latest API changes and functionalities.

By visiting the Connector Details page, you have the option to update the connector by clicking the "Update Connector" button. For insights into what changes the update includes, such as fixes or improvements, refer to the Changelog section. This section provides detailed information on the modifications made in the latest version of the connector, helping you understand the enhancements or corrections applied.

Most updates to connectors are designed to be non-disruptive and do not introduce breaking changes, ensuring a smooth transition to the newer version. However, there are instances where updates may necessitate breaking changes. These changes could be required to enhance security, improve functionality, or comply with modifications in third-party APIs. In such cases, these updates are essential for the continued effectiveness and reliability of the connector, even though they may require adjustments in how the connector is used within your workflows.

Connectors in Appmixer adhere to the Semantic Versioning Schema, which is structured as MAJOR.MINOR.PATCH. Updates that involve changes to the MINOR or PATCH version numbers generally do not impact existing integrations or automations, allowing for seamless updates. However, when there is a change in the MAJOR version number, it indicates that a breaking change has been introduced. Such changes can affect running integrations and automations that utilize the connector.

Major Version Upgrade

When connectors and their components undergo significant upgrades, such as radical changes to third-party service API endpoints, shifts from API key authentication to OAuth, or the introduction of new required fields in API endpoints, a major version upgrade of the connector might be necessary. These upgrades often render the connector incompatible with flows created using its older version. Consequently, after upgrading to a new major version of a connector, existing flows cannot be automatically updated to accommodate the changes, as such modifications typically require action from the flow's owner. This might involve re-authenticating due to changes in the authentication protocol or configuring new required fields, among other adjustments.

Should you attempt to update a connector that includes such a breaking change, a confirmation dialog will appear, cautioning you about the introduced breaking change. It will also notify you that all active flows (both integrations and automations) will be automatically stopped to prevent them from malfunctioning due to the incompatibility with the upgraded connector version.

Upon confirming the update, a progress indicator will be displayed, informing you that the flows are in the process of being stopped.

The payload of this webhook will contain relevant information to inform the user about the flow that has been stopped and potentially why it was stopped, providing a clear indication that action is needed on their part to update and restart their integrations. This proactive communication ensures users are aware of any necessary adjustments to maintain the functionality of their integrations after major updates.

Handle Flow Errors

When errors arise in running automations or integrations, Appmixer does not automatically send emails to your end-users. Instead, it offers complete customization of communication with your end-users, enabling you to send your own branded emails or other types of notifications. This approach allows you to maintain consistency in your communication strategy and ensure that all messages align with your brand identity.

It's worth noting that you can set up your own Appmixer automation to manage error notifications. Simply utilize the Webhook trigger as the starting point for your automation and register the Webhook URL with the WEBHOOK_FLOW_COMPONENT_ERROR System Webhook configuration in the Backoffice. This method allows you to automate the notification process effectively, tailoring it to meet your specific needs and and avoid implementing a new endpoint in your own backend application.

The payload that Appmixer sends to the registered URL when an error occurs in a flow has the following structure:

Recommendation for End-User Error Notification Content

While the error message from Appmixer provides comprehensive details about the issue, it's important to communicate this information to your end-users in a more digestible format. To ensure your notifications are user-friendly and avoid overwhelming your end-users with technical jargon, we recommend structuring your communications as follows:

Example: Data Validation Error

Given a scenario where the SendEmail component in a flow mistakenly references the Start time output of the OnStart trigger instead of an email address in the To field, a runtime error will occur. This mistake results in the flow attempting to use a timestamp as the recipient's email address, which is invalid. Consequently, this misconfiguration triggers a runtime error, which is then accurately recorded and can be reviewed in the log viewer.

When an unrecoverable error occurs, such as the one described with the SendEmail component misconfiguration, Appmixer will package the error details into a JSON object. This JSON payload is then sent via an HTTP POST request to the URL specified in the WEBHOOK_FLOW_COMPONENT_ERROR system variable. The structure of this payload is designed to provide comprehensive information about the error, including the component that caused it, the nature of the error, and any relevant data to identify and address the issue:

Example: Network Error

If the flow is configured correctly with a valid email address but encounters a network error preventing Appmixer from sending the email, this situation represents a temporary or external issue rather than a misconfiguration within the flow itself. In such cases, Appmixer will attempt to handle the error based on its retry policies or error-handling mechanisms. If the error persists and is deemed unrecoverable for that instance of execution, Appmixer may report this failure through the WEBHOOK_FLOW_COMPONENT_ERROR system variable, if configured. The notification sent to the specified webhook URL will detail the error, indicating it was a network issue affecting the email send action, allowing for appropriate troubleshooting or user notification.

Users can then review this error within the log viewer or under Insights, where all flow activities and errors are documented for analysis.

Appmixer sends out a JSON payload to the registered URL containing detailed information about the error:

It's important to note that Appmixer employs an automatic retry mechanism for handling network errors when sending messages. This involves multiple attempts to resend the message, following an exponential backoff strategy to optimize the timing of retries. Thus, the error notification sent via the WEBHOOK_FLOW_COMPONENT_ERROR system variable, along with the detailed JSON payload to the registered URL, occurs only if all retry attempts fail. This approach ensures that temporary issues have ample opportunity to be resolved before escalating the error, aiming to maintain the reliability of your automations and integrations without immediate interruption from transient network issues.

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

Explore the public , which houses almost all Appmixer connectors, for inspiration for your own connectors. Additionally, you're invited to contribute by submitting a pull request (PR) to the repository. This collaborative approach allows you to not only benefit from the collective knowledge of the Appmixer community but also to share your innovations with others.

First, you need to install the tool, which enables you to build, test and deploy custom connectors. You can install the tool using npm:

Now, you need to initialize the CLI tool to configure it for your Appmixer tenant and log in with an admin account. Before you publish your components, verify that your admin account is To do this, visit your Appmixer tenant's backoffice at https://backoffice.[YOURTENANT].appmixer.cloud. Navigate to the Users page, locate your user account, edit it, and add "appmixer" as the vendor. Without having the vendor assigned, you will not be able to publish components to your Appmixer tenant.

We can now proceed to edit the component.json manifest file, adding an input field to allow users to specify the type of random activity they wish to receive. This type field corresponds directly to the type query parameter of the .

The schema.properties section defines a for our input while the inspector.inputs section to collect input from the user.

On the Integrations page, click the "Create Integration" button located in the top right corner. Then, select the OnAppEvent trigger from the Trigger selector. Utilizing represents the simplest method for sending data to Appmixer.

To allow end-users to configure fields, click on the magic wand button next to each field. This will add the fields to the Configuration Wizard for end-user customization:

Please note, if Appmixer has not yet been , you can utilize our demo web app. This allows you to preview how your embedded integration marketplace will appear in your app once the actual embedding is completed.

The "Connectors" section of the interface shows all available connectors that you can manage, including options to install, update, or remove them. Connectors that are already installed will display an "Installed" badge at the top. Furthermore, you have the ability to search for connectors based on various criteria, such as their name or installation status, to easily find the ones you're interested in managing.

After installing a connector, it's advisable to consult the connector's documentation to determine if additional configuration is necessary. Appmixer simplifies OAuth configuration by automatically handling it, unless specified otherwise in the connector's configuration. This is achieved through the internal Authentication Hub proxy, which utilizes Appmixer's own OAuth credentials for each connector to authenticate your end-users remotely via this proxy. This approach streamlines the connector installation process, eliminating the need to register your own OAuth applications with each service provider. However, if you prefer to use your own OAuth credentials, you should refer to the "" section of the documentation for guidance.

Failing to stop flows that depend on a connector before its removal will lead to errors in the running flows when the connector components are triggered. These errors will be reported to the , to which you can subscribe for notifications. The impact of such failures varies by component type:

Action Components: These will fail to receive messages from other connected components. After a set period and a number of retries following an exponential backoff strategy, the message is discarded and moved to the dead-letter queue, accessible via the API. Should the connector be re-installed before exhausting all retry attempts, the action component can recover and will continue to process messages.

When creating custom connectors for your Appmixer tenant, there are three methods available for uploading them. First, you can utilize the Appmixer CLI to package and publish your connectors; for detailed instructions, refer to the "" section. Alternatively, you can publish your connector as a zip archive through the Appmixer REST API; for this method, consult the "" documentation. Lastly, if you prefer a direct approach, you can manually upload the connector via the Backoffice by clicking the "Add Connector from your Filesystem" button located at the bottom of the Connectors page. Each method provides a convenient way to integrate your custom connectors into Appmixer, allowing you to choose the best approach based on your preferences and needs.

Before proceeding with connector upgrades, it's recommended to configure the WEBHOOK_FLOW_STOPPED to point to a URL endpoint within your own application or to a Webhook within an Appmixer automation designed specifically for this scenario. This setup is intended to notify end-users (i.e., the owners of the integration instance or automation) that their integrations require reconfiguration due to the update.

To manage errors within running integrations or automations, configure the WEBHOOK_FLOW_COMPONENT_ERROR system variable with a custom URL in the interface. Appmixer will then send an HTTP POST request to this URL each time an error occurs in a flow, enabling you to respond or notify as necessary based on the error details provided. This setup provides a streamlined way to monitor and address issues in real-time.

filter

string

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

sharedWithPermissions

string

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

projection

string

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

sort

string

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

pattern

string

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

id

string

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

forceUpdate

boolean

A running flow cannot be updated unless forceUpdate=true.

object

An object with flow, name , customFields ,thumbnail or sharedWith 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

setOriginFlowId

Boolean

Default false. If true, the originFlowId property of the clone will be set to the original flow.

setComponentIdMap

Boolean

Default true. Stores a map of the component IDs from the original flow to the component IDs in the clone. This is later used in Integrations when performing updates of the Integration Templates.

additional

Object

sharedWith, type and wizard properties can be set in this object.

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

enqueueOnly

Boolean

If "true" then the response to this request will be returned as soon as the requests is enqueued in the engine. It will not wait for the component to process the request and create the response. The response code is 202 in this case. The default value is "false".

{
    // Response
}
{
    // Response
}

enqueueOnly

Boolean

If "true" then the response to this request will be returned as soon as the requests is enqueued in the engine. It will not wait for the component to process the request and create the response. The response code is 202 in this case. The default value is "false".

{
    // Response
}
{
    // Response
}

enqueueOnly

Boolean

If "true" then the response to this request will be returned as soon as the requests is enqueued in the engine. It will not wait for the component to process the request and create the response. The response code is 202 in this case. The default value is "false".

{
    // Response
}
{
    // Response
}

enqueueOnly

Boolean

If "true" then the response to this request will be returned as soon as the requests is enqueued in the engine. It will not wait for the component to process the request and create the response. The response code is 202 in this case. The default value is

"false".

$ npm install -g appmixer
$ appmixer url https://api.[YOURTENANT].appmixer.cloud
$ appmixer login your@admin.com
Password:
$ appmixer init component appmixer.boredapi.core.GetActivity \
--description "Get a random activity to do when I am bored." \
--author "Appmixer Team <info@appmixer.com>" \
--inPorts in \
--outPorts out \
--iconUri "https://cdn.iconscout.com/icon/free/png-256/free-bored-267462.png" \
--serviceLabel BoredAPI \
--serviceDescription "Get random activities."
$ tree appmixer
appmixer
└── boredapi
    ├── core
    │   └── GetActivity
    │       ├── GetActivity.js
    │       ├── component.json
    │       └── package.json
    └── service.json

3 directories, 4 files
{
    "name": "appmixer.boredapi.core.GetActivity",
    "description": "Get a random activity to do when I am bored.",
    "icon": "data:image/png;base64,iVBORw0KGgoAA...",
    "author": "Appmixer Team <info@appmixer.com>",
    "inPorts": [{
        "name": "in",
        "schema": {
            "type": "object",
            "properties": {}
        },
        "inspector": {
            "inputs": {}
        }
    }],
    "outPorts": [{
        "name": "out",
        "options": []
    }]
}
{
    "name": "appmixer.boredapi.core.GetActivity",
    "description": "Get a random activity to do when I am bored.",
    "icon": "data:image/png;base64,iVBORw0KGgoAA...",
    "author": "Appmixer Team <info@appmixer.com>",
    "inPorts": [{
        "name": "in",
        "schema": {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string",
                    "enum": [
                        "education", "recreational", "cooking"
                    ]
                }
            }
        },
        "inspector": {
            "inputs": {
                "type": {
                    "type": "select",
                    "index": 0,
                    "label": "Type",
                    "tooltip": "Type of the activity",
                    "options": [
                        { "content": "Education", "value": "education" },
                        { "content": "Recreational", "value": "recreational" },
                        { "content": "Cooking", "value": "cooking" }
                    ]
                }
            }
        }
    }],
    "outPorts": [{
        "name": "out",
        "options": [
            { "label": "Activity", "value": "activity" },
            { "label": "Accessibility", "value": "accessibility" },
            { "label": "Type", "value": "type" },
            { "label": "Participants", "value": "participants" },
            { "label": "Price", "value": "price" }
        ]
    }]
}
module.exports = {
    receive: async function(context) {
        let url = 'https://bored.api.lewagon.com/api/activity';
        url += '?type=' + context.messages.in.content.type;
        const { data } = await context.httpRequest({ url: url, method: 'GET' });
        return context.sendJson(data, 'out');
    }
};
$ appmixer test component appmixer/boredapi/core/GetActivity \
-i '{ "in": {"type": "recreational"} }'
$ appmixer pack appmixer/boredapi   # generates appmixer.boredapi.zip
$ appmixer publish appmixer.boredapi.zip
$ appmixer component ls | grep boredapi # optionally list all components
appmixer.boredapi.core.GetActivity
{
    "userId": "string",
    "email": "string",
    "title": "string",
    "created": "Date",
    "flowId": "string",
    "flowName": "string",
    "additional": {
        "module": "string",       # name of the module: appmixer.slack, for example
        "moduleLabel": "string",  # Slack, for example
        "reason": "New non-compatible version of a module appmixer.slack has been installed."
    }
}
{
    "err": {
        "message": "Validation error on ports: in",
        "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",
                    "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"
                    },
                    "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"
                }
            }
        ]
    }
}
{
    "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",
                    "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"
                    },
                    "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"
                }
            }
        ]
    }
}
{
    "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",
                    "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"
                    },
                    "flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
                    "messageId": "33b9fcbf-b326-4eb4-bba6-cf34979f4ba2",
                    "flowRunId": 1607944841843,
                    "quotaId": "qs-4882d03d-65e1-44dc-983e-d2a33071779d"
                },
                "content": {
                    "to": [
                        {
                            "email": "martin@client.io",
                            "type": "to"
                        }
                    ],
                    "from_email": "no-reply@appmixer.com"
                },
                "scope": {
                    "bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
                        "out": {
                            "started": "2020-12-14T11:20:41.858Z"
                        }
                    }
                },
                "originalContent": {
                    "started": "2020-12-14T11:20:41.858Z"
                }
            }
        ]
    }
}
Type input
Output of GetActivity consumed in the connected SendEmail component
An example email notification to your end-users.

Insights

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

Get Logs and Histogram

GET https://api.YOUR_TENANT.appmixer.cloud/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 the component's input port(s) and messages sent to the component's output port(s). They also contain any errors that occurred during the flow run or while trying to start/stop a flow. curl "https://api.appmixer.com/logs?from=0&size=30&sort=gridTimestamp:desc&query=gridTimestamp:[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=gridTimestamp:desc&query=gridTimestamp:[2019-03-04+TO+2019-03-08]" -H "Authorization: Bearer [ACCESS_TOKEN]"

Example with the searchAfter:

https://api.appmixer.com/logs?flowId=9c4673d7-a550-45a2-91c1-ad057fac0385&size=30&sort=gridTimestamp:asc&sort=_id:asc&query=gridTimestamp:[* TO *]&searchAfter=2023-10-25T15:03:17.721Z&searchAfter=wC5cZ4sB5dpG2lX0gmxi

The second value in the searchAfter is the _id of a log record.

Query Parameters

Name
Type
Description

portType

string

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

flowId

string

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

exclude

string

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

query

string

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

sort

string

A parameter to sort the result. Optionally followed by ":desc" to change the order. asc by default. This parameter can be passed multiple times to use multiple sortings.

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.

searchAfter

string

If using a sort parameter, this parameter can be used to specify from what value the logs will be fetch. Very useful for pagination and much more efficient than using from parameter for this purpose. Can be passed multiple times if you are using a secondary sorting. This is usually to ensure that no duplicates are returned.

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

Get Log Detail

GET https://api.YOUR_TENANT.appmixer.cloud/log/:logIndex/:logId

DEPRECATED. You can get the log details with /logs API:

curl "https://api.appmixer.com/logs?query=_id:AWKbQ6Vr9I6rzDWu4NbG&sort=@timestamp:desc" -H "Authorization: Bearer [ACCESS_TOKEN]"

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

Path Parameters

Name
Type
Description

logId

string

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

logIndex

string

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

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

Get Logs (Aggregations)

POST https://api.YOUR_TENANT.appmixer.cloud/logs

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

Query Parameters

Name
Type
Description

flowId

string

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

exclude

string

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

query

string

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

sort

string

A parameter to sort the result. Optionally followed by ":desc" to change the order. asc by default. This parameter can be passed multiple times to use multiple sortings.

size

number

Maximum number of logs returned. Useful for pagination.

from

number

Index of the first log returned. Useful for pagination.

search_after

string

If using a sort parameter, this parameter can be used to specify from what value the logs will be fetch. Very useful for pagination and much more efficient than using from parameter for this purpose. Can be passed multiple times if you are using a secondary sorting. This is usually to ensure that no duplicates are returned.

Request Body

Name
Type
Description

aggs

object

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

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

Get Usage Information for Current User

GET https://api.YOUR_TENANT.appmixer.cloud/telemetry

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

Query Parameters

Name
Type
Description

to

string

To date.

from

string

From date.

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

Get Usage Information For Other Users

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

Get usage information for a user identified by the userId query parameter. This call requires admin privileges. curl "https://api.appmixer.com/telemetry/messages?userId=54324413432141432&from=2024-01&to=2024-02 -H "Authorization: Bearer [ACCESS_TOKEN]"

Query Parameters

Name
Type
Description

to

string

To date.

from

string

From date.

userId

string

A user ID.

{
  "from": "2024-01-01",
  "to": "2024-02-01",
  "totalCount": 24,
  "totalSize": 8050,
  "stats": [
    {
      "size": 1772,
      "count": 4,
      "date": "2024-02"
    },
    {
      "size": 6278,
      "count": 20,
      "date": "2024-01"
    }
  ]
}

Get Flow Usage Information

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

Get usage information for a flow identified by flowId. This call requires admin privileges. curl "https://api.appmixer.com/telemetry/flows/ef4324-431ff-434fadf-424 -H "Authorization: Bearer [ACCESS_TOKEN]"

{
  "totalCount": 10,
  "totalSize": 9220
}

Data Stores

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

Get All Stores

GET https://api.YOUR_TENANT.appmixer.cloud/stores

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

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

Get One Store metadata

GET https://api.YOUR_TENANT.appmixer.cloud/stores/:id

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

Path Parameters

Name
Type
Description

id

string

Store ID.

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

Get Number of Records in a Store

GET https://api.YOUR_TENANT.appmixer.cloud/store/count

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

Path Parameters

Name
Type
Description

storeId

string

Store ID.

{
    "count": 681
}

Get Store Records

GET https://api.YOUR_TENANT.appmixer.cloud/store

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

Query Parameters

Name
Type
Description

storeId

string

Store ID.

sort

string

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

offset

number

Index of the first record returned.

limit

number

Maximum number of records returned.

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

Create a new Store

POST https://api.YOUR_TENANT.appmixer.cloud/stores

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

Request Body

Name
Type
Description

name

string

Name of the store.

{
    "storeId": "5c7f9bfe51dbaf0007f08db0"
}

Delete a Store

DELETE https://api.YOUR_TENANT.appmixer.cloud/stores/:id

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

Path Parameters

Name
Type
Description

id

string

Store ID.

Rename a Store

PUT https://api.YOUR_TENANT.appmixer.cloud/stores/:id

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

Path Parameters

Name
Type
Description

id

string

Store ID.

Request Body

Name
Type
Description

name

string

New name of the store.

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

Create a new Store Item

POST https://api.YOUR_TENANT.appmixer.cloud/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

key

string

Key under which the posted value will be stored.

id

string

Store ID.

Request Body

Name
Type
Description

string

Value to store under the key.

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

Update key or value of an existing store item

PATCH https://api.YOUR_TENANT.appmixer.cloud/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

id*

String

Store ID

key*

String

Key under which the updates are required

Request Body

Name
Type
Description

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

Delete Store Items

DELETE https://api.YOUR_TENANT.appmixer.cloud/store

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

Request Body

Name
Type
Description

items

array

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

{
    "deletedCount": 1
}

Download the content of a Data Store

GET https://api.YOUR_TENANT.appmixer.cloud/store/download/:storeId

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

Query Parameters

Name
Type
Description

format

String

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

{
    // Response
}
Appmixer Embed Integrations Demo
Appmixer Github repository
Appmixer CLI
Bored API endpoint
App Events
embedded into your product
Appmixer Backoffice
System Webhook
Unprocessed Messages
Build a Custom Connector
Apps endpoints
System Webhook
Appmixer Backoffice
Custom OAuth Credentials
Sign-in endpoint

User

API for users

Sign-in User

POST https://api.YOUR_TENANT.appmixer.cloud/user/auth

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

Request Body

Name
Type
Description

password*

string

Password.

username*

string

Username, has to have an email format.

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

Create User

POST https://api.YOUR_TENANT.appmixer.cloud/user

Request Body

Name
Type
Description

password*

string

Password.

email

string

Email address.

username

string

Email address.

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

Get Current User Information

GET https://api.YOUR_TENANT.appmixer.cloud/user

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

{
  "id": "58593f07c3ee4f239dc69ff7",
  "username": "tomas@client.io",
  "isActive": true,
  "email": "tomas@client.io",
  "scope": [
    "user"
  ],
  "plan": "beta"
}

Get User Information

GET https://api.YOUR_TENANT.appmixer.cloud/users/:userId

Admin token required.

Get all users

GET https://api.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/users/count

Admin token required

Update user

PUT https://api.YOUR_TENANT.appmixer.cloud/users/:userId

Admin token required.

Request Body

Name
Type
Description

scope

Array

Array of scopes.

vendor

String|Array

One or more vendors.

Delete user

DELETE https://api.YOUR_TENANT.appmixer.cloud/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.

{
    "ticket": "830639e3-c53a-42d6-ad43-0276674236b4"
}

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

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

Change user password

POST https://api.YOUR_TENANT.appmixer.cloud/user/change-password

User token required.

Request Body

Name
Type
Description

oldPassword*

String

Old password

newPassword*

String

New password

Reset user password

POST https://api.YOUR_TENANT.appmixer.cloud/user/reset-password

Admin token required.

Request Body

Name
Type
Description

email*

String

User email address

password*

String

New password

Forgot Password

POST https://api.YOUR_TENANT.appmixer.cloudforgot-password

Request Body

Name
Type
Description

email*

String

Email address

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

Reset forgotten password

POST https://api.YOUR_TENANT.appmixer.cloud/forgot-password/reset

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

Request Body

Name
Type
Description

password*

String

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

code*

String

Code generated via forgot-password.

{}

Authentication

The vast majority of API endpoints within the Appmixer require an access token to execute the calls. The following methods explain how to create a user and obtain the access token through the sign-in endpoint.

Sign-in User

POST https://api.YOUR_TENANT.appmixer.cloud/user/auth

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

You can sign in either with your username and password or with your email and password.

Request Body

Name
Type
Description

password*

string

Password.

username

string

Username. If the email is not provided in the body, a username is required. Hence, a username or email must be included.

email

string

Email. If the username is not provided in the body, an email is required. Hence, a username or email must be included.

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

Create User

POST https://api.YOUR_TENANT.appmixer.cloud/user

Request Body

Name
Type
Description

password*

string

Password.

email*

string

Email address.

username*

string

Username.

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

Get User Information

GET https://api.YOUR_TENANT.appmixer.cloud/user

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

{
  "id": "58593f07c3ee4f239dc69ff7",
  "username": "tomas@client.io",
  "isActive": true,
  "email": "tomas@client.io",
  "scope": [
    "user"
  ],
  "plan": "beta"
}

Files

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

Get file info

GET https://api.YOUR_TENANT.appmixer.cloud/files/metadata/:fileId

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

Path Parameters

Upload a file

POST https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

Get user files

GET https://api.YOUR_TENANT.appmixer.cloud/files

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

Query Parameters

Get number of files

GET https://api.YOUR_TENANT.appmixer.cloud/files/count

Used for paging.

Query Parameters

Remove file.

DELETE https://api.YOUR_TENANT.appmixer.cloud/files/:fileId

Delete all user files.

DELETE https://api.YOUR_TENANT.appmixer.cloud/files

Query Parameters

Accounts

Authentication to apps.

Get Accounts

GET https://api.YOUR_TENANT.appmixer.cloud/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

Query Parameters

Get All Accounts

GET https://api.YOUR_TENANT.appmixer.cloud/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

Example of filtering certain accounts:

Update Account Info

PUT https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

Create Account

POST https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

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

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

An example, this time an API Key account:

Another example with a PWD account type:

Test Account

POST https://api.YOUR_TENANT.appmixer.cloud/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

Remove Account

DELETE https://api.YOUR_TENANT.appmixer.cloud/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

List All Flows Using Account

GET https://api.YOUR_TENANT.appmixer.cloud/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

Generate Authentication Session Ticket

POST https://api.YOUR_TENANT.appmixer.cloud/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 https://api.YOUR_TENANT.appmixer.cloud/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

Query Parameters

Get Authentication Status

GET https://api.YOUR_TENANT.appmixer.cloud/auth/status/:ticket

Clear Authentication From Component

DELETE https://api.YOUR_TENANT.appmixer.cloud/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

Assign Account To Component

PUT https://api.YOUR_TENANT.appmixer.cloud/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

People Task

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

Get Tasks

GET https://api.YOUR_TENANT.appmixer.cloud/people-task/tasks

Return all tasks of a user.

Query Parameters

Get Task Count

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

Get the number of all tasks of a user.

Query Parameters

Get Task

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

Get a task detail.

Path Parameters

Create a New Task

POST https://api.YOUR_TENANT.appmixer.cloud/people-task/tasks

Request Body

Register a Webhook for a Task

POST https://api.YOUR_TENANT.appmixer.cloud/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

Delete a Webhook

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

Delete a previously registered webhook.

Path Parameters

Edit a Task

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

Edit an existing task.

Path Parameters

Request Body

Approve a Task

PUT https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

Reject a Task

PUT https://api.YOUR_TENANT.appmixer.cloud/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

Request Body

Variables

Get flow variables

POST /variables/:flowId/fetch

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

Headers

Body

Response

Example:

curl --location 'https://api.YOUR_TENANT.appmixer.cloud/variables/bfc64735-7cf0-4061-844c-e15a7147cbc7/fetch' --header 'Authorization: Bearer [ACCESS_TOKEN]' --header 'Content-Type: application/json' --data '{ "useCache": true, "flow": false, "components": { "IDs": [ "bd9891a7-3303-43d4-a223-714a8db11e05" ], "properties": true, "links": false } }'

Log viewer in Designer.
JSON Schema

Create user. By default, this endpoint is open (does not require authentication). This can be changed by setting the API_USER_CREATE_SCOPE . If you set the value of API_USER_CREATE_SCOPE to for example admin, then an admin token will be required to call this API. curl -XPOST "https://api.appmixer.com/user" -H "Content-type: application/json" -d '{ "username": "abc@example.com", "email": "abc@example.com", "password": "abc321" }'

See the configuration for more details.

Create user. By default, this endpoint is open (does not require authentication). This can be changed by setting the API_USER_CREATE_SCOPE . If you set the value of API_USER_CREATE_SCOPE to for example admin, then an admin token will be required to call this API. curl -XPOST "https://api.appmixer.com/user" -H "Content-type: application/json" -d '{ "username": "abc@example.com", "email": "abc@example.com", "password": "abc321" }'

Name
Type
Description
Name
Type
Description
Name
Type
Description

Example: https://api.appmixer.com/&sort=filename:1

Name
Type
Description
Name
Type
Description

Example: DELETE https://api.appmixer.com/ will delete all user's files with 'invoice' in the name.

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

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.

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

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

Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Type
Description
Name
Value
Name
Type
Description
system configuration
system configuration

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

Content-type

string

Must be set to multipart/form-data

uploader-file-id

string

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

uploader-file-name

string

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

uploader-chunk-size

string

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

uploader-chunk-number

string

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

uploader-chunks-total

string

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

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

limit

number

offset

number

sort

String

projection

String

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

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

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
}
{
    // Response
}

filter

String

Can be used to filter files.

{
    // Response
}

componentType

string

Component Type.

componentId

string

Component ID.

  "componentType": "appmixer.slack.list.SendChannelMessage",
  "auth": {
    "accounts": {
      "5a6e21f3b266224186ac7d03": {
        "accessTokenValid": true,
        "accountId": "5a6e21f3b266224186ac7d03",
        "tokenId": "5a6e21f3b266224186ac7d04",
        "componentAssigned": true,
        "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5",
        "scopeValid": true,
        "authorizedScope": [
          "channels:read",
          "chat:write:user"
        ],
        "name": "U0UFJ0MFG - client IO",
        "displayName": "client IO"
      }
    }
  }
}

filter

string

You can filter accounts.

[
  {
    "accountId": "5a6e21f3b266224186ac7d03",
    "name": "U0UFJ0MFG - client IO",
    "displayName": null,
    "service": "appmixer:slack",
    "userId": "58593f07c3ee4f239dc69ff7",
    "profileInfo": {
      "id": "U0UFJ0MFG - client IO"
    },
    "icon": "data:image/png;base64,...rkJggg==",
    "label": "Slack"
  },
  {
    "accountId": "5a7313abb3a60729efe76f1e",
    "name": "t.o.mas@client.io",
    "displayName": null,
    "service": "appmixer:pipedrive",
    "userId": "58593f07c3ee4f239dc69ff7",
    "profileInfo": {
      "name": "tomas",
      "email": "t.o.mas@client.io"
    },
    "icon": "data:image/png;base64,...rkJggg==",
    "label": "Pipedrive"
  }
]  
// 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).

profileInfo

object

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

{
    "accountId": "5f841f3a43f477a9fa8fa4e9",
    "name": "[Name of the account]",
    "displayName": null,
    "service": "[vendor:service]",
    "userId": "5f804b96ea48ec47a8c444a7",
    "profileInfo": {
        
    },
    "pre": {},
    "revoked": false
}
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]"
    }
}'
curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:acme",
    "token": {
        "username" : "[username]",
        "password" : "[password]"
    }
}'

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"
}
{
    "accountId": "5bc0bad6f4cb78001167b173",
    "tokenId": "65c49d44e49f774bb587c4e1",
    "finished": true,
    "updatedAt": "2024-02-08T09:22:12.496Z",
    "error": null
}

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

secret

string

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

limit

number

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

offset

number

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

projection

string

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

sort

string

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

filter

string

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

pattern

string

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

role

string

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

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

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
}

id

string

ID of a task.

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

decisionBy

string

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

status

string

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

description

string

The description of the task.

title

string

The title of the task.

requester

string

Requester's email address.

approver

string

Approver's email address.

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

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

id

string

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

id

string

Id of a task.

decisionBy

string

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

status

string

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

description

string

The description of the task.

title

string

The title of the task.

requester

string

Requester's email address.

approver

string

Approver's email address.

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

id

string

ID of a task.

secret

string

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

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

id

string

ID of a task.

secret

string

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

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

Content-Type

application/json

Authorization

Bearer <token>

useCache

boolean

Default true.

flow

boolean

Default true. If true, the response object will contain flow variables, too.

components

object

components.IDs

array<string>

Array of component IDs

components.links

boolean

Default true. If true, the response object will contain input link variables for the given component IDs.

components.properties

boolean

Default true. If true, the response object will contains properties variables for the given component IDs.

{
    "components": {
        "e58aba53-cc20-4847-b3ef-c0d35d5e4421": {
            "links": {
                "message": {
                    "a6bb7a84-89c5-41e9-8b59-6a3264a49272": {
                        "out": {
                            "inspector": {},
                            "variables": {
                                "static": {},
                                "dynamic": [
                                    {
                                        "componentId": "9f9542a2-b37d-4280-aa54-87f21912701b",
                                        "hardwired": true,
                                        "label": "Start time",
                                        "port": "out",
                                        "value": "{{{$.9f9542a2-b37d-4280-aa54-87f21912701b.out.started}}}"
                                    },
                                    {
                                        "componentId": "9f9542a2-b37d-4280-aa54-87f21912701b",
                                        "hardwired": true,
                                        "label": "Raw Output",
                                        "port": "out",
                                        "value": "{{{$.9f9542a2-b37d-4280-aa54-87f21912701b.out}}}"
                                    },
                                    {
                                        "componentId": "a6bb7a84-89c5-41e9-8b59-6a3264a49272",
                                        "hardwired": true,
                                        "label": "Raw Output",
                                        "port": "out",
                                        "value": "{{{$.a6bb7a84-89c5-41e9-8b59-6a3264a49272.out}}}"
                                    }
                                ]
                            }
                        }
                    }
                }
            },
            "properties": {
                "inspector": {
                    "inputs": {},
                    "groups": {},
                    "errors": {
                        "inputs": []
                    }
                },
                "variables": {
                    "static": {
                        "channelId": [
                            {
                                "label": "My Channel",
                                "value": "123"
                            },
                            {
                                "label": "Another Channel",
                                "value": "ABD1"
                            }
                        ]
                    }
                },
                "schema": {}
            }
        }
    },
    "flow": [
        {
            "label": "Flow ID",
            "category": [
                "Control"
            ],
            "name": "g_flowId"
        },
        {
            "label": "Flow Name",
            "category": [
                "Control"
            ],
            "name": "g_flowName"
        },
        {
            "label": "Random Number",
            "category": [
                "Math"
            ],
            "name": "g_random"
        },
        {
            "label": "PI",
            "category": [
                "Math"
            ],
            "name": "g_pi"
        },
        {
            "label": "UUID v4",
            "category": [
                "Control"
            ],
            "name": "g_uuid4"
        },
        {
            "label": "Timestamp (Unix)",
            "category": [
                "Date"
            ],
            "name": "g_timestamp"
        },
        {
            "label": "Now (ISO 8601)",
            "category": [
                "Date"
            ],
            "name": "g_now"
        },
        {
            "label": "User ID",
            "category": [
                "Control"
            ],
            "name": "g_userId"
        },
        {
            "label": "Webhook URL",
            "category": [
                "Control"
            ],
            "name": "g_webhookUrl"
        },
        {
            "label": "Custom Fields",
            "category": [
                "Control"
            ],
            "name": "g_customFields"
        }
    ]
}

Charts

Control the charts in a user dashboard (Insights UI).

Create Chart

POST https://api.YOUR_TENANT.appmixer.cloud/charts

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

Request Body

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.

type

string

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

name

string

Name of the chart.

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

Update Chart

PUT https://api.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/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".

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

Get One Chart

GET https://api.YOUR_TENANT.appmixer.cloud/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.YOUR_TENANT.appmixer.cloud/charts/:id

Path Parameters

Name
Type
Description

id

string

ID of a chart.

Config

System configuration. The following endpoints are only accessible to users with `admin` scope.

Get all configuration entries

GET https://api.YOUR_TENANT.appmixer.cloud/config

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

[
  {
    "key": "JWTSecret",
    "value": "OQekJ3DH4pRnWFl4wlN0hzhc5UIjdihEwFnwYLYUdXGXk+/f5JieT/1VLPUJnvALIGK014md41rUuarqYZscl2T5azHQmFhQmUKj8dEuoIELWB45wlkxDKcojCQi9Otk76itnmvKrbm/ZokDJxePNv2Edgc7/mLrTHG7l54w44c="
  },
  {
    "key": "WEBHOOK_FLOW_COMPONENT_ERROR",
    "value": "https://example.com/webhook"
  }
]

Create a configuration key/value pair

POST https://api.YOUR_TENANT.appmixer.cloud/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 https://api.YOUR_TENANT.appmixer.cloud/config/:key

Path Parameters

Name
Type
Description

key*

String

The key of the configuration to be removed

{ "ok": true }

Unprocessed Messages

When a message processing fails, even after a certain number of retries, Appmixer stops processing the message and archives it. You can fetch, delete, and even retry those messages programmatically.

Get messages

GET https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages

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

Path Parameters

Name
Type
Description

string

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

Get message

GET https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId

Get a single message.

Path Parameters

Name
Type
Description

messageId

string

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

DELETE https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId

Delete a message.

Path Parameters

Name
Type
Description

messageId

string

{}

Retry a message

POST https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId

Put the message back into Appmixer engine.

Path Parameters

Name
Type
Description

messageId

string

{}

Public Files

Public files are files that are available at the root location of your Appmixer Tenant API endpoint. For example, uploading verify.html will make the file available at API_URL/verify.html.

Returns a list of the public files

GET https://api.YOUR_TENANT.appmixer.cloud/public-files

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

[
  {
    "filename": "test.txt"
  }
]

Upload a public file

POST https://api.YOUR_TENANT.appmixer.cloud/public-files

curl --location 'https://acme.com/public-files' --header 'Authorization: bearer [the-admin-token]' --form 'file=@"/tmp/icon.png"' --form 'filename="icon.png"'

Request Body

Name
Type
Description

filename*

String

The name for the file

file*

File

The file to be uploaded

{ "ok": true }

Removes a public file

DELETE https://api.YOUR_TENANT.appmixer.cloud/public-files/:filename

Path Parameters

Name
Type
Description

filename*

String

The name of the file you want to remove

{ "ok": true }

Connector Configuration

Global configuration for your connectors. If a component contains either an auth section or authConfig section, values for the specified service will be injected into the `context.config` object.

Only users with admin scope can use these endpoints.

Get Services Configuration

GET https://api.YOUR_TENANT.appmixer.cloud/service-config

Get a list of stored configurations.

Query Parameters

Name
Type
Description

pattern

string

A term to filter configurations containing pattern on their service id

sort

string

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

offset

number

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

limit

number

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

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

Get Service Configuration

GET https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId

Get the configuration stored for the given service.

Path Parameters

Name
Type
Description

string

The service id. Example: appmixer:google

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

Create Service Configuration

POST https://api.YOUR_TENANT.appmixer.cloud/service-config

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

Request Body

Name
Type
Description

whatever

string

Any value for the whatever-key

serviceId

string

The serviceId. It should be in the form vendor:service. Example: appmixer:google

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

Update Service Configuration

PUT https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId

Updates the stored configuration for the given service. The payload should contain the whole configuration, as the payload content will overwrite the configuration stored under the service.

Path Parameters

Name
Type
Description

serviceId

string

The service id. Example

Request Body

Name
Type
Description

whatever-key

string

Any value you need

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

Delete Service Configuration

DELETE https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId

Removes the configuration from the given service.

Path Parameters

Name
Type
Description

serviceId

string

The service id. Example: appmixer:google

{}

ACL

ACLs can be used to control access to connectors by users or group of users or access to any other Appmixer functionality via limiting the API routes the users can use (API/UI).

Get ACL types

GET https://api.YOUR_TENANT.appmixer.cloud/acl-types

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

[
    "routes",
    "components"
]

Get ACL rules for components|routes

GET https://api.YOUR_TENANT.appmixer.cloud/acl/:type

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

Path Parameters

Name
Type
Description

type

string

components | routes

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

Update ACL rules

POST https://api.YOUR_TENANT.appmixer.cloud/:type

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

Path Parameters

Name
Type
Description

type

string

components | routes

Request Body

Name
Type
Description

array

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

Get available resource values

GET https://api.YOUR_TENANT.appmixer.cloud/acl/:type/resources

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

Path Parameters

Name
Type
Description

type

string

components | routes

['*', 'flows']

Get available action values

GET https://api.YOUR_TENANT.appmixer.cloud/acl/:type/actions

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

Path Parameters

Name
Type
Description

type

string

components | routes

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

Get available options for attributes property.

GET https://api.YOUR_TENANT.appmixer.cloud/acl/:type/resource/:resource/attributes

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

Path Parameters

Name
Type
Description

type

string

components | routes

resource

string

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

Modifiers

Modifiers are data transformation functions that can be used to transform data variables inside your flows. You can customize the list of modifiers or even define your own functions.

Get Modifiers

GET https://api.YOUR_TENANT.appmixer.cloud/modifiers

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

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

Edit Modifiers

PUT https://api.YOUR_TENANT.appmixer.cloud/modifiers

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

Request Body

Name
Type
Description

categories

object

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

modifiers

object

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

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

Delete Modifiers

DELETE https://api.YOUR_TENANT.appmixer.cloud/modifiers

Delete all modifiers. Restricted to admin users only.

{}

Test Modifier Function

POST https://api.YOUR_TENANT.appmixer.cloud/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\""
}
files?limit=10&filter=filename:~invoice
files?filter=filename:~invoice
appmixer.ui.PeopleTask UI SDK
People Tasks tutorial

Basic Structure

Introduction

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

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

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

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

Directory Structure

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

Service Manifest File

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

{
    "name": "[vendor].[service]",
    "label": "My App Label",
    "category": "applications",
    "categoryIndex": 2,
    "index": 1,
    "description": "My App Description",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
}    

Available fields are:

Field

Description

name

label

The label of your app.

category

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

categoryIndex

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

index

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

description

Description of your app.

icon

App icon in the Data URI format.

Module Manifest File

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

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

Available fields are:

Field

Description

name

label

The label of your app.

category

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

categoryIndex

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

index

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

description

Description of your app.

icon

App icon in the Data URI format.

Component

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

To make our example flow complete, it is important to note that any component can be configured using data generated on output ports of any component back in the chain of connected components. In our example, our SendChannelMessage component sends a message on Slack channel #qa-messages with text containing the city, humidity, pressure and temperature as it was received from the weather API. The user configures the flow in the designer UI simply by selecting placeholders (variables in the Appmixer 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 internal 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"
        ]
    }
}

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.

Flow

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

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

Flow Descriptor

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

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

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:

JSON Schema

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:

outPort.source

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.

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.

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:

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.

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.

icon

Manifest

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

Example manifest file:

Services, modules and components hierarchy
Google modules
Twilio service
Modules as separate apps.
A single app type of service.

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

Service manifest fields meaning.

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.

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

Inspector panel

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

Connected Accounts

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

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

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

https://openweathermap.org
authentication module
{
    "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]}}}"
                            }
                        }
                    }
                }
            }
        }
    }
}
{
    "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" }
            ]
        }
    ]
}
{
    "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"
                    }
                }
            }
        }
    ]
}
{ "name": "appmixer.twitter.statuses.CreateTweet" }
{ "label": "Create Tweet" }
{
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV..."
}
{
    "name": "appmixer.utils.controls.OnStart",
    "author": "Martin Krčmář <martin@client.io>",
    "label": "On Flow Start",
    "description": "This trigger fires once and only once the flow starts.",
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV...",
    "outPorts": [
        {
            "name": "out",
            "schema": {
                "properties": {
                    "started": {
                        "type": "string",
                        "format": "date-time"
                    }
                },
                "required": [ "started" ]
            },
            "options": [
                { "label": "Start time", "value": "started" }
            ]
        }
    ]
}
this section
Dynamic output port options.

private

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

JSON Schema
https://en.wikipedia.org/wiki/Data_URI_scheme

inPorts

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

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

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

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

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

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

inPort.schema

inPort.inspector

inPort.source

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

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

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

Note how we mapped the configuration properties of the CreateRow component to the configuration properties of the ListColumns component that is called internally to retrieve the list of columns. The data.properties section defined property mappings. To map input port values, you can use the data.messages section instead. Let's see another example of a component, this time with input message mappings, the trello.list.CreateCard component inspector definition:

"inspector": {
        "inputs": {
            "boardId": {
                "type": "select",
                "label": "Board",
                "index": 1,
                "source": {
                    "url": "/component/appmixer/trello/list/ListBoards?outPort=boards",
                    "data": {
                        "transform": "./transformers#boardsToSelectArray"
                    }
                },
                "tooltip": "Select a board."
            },
            "boardListId": {
                "type": "select",
                "label": "Board list",
                "index": 2,
                "source": {
                    "url": "/component/appmixer/trello/list/ListBoardsList?outPort=lists",
                    "data": {
                        "messages": {
                            "in/boardId": "inputs/in/boardId",
                            "in/isSource": true
                        },
                        "transform": "./transformers#boardListsToSelectArray"
                    }
                },
                "tooltip": "Select a list."
            },
            ...
        }
}

As you can see, the boardId (referenced to by the "inputs/in/boardId" syntax) configured by the end-user on the CreateCard component is mapped into the input port property of the same name of the ListBoards component ("in/boardId").

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:

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

inPort.maxConnections

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

webhook

quota

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

quota.manager

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

quota.resources

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

quota.scope

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

tick

description

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

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

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

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. This makes them suitable mainly for trigger type of components.

Configuration properties are defined using two objects schema and inspector.

properties.schema

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

Do not use special characters . or / in the name of the input.

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

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

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

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

Inspector built-in types:

text

A single line input field.

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

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 the 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, a 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 that 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 which means that you can use each variable once across all the fields inside the expression groups. To clarify this further, imagine the following scenario configuring an expression type:

  1. Click the ADD button to create a second group.

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

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

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: Equality between the values by deep comparison. Used for objects and arrays.

    • ne: Values are not equal.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

Flow
Variables Picker
Input Port Configuration using Variables

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

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

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 requests to. See the section, especially the for details and example.

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

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

Component Description

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

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

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

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:

quota manager
Component Behaviour
{
    "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"
}
{
    "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"
}
defines the form
properties
Properties Schema
Properties Inspector
source with expand

author

The author of the component. Example:

{
    "author": "David Durman <david@client.io>"
}
http://json-schema.org
Rappid Inspector definition format
CSS color formats
source

Option

Description

format

enableTime

Boolean. Enables time picker.

enableSeconds

Boolean. Enables seconds in the time picker.

maxDate

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

minDate

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

mode

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

time_24hr

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

weekNumbers

Boolean. Enables display of week numbers in calendar.

state

Dependencies

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

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

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

Note that the appmixer pack command from the Appmixer CLI ignores the node_modules directory when creating the zip archive representing your custom component. This is intended since when you publish a component to your Appmixer tenant, Appmixer will automatically download dependencies specified in the package.json file.

marker

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

firePatterns

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

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

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

{
    "firePatterns": [1, 1]
}

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

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

The following table lists all the possible fire pattern symbols:

Symbol

Description

*

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

1

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

0

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

A

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

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

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

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

localization

An optional object containing localization strings. For example:

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

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

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

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

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

Beta badge

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

https://momentjs.com/docs/#/parsing/string-format/
https://docs.npmjs.com/files/package.json
https://en.wikipedia.org/wiki/Data_URI_scheme
context.getWebhookUrl()
context.state

Example: Webhook Trigger

This tutorial shows you how you can implement your own custom webhook trigger component. The component will register its own webhook URL with a 3rd party API (possibly your own) and start accepting HTTP requests at that URL to further process and send to its output port. Moreover, we also show how to implement an API key based authentication for our component.

Demo Todo API

Throughout our example, we'll be using a non-existing Todo API service. This imaginary API provides the following endpoints and functionality:

  • Authentication: API key based authentication. All requests must provide the X-Api-Key HTTP header that contains an API key that identifies a particular user.

  • POST /webhooks HTTP endpoint with { url: SOME_URL } payload to register a new webhook URL that will be called by the Todo API server every time there's a change in the user's todo list (new todo item, deleted todo item, updated todo item). This endpoint returns the webhook ID in its response ({ id: WEBHOOK_ID }).

  • DELETE /webhooks/:id HTTP endpoint that unregisters a previously registered webhook identified by its ID.

  • GET /me HTTP endpoint that returns the user's profile.

  • Webhook callbacks: Our TODO API server calls registered webhooks every time there's a change in the user's todo list. The following events (webhook request payloads) are supported:

    • { event: "todo-created", todo: { label: String, id: String } }

    • { event: "todo-deleted", todo: { label: String, id: String } }

    • { event: "todo-updated", todo: { label: String, id: String } }

Appmixer Service Definition

Components in Appmixer are organized in a three level hierarchy: service, module, component. For example, a component that creates new rows in a Google Spreadsheet is organized as google.spreadsheets.CreateRow. This hierarchy allows for sharing e.g. configurations or authentication mechanism between components by placing them either on the service or module level (in our example, all Google components use the standard Google OAuth 2 mechanism for authentication, therefore, the authentication module (auth.js file) is inside the google/ directory). The component hierarchy is also reflected in the directory structure when you implement new services/modules/components. The directory/file structure of our demo component looks like this:

tododemo
├── core
│   └── NewTodo
│       ├── NewTodo.js
│       └── component.json
├── auth.js
└── service.json

The top level directory is called tododemo (our service), under which we have one module called core and one component called NewTodo.

The service directory contains two files, one that defines our authentication mechanism (remember our Todo API uses API key based authentication and we want to get the API key from the user) and one with our service manifest (service.json) with metadata such as icon, label and description.

Our module (core) contains just one component that resides the NewTodo directory. The component is defined with two files, component manifest ( component.json ) and component behaviour (NewTodo.js).

Let's now have a look at all the files in more detail.

Service Manifest (service.json)

The entire service.json manifest for our connector looks like this:

{
    "name": "appmixer.tododemo",
    "label": "Todo Demo",
    "description": "Appmixer Todo Demo Connector",
    "category": "applications",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA..."
}

The rest of the fields mainly define how this service will be displayed in the UI. Once we publish our service to Appmixer, you'll notice a new connector in the left panel of the Designer UI:

Service Authentication Module

The next file in our tododemo directory is the auth.js authentication module. This module defines how our users authenticate to our service. In our case, the Todo API requires API key based authentication so we want to collect the API key from the user when they use our NewTodo component in their flows/integrations:

Our auth.js file looks like this:

module.exports = {
    type: 'apiKey',
    definition: {
        auth: {
            apiKey: {
                type: 'text',
                name: 'API Key',
                tooltip: 'Your Todo app account API key. Find it in your user profile.'
            }
        },
        validate: {
            method: 'GET',
            url: '{{config.baseUrl}}/me',
            headers: {
                'X-Api-Key': '{{apiKey}}'
            }
        }
    }
};

The UI of the form looks like this:

As you can see, the form contains only one field with the label "API Key" (definition.auth.apiKey.name) and tooltip "Your Todo app account ..." (definition.auth.apiKey.tooltip). The type of the form field is text.

Component manifest (component.json)

At this point, our service definition is complete. We can start defining our NewTodo trigger component. Note that our core module does not have any definitions. In our example, we don't need to have separate modules under our service with their own definitions. We treat our entire service as a module. However, the core/ directory must still exist (or a directory with a different name that you choose).

Let's now have a look at the component.json manifest file:

{
    "name": "appmixer.tododemo.core.NewTodo",
    "author": "Appmixer <info@appmixer.com>",
    "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA...",
    "webhook": true,
    "auth": {
        "service": "appmixer:tododemo"
    },
    "outPorts": [
        {
            "name": "out",
            "options": [
                { "label": "Todo ID", "value": "id" },
                { "label": "Todo Label", "value": "label" }
            ]
        }
    ]
}

As you can see, the component manifest contains metadata about our component. Again, following the same convention, the name of the component must be of the form [vendor].[service].[module].[component] and it must follow the same directory structure our component lives in (core/NewTodo). The author field just contains the author of the component, possibly including their email address between < and > brackets. icon field is again an image icon represented with the Data URI scheme. The webhook: true field tells Appmixer to accept external HTTP requests on the component internal webhook URL (see below the context.getWebhookUrl() function that gives us the component URL endpoint). The auth.service field points to our authentication module and must be of the form [vendor].[service] (or [vendor].[service].[module] in case the auth.js authentication module is defined under the module directory (which is not our case).

The outPorts section defines the component output ports. In our NewTodo trigger component, we only have one output port (out) (In general, Appmixer supports an arbitrary number of output ports.):

The options section of our out output port then defines the variables that users can use in other connected components to reference output fields from our NewTodo component. For example, let's say we want to send a Slack message for each new todo. We can create a flow that looks like the one below and use the variables from our component when composing the Slack message:

As you can see, the variables that show up in the variables picker are the ones that we defined in our options section ("Todo ID" and "Todo Label"). The special "Raw Output" variable was added automatically by Appmixer to allow the user to use the raw JSON output of the component (which can be useful in some cases). Also note that the options field is an array of objects with the label and value fields. The label field simply defines the label of the output variable (e.g. "Todo Label"). The value field references a key of the component output object, i.e. the object passed to the context.sendJson(obj, outputPort) function in the first argument (see below the Component Behaviour section for more details).

Component Behaviour (NewTodo.js)

The last missing piece to our demo Todo connector is the actual component behaviour, i.e. what the component actually does. The component behaviour is implemented as a NodeJS module. In other words, anything that you can implement in NodeJS can be a component in Appmixer (file conversions, business logic utilities, any API calls, ...). In our example, our NewTodo.js file looks like this:

module.exports = {

    async receive(context) {
        // Components defined with webhook: true in the component.json file
        // are eligible to receive HTTP requests to the context.getWebhookUrl().
        // The headers and data of that request is available to us in the
        // context.messages.webhook.content object.
        if (context.messages.webhook) {
            const { data } = context.messages.webhook.content;
            // Remember the webhooks of the example Todo API contain a payload
            // of the form { event, todo: { id, label } }. In the line below,
            // we're interested only in todo-created events since our
            // NewTodo component is supposed to trigger when new todo items are
            // added.
            if (data.event === 'todo-created') {
                // If indeed a new todo item was created, send an output to the
                // only component output port 'out'. The output object the
                // todo object { id, label } which fields correspond to the
                // 'options' list from our output port definition in component.json.
                return context.sendJson(data.todo, 'out');
            }
        }
    },

    async start(context) {
        // In our start() method, we register the component webhook with our
        // Todo API so that we're notified of changes.
        
        // Note the context.auth object gives us the values the user filled in
        // in the authentication form defined in our tododemo service auth.js
        // file.
        const { apiKey } = context.auth;
        // context.config object allows us to use the dynamic configuration 
        // from the Backoffice, the same way as we used it in our auth.js file.
        const url = context.config.baseUrl + '/webhooks';
        // context.getWebhookUrl() gives us the component webhook URL that 
        // 3rd parties (our Todo API) can call to reach our component and send
        // input to it.
        const { data } = await context.httpRequest.post(url, { url: context.getWebhookUrl() }, {
            headers: {
                'X-Api-Key': apiKey,
                'Content-Type': 'application/json'
            }
        });
        // context.saveState(myState) allows us to save a temporary state.
        // This state (an arbitrary JSON object) is persisted only for the
        // period the component is running (i.e. from the start of the flow
        // until the flow stops. See https://docs.appmixer.com/appmixer/component-definition/behaviour#component-state
        // for details.
        // For our purposes, we need to store the webhook ID that we received
        // from the webhook subscription endpoint so that we can later
        // unsubscribe it in the stop() method below. Note that you can't
        // just store the webhook ID in the NodeJS module local variable since
        // it is not guaranteed by the Appmixer engine that it will keep the 
        // NodeJS module in memory. Moreoever, if Appmixer is running in a cluster
        // environment, different nodes can execute the start() and stop() methods.
        return context.saveState({ id: data.id });
    },

    async stop(context) {
        const { apiKey } = context.auth;
        const url = context.config.baseUrl + '/webhooks';
        // The component state object is available to use in the context.state
        // property. This property contains the object that we previously
        // saved using the context.saveState(myState) method.
        return context.httpRequest.delete(url + '/' + context.state.id, {
            headers: {
                'X-Api-Key': apiKey
            }
        });
    }
};
  • receive(context) that the Appmixer engine calls when the component receives an input (in our case, since our component does not have any input ports, the only input it can receive is from the component webhook URL, i.e. HTTP requests to the context.getWebhookUrl() endpoint).

  • start(context) that the Appmixer engine calls when the flow starts. This is the place where we initiate the HTTP request to the Todo API to register the component webhook URL so that our component is notified of changes in the user todo list (the actual changes are received in the receive() method).

  • stop(context) that the Appmixer engine calls when the flow stops. This is the place where we unsubscribe our webhook from the Todo API to properly clean up after ourselves (remove the webhook from the Todo API).

Packing and Publishing our Service

$ npm install -g appmixer
$ appmixer url https://api.your-tenant.appmixer.cloud # or any other custom endpoint in case of self-managed Appmixer installation
$ appmixer login your-admin-user@example.com # note the user must have the "appmixer" vendor set in Backoffice
$ appmixer pack tododemo/
$ appmixer publish appmixer.tododemo.zip

You should now be able to see the "Todo Demo" connector in the left panel of the Designer UI:

TIP: during debugging of your component, you'll be often re-packing/re-publishing the component and running a sample flow to see if it behaves correctly. Use the Log panel of the Designer UI to see the activity in your flow:

TIP 2: use the await context.log({ "foo": "bar" }) function to log any JSON object anywhere in your component behaviour file (our NewTodo.js). This allows you to print your own custom log messages in the Log panel.

Download the demo Service

You can download our sample service below. It's important to note that the service will not work without modifications since we used a dummy, non-existing Todo API. You should modify the service/component to fit your own needs and point it to your own API.

7KB
appmixer.tododemo.zip
archive

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 execute. 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 retries to send the messages later using an exponential back-off strategy that prolongs intervals between the retries.

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 be managed and recovered using the Unprocessed Messages Appmixer REST API.

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

Virtual Method

Description

receive(context)

Called whenever there are new messages on input ports that the component is ready to consume. This method must return a promise that when resolved, tells Appmixer that the messages were successfully processed. When rejected, the engine retries to send the messages to the component again later.

tick(context)

Called whenever the polling timer sends a tick. This method is usually used by trigger Components to implement a API polling mechanism or for schedulers.

start(context)

Called when Appmixer signals the component to start (when the flow starts). This method is usually used by trigger components that might schedule an internal timer to generate outgoing messages in regular intervals or to register a webhook URL (context.getWebhookUrl() with a 3rd party API).

stop(context)

Called when Appmixer signals the component to stop (when the flow stops). This is the right place to do a graceful shutdown if necessary. Webhook-based trigger components use this place to unregister their webhook URLs with 3rd a party API.

Context

All virtual methods have one argument, the context. The context object 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 resolved (replaced with actual data). For example:

{
    receive(context) {
        const smsContent = context.messages.message.content;
    }
}

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:

Humidity: {{{$.ec8cd99f-0ad3-4bca-9efc-ebea5be6b596.weather.main.humidity}}}

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.

context.messages.message.content === 'Humidity: 75'

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)

Call this method 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)

A convenient method for sending an array of objects to an output port. Note that this method does not send the entire array to the output port in one go but rather sends items in the array one-by-one to the output port. Therefore, your output port schema definition should contain the schema of the items of the array, not the array itself.

const Promise = require('bluebird');

module.exports = {
    async receive(context) {
 
        // Without context.sendArray() function:
        const arrayOfObjects = [
            { name: 'John', surname: 'Doe' },
            { name: 'Martin', surname: 'Tester' }
        ];   
        await Promise.map(arrayOfObjects, item => {
            return context.sendJson(item, 'out');
        });
        
        // With context.sendArray() function:
        await context.sendArray(arrayOfObjects, 'out');
    }
}

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 connector or implicit properties in case of OAuth (context.auth.accessToken). For example, if our authentication module for our service (auth.js) looks like this:

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

we can use the context.auth.accountSID and context.auth.authenticationToken in the component virtual methods to access the values for those properties that Appmixer requested from end-users when they authenticated to the connector:

{
    receive(context) {
        let { accountSID, authenticationToken } = context.auth;
    }
}

Backoffice configuration

context.config

When you configure your connector in the Backoffice, you can access the values in the context.auth or context.config objects. context.config is an alias to the original context.auth. This is especially handy for any configuration that you might want to have dynamically changed without the need to redeploy your connector with new configuration.

module.exports = {
    receive(context) {
        const endpoint = context.config.baseUrl + '/weather';
        const { data } = await context.httpRequest.get(endpoint);
        // process results
    }
};

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:

{
    "properties": {
        "schema": {
            "properties": {
                "fromNumber": { "type": "string" }
            }
        },
        "inspector": {
            ...
        }
    }
}

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

{
    receive(context) {
        const fromNumber = context.properties.fromNumber;
    }
}

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, otherwise, the context.state will be cleared when the flow containing the component stops.

context.state is a simple object with keys mapped to values that are stored in the internal Appmixer database. This object is loaded on-demand in each receive() call. It is not recommended to store large amounts of data here. Example:

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

The context.state is especially useful for trigger-type of components when polling an API for changes to e.g. 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 heavily slow down the processing of the component input messages.

async context.loadState()

Load the component's state from internal DB. Normally, you do not need to call this method explicitely since the component's state is loaded just before the component is triggered and the state is available in context.state. However, there are cases when a component needs to reload its state from the DB where this function is useful.

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.

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 a string, number or a JSON object.

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 the service state is available across all components in the connector.

async context.service.loadState()

Load the state from the DB. The returned value is an array of state items each having the key and value properties, e.g. [{ key: "A", value: 1 }, { key: "B", value: 2 }].

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.

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

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)

{
    receive(context) {
        // getAttachment() is an example 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');
            });
    }
}

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

The structure of the returned object looks like this:

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

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:

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

async context.getFileInfo(fileId)

Returns a promise, which when resolved returns the file information (name, length, content type...).

Example return object:

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

async context.loadFile(fileId)

Load a file from the Appmixer file storage. The function returns a promise that when resolved, returns the file data as a Buffer.

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

context.readFileStream(fileId)

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

async context.removeFile(fileId)

Remove a file from the Appmixer file storage. The function returns a promise.

Webhook

context.getWebhookUrl()

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

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

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

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

Property

Description

content.method

HTTP method of the request.

content.hostname

Hostname of the Appmixer API.

content.headers

HTTP headers of the request.

content.query

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

content.data

Object with the body parameters of the request.

correlationId

A special ID generated by Appmixer that 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.

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 or GET requests to.

When a request is received by the component, the context.messages.webhook.content.data contains the body of your HTTP request. 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

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

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.

await context.store.find(storeId, { query: { key: { $nin: rowIds } } })

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

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

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

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

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

async context.store.unregisterWebhook(storeId);

Unregister a webhook.

Scheduling

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. This is especially useful for any kind of scheduling components.

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:

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

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

async context.clearTimeout(timeoutId)

Clear (cancel) a scheduled timeout.

Miscellaneous

async context.callAppmixer(request)

Call an Appmixer REST API endpoint. You can call any of the Appmixer endpoints defined in the API section. 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 runs in. For example:

const task = await context.callAppmixer({
    endPoint: '/people-task/tasks',
    method: 'POST',
    body: {
        title: 'My Task',
        description: 'My Example Task',
        requester: 'john@example.com',
        approver: 'alice@example.com',
        decisionBy: (function() {
            const tomorrow = new Date;
            tomorrow.setDate(tomorrow.getDate() + 1);
            return tomorrow.toISOString();
        })()
    }
});

async context.stopFlow()

Stop the running flow. Example:

module.exports = {
    async receive(context) {
        return context.stopFlow();
    }
};

context.componentId

The ID of the component.

context.flowId

The ID of the flow the component runs in.

context.flowDescriptor

The flow descriptor of the running flow. 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 component with context.properties and the current input message with context.messages.myInPort.content but it can be useful in some advanced scenarios.

context.customFields

context.evalJavaScript(code, jsonData)

This function lets you evaluate a JavaScript code in a sandbox. The first argument is the JavaScript code and the second is an object with data available to the code. The object is then available under $data variable.

'use strict';

module.exports = {

    receive(context) {

        const code = `
        function sum(a, b) {
            return a + b;
        }
        sum(parseInt($data.number), 100);
        `;

        let sum = 0;

        // You can call the context.evalJavascript multiple times in receive()
        for (let i = 0; i < 3; i++) {
            const result = context.evalJavaScript(code, context.messages.webhook.content.data);
            sum = sum + result;
        }

        return context.response({ result: sum });
    }
};

context.setMaxWait(timestamp)

To support components that receive inputs and wait for some future asynchronous response to continue the flow execution from the state of the flow at the time the inputs arrived, Appmixer internally stores a data structure called Continuity scope. The Continuity scope is a document stored in the Appmixer internal DB and contains all the data the flow produced until it reached the component with the first message.

For example, some components receive an input message, call a 3rd party API, and wait for an asynchronous push-type of webhook call originating from the 3rd party API - that arrives at a later time - to receive at the component webhook URL. When the webhook arrives, the component produces a JSON and sends it to its output port. At this point, the component must have the state of the flow (all the related data from the same flow "run") to be able to resolve variables and continue execution.

The Continuity scope documents are deleted after a certain time (by default 100 days). If you need to have a component that can wait more than 100 days for the incoming webhook to resume the flow, you can use this function to adjust the timeout. The function excepts Date, number or a string. If the argument is a number, it is considered to be the number of milliseconds from now (the time of the function call). If it is a string, it will be converted to Date object using new Date(string)function.

async context.loadVariables()

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

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

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

Example:

{
    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 context.log(object)

Log a message. The log message will be available to the end-users in the log panel of the Designer UI or in the Insights page of the Appmixer Studio. The argument has to be an object that can be stringified into JSON.

Example:

{
    async start(context) {
        
        await context.log({ test: 'my test log' });
        return context.sendJson({ started: (new Date()).toISOString() }, 'out');
    }
}

And the object can be seen in the log panel as:

async context.lock(lockName, options)

lockName string will be automatically prefixed with vendor.service:. If a component type is appmixer.google.gmail.NewEmail, 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:

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

Error Handling

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

receive(context)

Sometimes you, as a developer of a component, know that there is no point in retrying a message since no matter how many times Appmixer tries, the message will fail repeatedly. In such cases, you can tell Appmixer to cancel the message by throwing the context.CancelError(reason) error object. This instructs Appmixer not to apply the auto-retry mechanism and the message will simply be discarded.

/**
 * 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;
        }
    }
};

tick(context)

If a tick function throws an exception, the exception will be logged (and visible in Insights). Appmixer will not apply the auto-retry mechanism since it assumes the developer handles the error manually inside the component code next time the tick() function is executed.

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.

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

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:

If you're using a Self-Managed Appmixer package, you should link your own Appmixer JavaScript SDK from your own Appmixer Studio URL (i.e. instead of https://my.YOUR_TENANT.appmixer.cloud, you will reference your own Studio URL). Alternatively, you can download the appmixer.js file and link to it from whatever location you will put it in.

Advanced Usage

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

Choose Appmixer UI widgets to include:

Introduction

Appmixer SKD is a toolkit to embed workflow automation and integration capabilities into your products. Gain a whole new set of comprehensive features with ease.

Quick Start

Example: twilio.SendSMS

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

Directory Structure

Component Behaviour (sms/SendSMS/SendSMS.js)

Defines how the component reacts on incoming messages.

Component Manifest (sms/SendSMS/component.js)

Defines the component properties and metadata.

Component Dependencies (sms/SendSMS/package.json)

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

Service Manifest (service.json)

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

Service Authentication Module (auth.js)

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

Service Authentication Module Dependencies (package.json)

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

Helper Component (sms/ListFromNumbers/ListFromNumbers.js)

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

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

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

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

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

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

Authentication

Appmixer provides an easy way to configure authentication modules. Most of the time, it's only 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, it has one argument called 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 since it is common for all authentication mechanisms.

API key

This is the most basic type of third-party authentication. In order to use this mechanism, type property must be set to apiKey. Here is an example from Freshdesk components:

Next, we explain the fields inside the definition object:

auth (object)

This is the definition for the Web Form that will be displayed to the user to collect information required by the third-party application. Freshdesk requires the domain name and the API key in order to authenticate the user. So we define two fields representing these two 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:

While this field is optional, is recommended for a better UX since it is used to request the user profile information from a 3rd party API and together with accountNameFromProfileInfo show the display name of the 3rd party account in Appmixer UIs.

This field is the dot-separated 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.

Similar to requestProfileInfo. This property 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 value. Otherwise, throw an error. You can also define validate as an object. In that case, the object has the same structure as the object passed into the axios library. For example:

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

If validate is specified as an object, the response error can vary from API to API. Appmixer will use internal heuristics to try to find the error message in the response object. 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:

OAuth 2

The requestAccessToken is used to get the access token while the refreshAccessToken is used to refresh the access token later. Next, see the definition object's properties explained in more detail:

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:

The same logic applies to the following property requestAccessToken.

Part of the OAuth 2 specification is the ability to refresh short-lived access 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.

scope

String or an array of strings.

scopeDelimiter

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

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

refreshBeforeExp

By default, Appmixer 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_TENANT_API_URL]/auth/[service]/callback.

For example, if the service you're developing is called myService then the redirect URI will be https://[APPMIXER_TENANT_API_URL]/auth/myService/callback.

The redirect URI can be changed per module in the Backoffice -> Connector Configuration page:

Context

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

async context.httpRequest

Custom "Connect account" button

Setting OAuth 1,2 secrets

The most important field in the service manifest file is the name field. This field must follow the service naming syntax of the form [vendor].[service]. In our case, we use appmixer vendor name but you can create your own vendor too. Only keep in mind that to be able to publish a component with a certain vendor name, you must set this vendor name in the user profile via the Backoffice admin panel. See for details. Our service name is simply tododemo.

As you can see, the metadata fields are displayed in the UI: the label ("Todo Demo"), description ("Appmixer Todo Demo Connector"), icon (represented as a Data URI scheme instead of a URL link for better portability, see ) and the connector is displayed in the collapsible "Applications" category in the left panel.

As you can see, the authentication module is a NodeJS module that exports a JavaScript object with the definition of our authentication scheme. The top level type field defines the type of the authentication scheme that Appmixer understands (see for details). In our case, we want to use the apiKey authentication type. The definition.auth section then describes the fields of the form that will be displayed to the user when they click on "Connect account", i.e. the API key that we want to collect from them. Note the key of each field is then used to reference the values in our component behaviour (see below). In our case, this is just one field with the key apiKey.

The definition.validate section of our authentication module tells Appmixer how to validate the user provided credentials (the api key in this case). The section can be defined as a templated HTTP request with url, method, headers and data fields (Appmixer actually uses the axios library to make the request so any axios request configuration field is supported: ). As you can see, you can use fields from the definition.auth object by enclosing them with the {{ and }} brackets. We take advantage of that by injecting the user provided api key in the X-Api-Key HTTP header. Moreover, notice the url field and the use of {{config.baseUrl}}. The {{config.CONFIG_KEY}} allow us to use service configuration values defined in the Backoffice admin panel. This is very handy if you don't want to hardcode certain values in your service/module/component definition and instead, make those values configurable via Backoffice. To do that, visit your Appmixer tenant Backoffice interface, go to "Services", click "Add" to add a new service configuration, provide the correct service ID in the form [vendor]:[service] and add your custom configuration key/values by clicking on the "burger" icon:

The module exports functions known to the Appmixer engine that the Appmixer engine calls at the right times. See Component Behaviour for more details: . For our example component, we export three virtual methods (naming convention for the exported functions):

At this point, we have our entire service ready to be packed and published to our Appmixer tenant. To pack and publish services/modules, use the Appmixer CLI tool (). In short, the process is pretty straightforward and looks like this:

Variables
Authentication UI dialog for the above auth.js definition

This method has been deprecated. Use 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. 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.

See, for example, the component for an example of how safeFileStream() can be used.

This method has been deprecated. Use 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).

Since it is very common for components to initiate HTTP requests, Appmixer provides a convenient method to do so. The httpRequest object/function is a wrapper around the well known library.

Flow properties are available in this object.

This method allows components to create a cluster 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 (you can define a quota the way that only one receive call can be executed at a time) or using locks. 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.

If this function throws an exception, Appmixer will try to process the message that triggered this receive call again later using an exponential backoff strategy. In total, Appmixer will try to process the failing message 30 times before it is saved into collection. Every unsuccessful attempt will be logged and visible in Insights.

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

See the following repositories for more comprehensive demos on how Appmixer can be embedded in your apps , .

Refer to the getting started guide to get started quickly.

Connectors that require authentication from the user must implement the authentication module. 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 out-of-the-box that are common for today's APIs: , , , and .

The values configured 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.apiKeyin our component .

requestProfileInfo () (optional)

See that implement this method.

accountNameFromProfileInfo ()

validate ()

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 above. These 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 authentication 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 value. Otherwise, throw an error.

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 :

accountNameFromProfileInfo ()

Works exactly the same way as described in the section.

authUrl ()

Standard means, there is a response_type parameter set to code, the client_id, redirect_uri, state and scope parameters. 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. See, for example, the .

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 optional if the implementation does not have tokens that expire). Inside this function, you should call the endpoint which handles the access tokens for the application. The following context properties are available to you in this function: clientId, clientSecret, callbackUrl and authorizationCode. See, for example, .

requestProfileInfo () (optional)

Works exactly the same way as described in the section.

refreshAccessToken ()

validateAccessToken ()

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

Wrapper around the library, making it easy to initiate HTTP requests without the need to import a 3rd party library:

Appmixer allows you to redefine the Connect Account button in the Designer and Integration Wizards. This is especially useful if the 3rd party has specific requirements for branding of the sign-in buttons, such as .

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

To set your OAuth applications secrets, follow the guideline.

https://docs.appmixer.com/appmixer/appmixer-self-managed/installation#enabling-users-to-publish-custom-components
https://en.wikipedia.org/wiki/Data_URI_scheme
https://docs.appmixer.com/appmixer/component-definition/authentication#authentication-module-structure
https://github.com/axios/axios#request-config
https://docs.appmixer.com/appmixer/component-definition/behaviour
https://docs.appmixer.com/appmixer/appmixer-cli/appmixer-cli
AWS S3 GetFileObject
axios
quota
unprocessedMessages
context.saveFileStream
context.getFileReadStream
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'
    }]
};
<script src="https://my.YOUR_TENANT.appmixer.cloud/appmixer/appmixer.js"></script>

<script type="module">
const appmixer = new Appmixer({ baseUrl: 'https://api.YOUR_TENANT.appmixer.cloud' })
appmixer.api.authenticateUser(username, password).then(auth => {
    appmixer.set('accessToken', auth.token);
    ...
    const integrations = new appmixer.ui.Integrations({ el: '#integrations' });
    integrations.open();
});
</script>
wget https://my.YOUR_TENANT.appmixer.cloud/appmixer/package/appmixer.es.js
wget https://my.YOUR_TENANT.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(/* ... */)
twilio/
├── auth.js
├── package.json
├── service.json
└── sms
    ├── ListFromNumbers
    │   ├── ListFromNumbers.js
    │   ├── component.json
    │   ├── package.json
    │   └── transformers.js
    └── SendSMS
        ├── SendSMS.js
        ├── component.json
        └── package.json
const twilio = require('twilio');

module.exports = {

    receive(context) {

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

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

module.exports = {

    receive(context) {

        let { accountSID, authenticationToken } = context.auth;
        let client = twilio(accountSID, authenticationToken);
        return client.incomingPhoneNumbers.list()
            .then(res => {
                return context.sendJson(res, 'numbers');
            });
    }
};
{
    "name": "appmixer.twilio.sms.ListFromNumbers",
    "author": "David Durman <david@client.io>",
    "description": "When triggered, returns a list of numbers from user.",
    "private": true,
    "auth": {
        "service": "appmixer:twilio"
    },
    "inPorts": [ "in" ],
    "outPorts": [ "numbers" ]
}
{
    "name": "appmixer.twilio.sms.ListFromNumbers",
    "version": "1.0.0",
    "description": "Appmixer component for twilio to get a list of phone numbers of a user.",
    "main": "ListFromNumbers.js",
    "author": "David Durman <david@client.io>",
    "dependencies": {
        "twilio": "^3.14.0"
    }
}
module.exports.fromNumbersToSelectArray = (numbers) => {
    let transformed = [];
    if (Array.isArray(numbers)) {
        numbers.forEach(number => {
            transformed.push({
                label: number['phoneNumber'],
                value: number['phoneNumber']
            });
        });
    }
    return transformed;
};
// or any library you want to perform API requests
const request = require('request-promise');

module.exports = {

    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
            }
        },
        
        // and finally, if a string is used, it is to specify just the URL.
        // In this case, Appmixer will perform GET request to that URL.
        requestProfileInfo: 'https://acme.com/get-profile-info?apiKey={{apiKey}}'
        
        // Note that 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, depends 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: 'appmixer@example.com',
        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 that we want to locally store and reference between
    // the 'requestAccessToken' and 'requestProfileInfo' functions.
    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';
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,iVBORw0KGgoAAAANSUhEUgAAAX4AAABcCAYAAABpyd51AAAAAXNSR0IArs4c6QAAHvtJREFUeAHtXQmYFNW1/qvXWRnWYQYHBMdRhHEQkEXDaCCIGty+MRHUCCbikodxS4TkkfceWdSEGI1PeZFgVBQjfu/Bp4njU8HlgYphBJR9G0dkGVaZjZnptd653V3dVdVV001P90w3nPt91V1113P/e+vcc8899xbAjhFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEMhEBKU6i440XZ3YcjRFgBBgBRiBFCMix8o3F0K2UgSV0ibix4scqj8MZAUaAEWAEUoOAYPji8ocun1kxZoxc+FtH3fH24B6DLv6dZHFMkCyW/maZsD8jwAgwAoxA9yMg+/2HZb/7o6bDm3++4b8mfkUUCeYfNQMwY/yC6Q8pGHLpOkmy9Or+6jAFjAAjwAgwAvEiIMv+hhP7Ph37+aIpX1KaKMnfZpKRLX/gyAWC6Y89x4qHvutAvx5C48PODIGjTX488ZYb676MwtgsCfszAowAI5ASBIh39ywoHvF7yvxmuqKYkhE3F7MAh8WS9S1BETN9gUJsJwZGgRU7RoARYATSAYEQDxdMKUqzY8r4Jau1UBDPkn78TchYxY8Vx2QEGIHUIhDi4afE+M1UQKmllHNnBBgBRoARSCYCgpfHLfEbzQSSSQznxQgwAowAI5B6BAQvj4vxC1KiIqaePi6BEWAEGAFGIMkIGPJyluyTjDJnxwgwAoxAuiPAjD/dW4jpYwQYAUYgyQgw408yoJwdI8AIMALpjgAz/nRvIaaPEWAEGIEkI8CMP8mAcnaMACPACKQ7Asz4072FmD5GgBFgBJKMADP+JAPK2TECjAAjkO4IMONP9xZi+hgBRoARSDICzPiTDChnxwgwAoxAuiPAjD/dW4jpYwQYAUYgyQgw408yoJwdI8AIMALpjgAz/nRvIaaPEWAEGIEkI8CMP8mAcnaMACPACKQ7Asz4072FmD5GgBFgBJKMQNp/cMV3uB7umk/h2b4Z3j27IDecgL+5CbBIsPTqA0vvPrAWFsExehwcYy+h575JhoizYwQYAUbg9EIgLRm/LMtwfbgS7W+ugOeLDaaI++sPQFzerZvg+uDdQDzb0OHImT4DzgkTTdNxACPACDACZzICacf43ev/iZZF/wnfl7sTahfvjq1omj8XtrKhyL3rJ3CMHJNQPpyIEWAEGIHTFYG00fHLbjean3kcjXN/kjDTVzeSd/cOND48GydfeBay368O4ntGgBFgBM5oBNJC4vc3NaLxF/fBu3N70huj9ZXn4a3bg4JfP570vDlDRoARYAQyEYFuZ/z+E9+gYc5s+OpqU4Ofw4ns629KTd6cKyPACDACGYhAtzJ+2eNB4388nFKmX/CbP5LFz9gMbBommRGID4ExI+245TwLPF7AbpOx5n03VhyNLy3HOnUE8gfY8PNKK+yEN4iDHtvqxoIt8qln1I0pupXxtyz8I7zbNsdVfdt5F8Ax7lsQVjuWXr0Bsvzxk2mnd9d2uNd9Au/2Ldp8SNI/3Zn+ZHrhp1xgxVm9JNH/AOqIza1+1H3lwxsbvNhyUgtJ4KmfDUt+4ERJNj35ZLzzRhsW7MicTnvzNVmYVWENVKVhnxtzXvYgRXNFA/CS75WM+gy7wI6KwVKYuOyvPcT4U9em+f2suONSG0YWWZCl4iAnjvmwfqsXi7ec3mtqhWdZMb40UnGvw0eMX4wCmeMi1HcxzW4y0xTmmrGcbdiFyLv7ftiHVxhGddJgkHvbLHjIpLNl0VPBgeQ0Z/rl5Xb821QH+gb5nwaXvn0sGDLQhkmVTuzf6sK9b3jRrIoxeZw9yPSFn1XClZfbiPF7VDHS+NZhxTUhpi+o7DnQjqv6ebAwU6XbJNXH7dO2mZD8U+Mk3HmjE9PPN+h4VGDfAgvKSu2YfoUPK/7uwsLa1A0+qalfnLnq8G3X4R9nLt0arVsYv9/nRcvTj8WsePb0mcj90Y8hWWIbH4mBoeeTi3Dyr38ObuY6TdU7Yyqz8DuaZsbjSoY7sayHhKkkFSvOqdwo/7aIpKh4Zc6/jJZI1TKHbFNK9fWRMP/uHFT2ockcMReb1Y/nnmrDq0YzOdM8kxVgwYL7sjE6L478sq2ompaDc1e24sGa05T5xwFDOkeJzVFTQf2Jd+AsX0092XxKmH3TbcibNTsupq+QKFltyBO2+6cp00c/O35pwPRbjosptgeb9vmFtkfjsgY6sGRyZKBYvdmLBlWM9ev0KVSB6XZLou0nX0UYibfRj23qyqQbvbHoiaM+BTnBTGyBJpSQZ4+VaWrC58wyYvoyqRW9+HSnF4daIu2iUFBxRRaqcpUn/k8nBLpF4pfrl8J54Qk6aqENJ1cMIV29Vg7NuqYqwMDTCah0oGXqOBrYNIT4sezFNiw+qPJ0WPDID7MxnqRExZVcaEPpKl9AF96814MbH/WgOFdCy0lZowZS4qfz/8K/tWKhQ0IxZNS705nS+GiLVR/9hMat94ivmE7Fyi914MpCbRbtRzyY/5wbNSrv8nIHHrnOruqjFtx6lRUrlmegLkRVr9PxtssZv+xpgHzszQCWtv7t6PHDnTj55tnw7C4I+El5+cidde/piHXS63Rok1vL9EUJbj/mLXVh+f1O9FRKpKn3BHqoFdJxrgU3D7PAEQo7RgvB1YYLgRKqKu2YONiCoJAp49B+H17+wIvWs0nXXighwHfb/FhBi3nKOkLxACumnBVUHzmIOb9fQwNOTyseoLWEoaR2CjhaVN68iXTzCSwCjhlqw7D8EPFeGe9u9KE+9JiysmmguXmENYxZ8zekw9bpr8uJrkuILje9Ucf2eAnTEFGhv8qRNpyjvG1E9wqiW2BmXB/CfowVTq+EUrEIH3YSLhprg7uFPJp9eNVkUd7bRuG5VsyZTGUqmFO/WP+FB4tN0oSLMLiZcZlCeCiQ9Gv3EdOv1cXdssWNx2nBd/7YyAyz5yASOhAUOnTRA4+TCZcJ51hRlBPsGx6ic8cuH14K4WOURu0nFpqnU9sMK7SAujk5GceP+vHBpx6sijkbTKyPq8uPdZ/f04LpF1OfDdFnI/oO1/vw+movarpRcNG1aKxqdD5cPrIc8LvCGUlZfuTeWIf2tYVoX12M7O/fCkueVq4NRz7Db5yR9ymIhJmW5qQPOxplXEwwen0SskCMOSQplg6zY9YVkWb37nOh+mVdRsLyZyZZ/iijQwj3Mlo0rrzIjmM0qwgvLNN6zfotLig2VVO+7cQMlYXJ6EFeDDnfFrQ6UrVf2WAbpozx4KEXohmIKpruVsKM650YFsZBhqOuFYtDL3iqys6nReRZV6h0LEfIXLJWLXpb8ECVE0MUaodLqH5OGz7zalU4tcexrW2odpvUB2Q1c4WT2i3aVVziRMDMgXDfvCOCuzpmxaQsVOeRxY3ak/IUC69XkSXUvbTmowyWmihGD7lkKFAcGrBD4evfN7ekWrOW1u+I8Yff4GwLhlI/qtUxufKRDvxqih0kE+gcMXGis+o7Piz7bxcW741WIQUTWDDvtixMGqilTYSVDQTGj3Lgtp1k3LBca9wQLqwTfTycR4c3Eh6YloVrS6O16cL4YvxYB9Z/0I45a83V3R1m38nACAfoZEbxJpcbPo6KKlHbZV96BLbB1NmrpkeFG3l89mXnpo90uCdGDYnqdUZFpY1fk0v7EhSNcmJBo1HnkTFvYasx3ToeH2WR0NOG5XeqZgv6XOhF7qv2oxdaCJiK01uYlBHTN3N5xXY8eo0P096Mvy3bBANRScFqfpKqsptrfdhP854SpSJ9rBgDT0TNQTMg4jURV2jFZApfpfiQVFqs3NO/97gXq0OEm9VH10yq1HHcRjH9SJqetObz+FV+3Pp2/JhHUtOdz4d/dGSzftKLexb7UaKMkx45SrItHePEUyrhQ5O/8kAWT9NvzcHgv7diXlR5UlwLzSXnO/H63RbctUgnXHSyjyskmv9LmEeL8pNU6tbouBJGT8zGktx2zCQ1bFc787cyRZTILYpsGF2Ao6ICUnZoNSs6WOMzd1m75jmRh7fn5sBOJo2Z4lZt9+HhUVaV9BzsPO+M8WF1jQfL1tOUWs0JE6jYnO8ZMH1S52w76EchSSp9dbMAEiTjcvv3edFks2BYsVYC6jvcjkpi/GviyiWxSJ0v24eN9TJKFMnXasHYfkBNSJ0zldZQtC+SFROHSlgVUquUn6uVvg/RQKKoxgxr1CpjJ6kDepCqp2ygFq+GIz4cJT7hoZ+9hokjnqLebTRNLCO1nNoVldswhhi/Wj+vDlff51P5pCWMOFLTHYw8Gd7Vk6rFdEZBM4hHDZj+flok/oZwrdDVd/x1WZi6S8yOIkVVXZ8VZV3kbfFh2wEZhaRaKlIJBugTLVykqo8rFFZOdkYx/XYyRKindu1FQkNP1TtUMtaJmWtbsaSLLbW0/VWhPJX/rgOmuUv5o03DUhHQQIub/RQdaCoKSHaetDD727VWzL9Ey21teVZMmiguoKXRh3UbPHh+bUT3HTcZZDV0uW4R7xhNl6fRdDno3HjgtmxcazC9Ni9DuwBdTNP456Y5ImoIetmHEmdZE1Mfa16CeUjyyv54jx/XFiu4S7iQmDmIwQESLj1Hy5wFPUOHU9wdQdxGa2aWMjZuizG9p6nLnBeCUuAjD+ZifJiRyfifl9rxqooJGtfdj5cWt2FJaGAqJl3Li1X2yOBE0vRFhHlNHJgLlY2oRZhRuGWYzCWNSdH5Vl1F+0/Ufj6i9fkIrfln2/HirQ7VYGPBjClWVCuzQho4bh2uxfsQ7Ve5lfarBJ0Ld5Ip6fTSyGDXt8KBqnfbsELglpI+rq6QFfeo1jhESN26dswKS/U0GyDji4j6TMIUstRbkugMTF30KdxrETyFhAlH9TaaJ3X0Nw9LQcgJYvyZ5taQXvCxNV6YzXfyCsQAkIWlNJt5xMD0s6P6jim3RhiyiEiLeA+Emb7wkPGnl9uwLSbjEXGDbjfRq7Y6qq/1gtTbGncK2WnSxXpIZtk1O3wazAcKxi8cKanLwwrtoFfAm5h9eeCRFmT7R5iQWHz/OJbIHM5GCi2shz2QF8eEeNvK9jDTFynraYPetkQxJ44fZvqUV8thnTRPg8jTs3OwfHY2Xou6cvDGbCdor2HIWTBxiAoL8v30DS2twupszkqFiQeT9aWF89JQDuUVNtWgQJ7UR+8JM/1gpMWvtWGTpr4WVNK6i3Cp6OPBUoO/pbRYXaT2aPSomL4IkPHImx5NXyo6T6sKVCdP1X3XM/5U1SSBfE9G1pgTSN19SVatcWHq461YscmHFjMySIU1njZ7vXGLomw1ixjx16th6sjm32jK3nYKKsnoXaQyth7umgE3qWUf9WKXaoTK6m8NMKNyUvMY8H2AGOLkAYQt/Q8KS+xA+0Gy5ohAfsp3KhJOIa2MbQlinmPXMuqsfAmKUVWAALLGGVQgoSft2hU7d7UX7TsgQWSAMliRhcug8CBAqWm94HUDK6NaUlvWqfsYGROMCKUbPUTLsuo2Gi3ektXU5+oMgMKzgulS0cfVDTGWmLjG0RhWRaq1m2lACF/najEFWY11uBygyTA5D+rBPDk5xsrFRmabntAcVB/XfVjvk9LnHtm6BkhpaUnOnKbcC99spwuoJInosuE2jD2XrCl0/S5vMG3gosW8mXFMJfWLo20Gm3KSUQt9OcnIM948Ei9bxrp9MioUFQIxdDLMQG/V8QUNX3lwsK8dwwIjgYSRZDaLOq2O/MvdWoYUL92djUcq8MScjkPYiHmfTTmpV+p0UaLKCQ9WtNArBJXwQEl9+Juo2MKD/CnRkPCASYxRDB7kp2+/ZpM+uqdBK1zkhVS6+vSp6uPhatEaw+zrwk+mN5oJimms5AXEarPklaTk5DzLlPHLzeuVWF3y3zsvgxm/CqE1pEsWl3BT6Yyeuyq1UmjJCHtci3kusi8XOmvFtWln3Ir3Gfv/4S4fZoUP5yLGPtoG+uRz2O34pwefXEQ22+cHMSwabMNMWtCOOBmfbdMypEhYet7VHvEH1BJq01ANk2rw4a8rXegT4iRuWpCeMsmBIp0AYlS7lsPm9v36+OHBQxewZ78xnsokQxcdadnHCavwGKcnOEXPXc74pbxyyC2fG1Znd8M+DPK0IcceG4ZLy2L3rKNNMnaTTtLICXNOZTu8UXja+dGGnPnX2FBAzNhDrWZvpxMBSbepV8VUkxqo+ms/qmmBLPyy0gJqPIt5Tt25PfEridIOrZQQVE+M/9jVZNkUyn30ZU466E4pyo9PaFPXajqr96Hzg8jZCm2o6hkZSNHixbtdbL2hUJfw/7GglB7uSw4bbjzbhQV7lRxJrVKjlhAkDJ9AjN/oFSa1UVjap+R5AXWZN2ojmJKz+l+tIVL7n1tC+BpuQFTHitx3eR+nNR1hEdeh9SCdqHs8QmKX3HU94+/5LciHlmoqRycs4+W2c7Go9Wzcvfst/GjYjZpwo4fffD/cFY2CA37PvucyZfz9SS9pERsIMsTlkwhVGZY2iWiauvcxYPyB6uwVA4IjsqGIPM0kJnX19dLQgEEkrdIOSnYhBGhj3HayTagUm8wFLGGmT5YvR2i3rohGi8ARm39idMSxgges0Y7evQlYWok8u9Op6xyi43LaybtAv+kvHhppT5t6iIDJ5i6x61i7Y5l245qYEgV09wZ9VK9rbychULhU93FXuygnwlcO7XDjJ4pFUoCC9PhRz0O7hCKp8EbIEklKIdfit2FO81j8uXUY7WeUsHTH39Hi7rxY5CV749X0Epq5saWqt9YsUhr5Nx/yaw5Xg9WGeytNmo92JfbT0W4mMamjrd2pnR31He5QWWSEYtJmJO1Lqc7hdL+X8dHeEEai+6i6V91O5cGHj2gtQO2CB6wBe3T4quOY3Xf/rEtGNRkRqF3WQCeenWj2/kjoYRYkdpQLJX/YWXDTpOjIVZNpR284Dt3QhreVIcnlw61aWooq7LRZTufI5PMGZS0mFLSJ9sAIl+o+vpZm22pXVOHEnfqXUUSgNaKn6bTTeeWRQUKdLtX3JpwjdcVK9p7w9r4yUMAebw/c3nA5PnJHDKCaPSfx9KZXOk1A9edeHKZjC8zcOFoIzShHOyLXHNFSXFaZjVfIJn7qALK0IM6eTwevTb3EgeU/cmim1HRGBnbEYbNdTx+T2K8pwoKH7svCTMpfuFKywX+to129mrSn58M/N/siUmu4C8lYuznywr+/RcucgkjQyaIGFiwdoyTjc401DunPJ9gCB+yV5nacMpmhNaQ+pGOhNK7skiwsv436Xj86MC/U9yaPseOVn2WjTCdlRB5lvKqTzktGZeHZq6hOgdwlzKQP7czW2elvoo8KNYdKr9/igXZ93IpfPJiFm0N9tJi+jrXkx86wOi6QjBarltUGM0h1H6/fqKdPwvQ7czBvpCVsDVVJ39N4jWgeRjusJ11HZtcju575d7mqR8DvKbwJKw9swIKWEXCp58vBtqFzUN5FLun57xtxW8jn1P720KLRovd0PVWVhZNqPZK22Wea+9OrLlxCh68pOmZBfxEx44fE1UFl9q/zYE0H4ZEgOoRtgx+/GKWSB0gymXF7DmZEIp3Rd82kRtvns4PM9CNO6O5VA2stfYVKvRYgInrp6OzVkRRx3+mtUIQEuVQc1uP24q7HXXHpx+MuzDSijAf/5kb17ap1I4orPoTz0J0x5iS0QYuOUwq7LWIQuTgHFao1gDI6emQpXUINFMWQyA5+geZMfz+eed+rPfKBTmebRX30dhpvldlVuEC6WfO/6iMbUt3H/fjDu178hdaCIk7CpKtp09bVQVWXOkTEueg86kxkltqVTvWGd12xOf2r8DfrDYZMX6Hi5R1v4JkvlsIvRyQpJayj/y37fJj7ajvp8sxjXUfWGA7dQqZ57DQKIal/2mIX6JDMuN2xWjqoKrxrMHayVW+34x06nqArXUQijF2qns2cSlqj3E89vR8bj2jxOUSLvppFduLW63WzM7NjGmLV583PTDryKfSBpNT7IG1Eoq2vGk2NUcZqP9rw8cQz7VilkcFoEPlLO3ZrTIOCifQMUdhuPrbYrcWWom6pceGJddEAGDF9sWt2vm6mleo+XrvRFdhkqYZCuY+qIw1sc18zaWMlUQr+u4Xx20g//fCoO2JWZ8mO1zHrvV9i07GdMePubT6IR2qexf0rn8eJVnMgc2l54ZZLT/11j0lAV0WgjUQzf9+K59Z6DD9+oZDRQHrRZa+1Yhp1KmWaLMJa1YdG0nN71O5lGQteoPw3qFQaSqb0v5s++EKfVo049T35BhfPIsFNBi94RFcSjKfhC5GkhnetmvK0X6xKddkKQf+nWTuS8YlOBy7ivUVSf8TRHgCTYxo6qo9I30yHL4md2urcAvmqZhxdVW+xA/h62ji4bBN9zKeDRmug2c37tGP7hifbUW20XEe6/nuepHxIX2+4A53W53ZvctGMRj9oRBCtXtWOH7ziQp2JHb/4OJHo/5GjEiJpxT6BzvTx2O8QIDZZ3vAizW50QkKYCnFM9jqKs9Ct2RMRDk/xjZFySQxKhZMeaTkgyn7vX1OnTHzss78E1Drx1PGCXqWYMGAUhvcuQ++sAtr57sGBlsPYT9f2b2rxcf0Gas6gJGZtK0P24dmw+HpFZT1roh03kx48Ve47jxr19FSVFtTrD+1Lh4DR5hjQhjQn2eLX1fs7d1ib+NAJba6pp8X2yUMtaDwuo6CPhDo6EbXWbkM1qZvCNlWkcrifVA5bUldFzlkgQG0yWZwJRAO3YPR7qI27+0M0xf0sGE4boxrbqH9Q32tsorUkMq1UCxqxG0/CmFILmSnLaKT+luXx4/ODp5aHoGMU9c8jop/2oD1zZAhRG+s17MI+LtbextHhcy5hWUQ4tR/30zlJ2lljbJwSi/H+vDzaOAUx/9TIDlEzj8SyTyzVz0b+ELsbvsLm47tiZrD9RC3EFY/zZe/GyZJ/DzB/W/vQcJIRZJ74vbH6yXU4OCNvmkliryEJKnmOjry9J4dOP6TDs+jrXksU6Taw2VrCHPX+ACrU2+Bnpp888M1zooF4ldIW5rG6NCRwCqfJJvz4CaH+S6eVdsYJOsIfvomLnq7t4+IdTbe261bGb7fa8fiEufiXD3+F2savO9P2UWllWxNaB/wOzm9ugrPhuxhEEsGvv5fV8UaKqFzOPI87b1G+rUqnIt6ei0l0DMFHu2mdhSwQvj3ajiLdZOmLTzWCxJkHGNc44xDgPm6wiN7VrSjUNosm/gr3rf4ttpHKJqlOkuHq8xpKChvw2OQ7kJdlpNlKaokZn1mv0CfwlIqUDLZj+mDlSfdPC1NPRn0kQxeHHxmBNEOA+zjQLYu7+n5Q4MzH4km/xU1lZO+UZDeheDQWXTsdRXS4FLvYCCx4jk79rI1tSXWIFh3vooUpjTVL7Ow5BiPQ7QhwH08DiV/pBQ5S+whLn8sGXIynvniJdP97laCE/ns6e+AOOvphWtl3IWXQ0QwJVTapiejUTzrP/CU6Z37GeBtGllggJCRhKid2Qx89QJ/eI4uialqAY8cIZCYC3Me7Vcdv1GnGFY3A0v5/wMqvPyHJcyU2HN1qFM3Ur39OX9xy3lRUlU5Blo1sN9klhEAznbq48O3OLbolVDAnYgS6CIEzuY+nHeMXbW6RLLjy7AmBq/7kUaw99Dm2kOXPzhN1OOFqQpM7uI2kt7MAvbJ6oDC7D0YVDsd4GjSG9Cjpom7DxTACjAAjkJkIpCXjV0NZnNuPpPcrApfan+8ZAUaAEWAEEkOAVzwTw41TMQKMACOQsQgw48/YpmPCGQFGgBFIDAFm/InhxqkYAUaAEchYBJjxZ2zTMeGMACPACCSGADP+xHDjVIwAI8AIZCwCzPgztumYcEaAEWAEEkOAGX9iuHEqRoARYAQyFgFm/BnbdEw4I8AIMAKJIcCMPzHcOBUjwAgwAhmLADP+jG06JpwRYAQYgcQQYMafGG6cihFgBBiBjEWAGX/GNh0TzggwAoxAYggw408MN07FCDACjEDGIsCMP2ObjglnBBgBRiAxBJjxJ4Ybp2IEGAFGIGMRYMafsU3HhDMCjAAjkBgCzPgTw41TMQKMACOQsQiYMX7Z7/UcE7U62uTP2Mp1NeGMVVcjzuUxAoyAGQIhHi4bhRsxfhHR52k7tkEkeOItNzN/I+R0foLpC6zYMQKMACOQDgiEeLiPaIli/pIBgWIwKBgwbtbF513zh+UWiz3fIA57MQKMACPACKQpArLP07TzHz+98WDN8+uJxEa6NKobqwnd1uYDG9xtJ776uMdZF/WXrNkFFqst2yQuezMCjAAjwAikAQI+j+sbd/OBT3f946dzD218ZSeR1ExXlCrCSOIX5DvoEpJ+H7oK6MqiS8wEzOJTEDtGgBFgBBiBbkRAqHSEZN9Ol5Dyj9NlyPhtFGDkvOR5MhTgon/B+MXsgBl/CBT+YwQYAUYgzRAIrM8STYLxt9AleLjg5VGuI0YuwuyhSwwQLPFHwccejAAjwAikDQKKxC+YvSd0RS3sCmo7YvwiXDgRR7kCHvzDCDACjAAjkJYICEavXGlJIBPFCDACjAAjwAgwAowAI8AIMAKpRuD/AebmxjtTus0OAAAAAElFTkSuQmCC' },

            scope: ['profile', 'email'],

            accountNameFromProfileInfo: function(context) {

                return context.profileInfo.email;
            },
            
    ...
}
requestProfileInfo
Setting custom redirect URI just for appmixer:google module.
Custom "Connect account" button in the Designer.
Custom "Connect account" button in the Wizard.
here
https://github.com/clientIO/appmixer-demo-embedded-integrations
https://github.com/clientIO/appmixer-demo-firebase-vanilla
Embed into Your Application
examples of real connectors
Asana auth.js module
JIRA auth.js module definition
the JIRA auth.js module definition
axios
Google
Google auth.js
API key
OAuth 1
OAuth 2
Password
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
NodeJS module
context
Connector Configuration

Insights Logs

Browse logs of messages that passed through flows.

Configuration

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

config.el ...

config.flowId

ID of a flow to filter the logs by.

Instance

State

loader

Type: Boolean | Default: null

Toggle a custom loading state.

error

Type: String | Default: null

Toggle a custom error message.

Example

API Module

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

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

Similar to the api.authenticateUser, but uses email instead of username.

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. If the email parameter is not provided, then the username will be copied as email.

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

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

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.

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:

config.el ...

Instance

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

Constructor

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

Configuration

Set up a new appmixer instance with configuration parameters passed directly into the constructor or use the set/get methods:

baseUrl

Type: String | Default: null

Base URL of your Appmixer engine REST API.

accessToken

Type: String | Default: null

Access token of an authorized user.

debug

Type: Boolean | Default: false

Enable debugger for development purposes.

theme

Type: Object | Default: DefaultTheme

l10n

Type: Object | Default: DefaultL10N

Define custom localization texts.

lang

Type: String | Default: en

Specify a language code for the localization of components.

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.

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:

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.

Designer

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

Configuration

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

config.el ...

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:

config.options.autoOpenLogs

Type: Boolean | Default: true

Automatically open logs view when the flow is running.

config.options.triggerSelector

Type: Object | Default: null

Automatically open trigger selector dialog when the flow has no trigger.

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

An event containing an array with flow validation errors. If the array is empty, there are no validation errors in 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

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:

config.el ...

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.

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:

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:

In Appmixer 6, the FlowManager widget is meant to display Automations only. These are regular flows, not the Integrations. This can be overwritten with the customFilter:

config.options.sorting

Type: Object[] | Default: []

Create dropdown inputs with built-in sorting:

Instance

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:

Example

Variables dynamically populated at design time with available Twilio phone numbers.
Authentication form for Freshdesk, defined by auth object

Learn about widget config .

Learn about widget instance .

Name
Description

Get flow. The returned promise resolves to an object with the following information: { id, flow, name, stage, btime, mtime, thumbnail }, where flow is the Flow Descriptor, stage is either 'running' or 'stopped', btime is the time the flow was created ("birth" time), mtime is the time the flow was modified and thumbnail contains a thumbnail image (self-contained, in the format).

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

Learn about widget config .

Learn about widget instance .

Learn about widget config .

Specify Vue under widget to create a custom toolbar button.

Learn about widget instance .

Learn about widget config .

Learn about widget instance .

const insightsLogs = appmixer.ui.InsightsLogs(config)

insightsLogs.set(key, value)
insightsLogs.get(key)
insightsLogs.state(name, value)
const insightsLogs = appmixer.ui.InsightsLogs({
    el: '#insights-logs'
})

insightsLogs.open()

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.

await appmixer.api.authenticateUser(username, password)
await appmixer.api.authenticateWithEmailAndPassword(email, password)
await appmixer.api.signupUser(username, password, [email])
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 => { ... }
const insightsChartEditor = appmixer.ui.InsightsChartEditor(config)

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

insightsChartEditor.open()
const appmixer = new Appmixer({/* [name]: value */})

appmixer.set(name, value)
appmixer.get(name)
appmixer.ui('Widget', {/* ... */})
appmixer.ui.Widget({/* ... */})
appmixer.set(key, value)
appmixer.get(key, value)
appmixer.registerCustomComponentShape(name, shape)
appmixer.registerInspectorField(type, Field, options)
const appmixer = new Appmixer(/* ... */)
const widget = appmixer.ui.FlowManager(config)
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)
const designer = appmixer.ui.Designer(config)

designer.set(key, value)
designer.get(key)
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()
                  }
              }
          }
        }]
    ]
  }
}
const designer = appmixer.ui.Designer({
  /* ... */
  options: {
      autoOpenLogs: true
      toolbar: [
        ['logs']
    ]
  }
}
const designer = appmixer.ui.Designer({
  /* ... */
  options: {
    triggerSelector: {
      enabled: true,
      featured: [
        {
          name: 'appmixer.utils.timers.Timer'
        },
        {
          name: 'appmixer.utils.controls.OnStart',
          label: 'Custom label',
          description: 'Custom description',
          marker: 'Custom marker text',
          icon: 'data:image/svg+xml;base64,...',
        }
      ]
    }
  }
}
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:validation', errors => {
    console.log('flow:validation', '===>', errors);
});

// Example
[
    {
        "keyword": "required",
        "dataPath": ".text",
        "schemaPath": "#/required",
        "params": {
            "missingProperty": "text"
        },
        "message": "Should have required property \"Message\".",
        "schema": {
            "text": {
                "type": "string"
            }
        },
        "parentSchema": {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string"
                }
            },
            "required": [
                "text"
            ]
        },
        "data": {
            "message.d9a25ebe-84ef-4460-a061-f9acac76d28f.out.lambda": {}
        },
        "componentId": "d1c48d6f-0225-46a8-9600-1c19adf75768",
        "descriptorPath": "config.transform.message.d9a25ebe-84ef-4460-a061-f9acac76d28f.out.lambda.text",
        "fieldLabel": "Message"
    }
]
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()
const flowManager = appmixer.ui.FlowManager(config)

flowManager.set(key, value)
flowManager.get(key)
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: {
        customFilter: {
            type: [                  // each flow has a 'type' property
                'automation',        
                'integration-instance'
            ]
        }
    }
});
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()
Insights Logs
Insights Chart Editor
Designer
Flow Manager
Flow Manager Menu
Flow Manager Filters
Flow Manager Sorting
Flow Manager Sharing
Data URI
ComponentOptions
here
here
here
here
here
here
here
here
GET /accounts

Custom Theme

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

Basic usage

To customize the UI widgets, you need to specify a theme JSON object either in the Appmixer constructor:

const appmixer = new Appmixer({
    theme: {
        variables: {
            font: {
                family: "sans-serif"
            },
            colors: {
                neutral: "orange"
            }
        }
    }
});

and/or use the option with individual widgets:

const flowManager = appmixer.ui.FlowManager({
    el: "#my-flow-manager",
    theme: {
        variables: {
            colors: {
                neutral: "purple"
            }
        }
    }
});

Usage with multiple themes

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

// 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: {
            neutral: 'green'
        }
    }
});

Variables

The easiest way to change the overall styling is to use the theme variables. The following example shows a complete list of variables that you can set in your theme to match your product branding:

appmixer.set('theme', {
    mode: 'light', // Determines the color mode of the theme: 'light' or 'dark'.
    variables: {
        // Font variables including font family, weights, and size.
        font: {
            family: '\'SF Pro Text\', \'Helvetica Neue\', \'Helvetica\', \'Arial\', sans-serif',
            familyMono: '\'SF Mono\', \'ui-monospace\', Menlo, monospace',
            weightRegular: 400,
            weightMedium: 500,
            weightSemibold: 600,
            weightBold: 700, // Added key for bold font weight.
            size: 14
        },
        // Color variables for various UI elements, with light and dark mode defaults.
        colors: {
            background: '#FFFFFF', // Color of the background. The areas accomodate surfaces.
            surface: '#FFFFFF', // Color of the surfaces above background and other surfaces.
            separator: '#E0E0E2', // Separator is a special color for various borders and lines.
            neutral: '#1F2338',
            primary: '#2A64F6', // Colors for primary, secondary and tertiary actions of the user.
            secondary: '#6B7EB3',
            tertiary: '#8C6C87',
            error: '#B3261E',
            warning: '#B56C09',
            success: '#08B685',
            modifier: '#C558CF', // Special color for variables that have been modified via Modifiers.
            highlighter: '#FFA500'
        },
        // Shadow variables for different elevation levels and UI elements.
        shadows: {
            level0: '0 0 4px 0 rgba(60, 64, 67, 0.3)', // Shadows from level 0 to 5 serve to assist with elevation levels between surfaces.
            level1: '0 0 1px rgb(125 125 126)',
            level2: '0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.12)',
            level3: '0 2px 4px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 1px rgba(60, 64, 67, 0.12)',
            level4: '0 3px 9px 0 rgba(60, 64, 67, 0.15), 0 3px 12px 1px rgba(60, 64, 67, 0.12)',
            level5: '0 4px 8px 0 rgba(60, 64, 67, 0.3), 0 4px 12px 1px rgba(60, 64, 67, 0.12)',
            backdrop: 'rgba(0 0 0 / 88%)', // Backdrop is a special shadow to cover the background of modals and popups.
            blur: 'rgba(0 0 0 / 10%)', // Blur is an alternative for "backdrop" when the background is blurred instead.
            popover: '0 3px 9px rgba(0 0 0 / 12%)', // Popover is a special shadow for popups that should be at the maximum elevation.
            icon: '0 1px 3px rgb(0 0 0 / 6%)'
        },
        // Radius variables for element and container corners.
        corners: {
            elementRadiusSmall: '3px',
            elementRadiusMedium: '6px',
            elementRadiusLarge: '9px',
            elementRadiusRound: '300px',
            containerRadiusSmall: '3px',
            containerRadiusMedium: '6px',
            containerRadiusLarge: '9px'
        },
        // Border width variables for UI elements.
        dividers: {
            regular: '1px', // Width of border lines and separator lines in the UI.
            medium: '2px',
            semibold: '3px',
            bold: '6px',
            extrabold: '9px'
        }
    }
})

Shapes

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

appmixer.set('theme', {
    ui: {
        shapes: {
            action: "action",
            trigger: "trigger",
            selection: "selection"
        }
    }
})

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

action

action-vertical

action-dark

action-vertical-dark

trigger

trigger-vertical

trigger-dark

trigger-vertical-dark

selection

selection-vertical

selection-dark

selection-vertical-dark

action/trigger

action-vertical/trigger-vertical

action-dark/trigger-dark

Special version of actions and triggers that works better on dark backgrounds.

action-vertical-dark/trigger-vertical-dark

Special version of vertical actions and triggers that works better on dark backgrounds.

Charts

Charts (used in the Insights widgets - Insights Logs, Insights Chart Editor and Insights Dashboard) 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.

appmixer.set('theme', {
    ui: {
        charts: {
            legendFontSize: "12px",
            legendFontFamily: "sans-serif",
            legendFontColor: "black",
            tickFontSize: "black",
            tickFontFamily: "monospaced",
            tickFontColor: "black",
            gridColor: "lightgray",
            colorway: [
                '#493843',
                '#61988E',
                '#A0B2A6',
                '#CBBFBB'
            ]
        }
    }
})

Advanced UI Styling

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.

appmixer.ui.FlowManager({
    el: '#app',
    theme: {
        ui: {
            '#FlowManager': {
                background: 'lightblue',
                '#header': {
                    padding: '0 0 24px 0',
                    '#buttonCreateFlow': {
                        color: 'yellow',
                        '@hovered': {
                            color: 'white'
                        }
                    }
                }
            }
        }
    }
});

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.

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:

wget  https://my.appmixer.com/appmixer/package/theme-light.json
wget  https://my.appmixer.com/appmixer/package/theme-dark.json

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

People Tasks

Manage tasks created by utility components of flows.

Configuration

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

const peopleTasks = appmixer.ui.PeopleTasks(config)

peopleTasks.set(key, value)
peopleTasks.get(key)

config.el ...

Instance

State

peopleTasks.state(name, value)

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({
    el: '#people-tasks'
})

peopleTasks.open()

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

Instance

State

connectors.state(name, value)

loader

Type: Boolean | Default: null

Toggle a custom loading state.

error

Type: String | Default: null

Toggle a custom error message.

Example

const connectors = appmixer.ui.Connectors({
    el: '#connectors'
})

connectors.open()

Wizard

Manage a flow that is used as an integration instance.

Configuration

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

const wizard = appmixer.ui.Wizard(config)

wizard.set(key, value)
wizard.get(key)

config.el ...

config.flowId

Type: String | Default: null

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

Instance

State

wizard.state(name, value)

loader

Type: Boolean | Default: null

Toggle a custom loading state.

error

Type: String | Default: null

Toggle a custom error message.

Events

wizard.on(event, handler)

flow:start

wizard.on('flow:start', flowId => {/* ... */})

Submit the form and start the flow.

flow:validation

wizard.on('flow:validation', errors => {/* ... */})

Flow validation errors changed.

cancel

wizard.on('cancel', () => {/* ... */})

Click a button to close the form.

close

wizard.on('close', () => {/* ... */})

Submit the form and wait for the flow to start.

Example

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

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:

var appmixer = new Appmixer({ baseUrl: BASE_URL });
appmixer.set('strings', STRINGS);

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:

appmixer.set('strings', {
    ui: {
        flowManager: {
            search: 'Search flows',
            header: {
                buttonCreateFlow: 'Create new Flow'
            }
        }
    }
});

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.

wget https://my.appmixer.com/appmixer/package/strings-en.json

Time Localization

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

appmixer.set('strings', {
  time: {
    months: [...],
    monthsShort: [...],
    weekDaysShort: [...],
    ordinal(number){ ... },
    relativeTime: {...}
  }
});

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:

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

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:

appmixer.set('strings', {
    ui: {
        flowManager: {
            pagination: '{{range}} of {{total}} flow|{{range}} of {{total}} flows'
        }
    }
});

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.

Storage

Manage records associated with data storage utility components of flows.

Configuration

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

const storage = appmixer.ui.Storage(config)

storage.set(key, value)
storage.get(key)

config.el ...

config.storeId

Type: String | Default: []

ID of a store to open within the storage.

Instance

State

storage.state(name, value)

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({
    el: '#storage'
})

storage.open()

Insights Dashboard

Browse and manipulate charts created by the current user.

Configuration

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

const insightsDashboard = appmixer.ui.InsightsDashboard(config)

insightsDashboard.set(key, value)
insightsDashboard.get(key)

config.el ...

Instance

State

insightsDashboard.state(name, value)

loader

Type: Boolean | Default: null

Toggle a custom loading state.

error

Type: String | Default: null

Toggle a custom error message.

Events

insightsDashboard.on(event, handler)

chart:clone

insightsDashboard.on('chart:clone', chartId => {/* ... */})

Clone chart.

chart:remove

insightsDashboard.on('chart:remove', chartId => {/* ... */})

Remove chart.

chart:open

insightsDashboard.on('chart:open', chartId => {/* ... */})

Open chart in Chart Editor.

Example

const insightsDashboard = appmixer.ui.InsightsDashboard({
    el: '#insights-dashboard'
})

insightsDashboard.open()

Getting Started

Appmixer Backoffice is an administration UI for Appmixer. Use it to install, update and configure connectors, have an oversight of all the flows and users in Appmixer and set system configuration.

The Appmixer Backoffice is accessible via the "Admin" menu item in the Appmixer Studio:

Custom API

Appmixer SDK allows you to override API methods used by the SDK instance. This can be handy in edge case scenarios where you need to override the API requests and their parameters or response values.

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:

An example how to redefine the flow update request.

Getting Started

Appmixer command-line tool.

Use the Appmixer CLI tool to interact with the Appmixer engine remotely from the command line. It can be used to list flows, start/stop flows but more importantly, to develop and publish custom components.

Installation

  • npm install -g appmixer

Help

Display the command options with the -h option:

Each command has its own help information:

Initialization

Login to your Appmixer account and enter your password:

Creating Custom Components

Generate a sample component

The best way to start implementing your own custom components is to use the generator tool to generate a sample component. This gives you the basic skeleton of your component/service that you can later tweak. Use the appmixer init example command to generate a sample component:

As you can see, we have to give this command a type of example we want to generate. We will start with the most simple service type that does not use any authentication (OAuth1, OAuth2, API keys). In other words, the component will not ask the user to connect any account in the Inspector panel when using the component in a flow.

The command prints all the generated files and the directory structure. Note that we have just generated a working component with the myservice.mymodule.MyComponent type.

Pack your component

Now we're ready to pack our service (i.e. create a zip archive with all the generated files) using the appmixer pack appmixer/myservicecommand:

You can pack the entire service or just the module or even individual components by providing the path to the appmixer pack command.

The pack command generated the appmixer.myservice.zip file in the current directory.

Publish your component

The last step is to publish our component for our users to use. This is done using the appmixer publish command:

Our component is now published and ready to be used:

Notice the labels and icons of the service in the component panel on the left and of the component in the Inspector selector match our definitions from the myservice/service.json and myservice/mymodule/MyComponent/component.json manifest files.

List all available components

To see all the available components uploaded to Appmixer, use the appmixer component ls command:

Remove your component

If you decide your component is no longer needed, you can remove your component from the system with the appmixer remove command:

The remove command lets you specify any portion of the fully qualified component type. For example, if you want to remove the entire service, you can do that with:

It's important to note that removing a component that is used in any of the running flows will cause the flows to stop working. The flows will start generating errors that will be visible in the Insights section. The remove command does not automatically stop the flows. Always make sure the component is not used in any of the running flows before you remove it. IMPORTANT: Removing a component cannot be undone!

Updating your component

You can re-publish (appmixer publish) your component which effectively replaces the old component with the new one.

Note that if you "dramatically" change your component, e.g. removing ports, flows using the component type might require re-configuration since the ports that were used to connect to other components will not exist. The same goes for other major changes: changing output parameters that are used in connected components, changing inspector config and properties, etc.

Testing your component

If want to run the next commands on Windows, make sure you escape the JSONs correctly. For example, instead of

appmixer test component HelloAppmixer -i '{"in": {"text": "abc"}}'

you have to use

appmixer test component HelloAppmixer -i "{\"in\": {\"text\": \"abc\"}}"

Writing/updating code of your component, re-publishing it and re-configuring/restarting your flow every time you need to test your component would be a long process. Therefore, the Appmixer CLI tool provides a command to test your component locally on your machine, before publishing it to Appmixer. The appmixer test command allows you to test your component by sending messages to it, configuring properties and testing authentication methods:

We'll start by exploring the component testing tool:

Let's say we want to send a message to our component and see how it reacts, i.e. what is the output of our component. We know our component has an input port called in and requires the sourceData property as part of the incoming messages (see the component.json file of your MyComponent, especially the inPorts section). We can use the test command for this:

The command prints out a lot of useful information, for example, the output of the component (see "Component sent a message to its output port: out"). Since our component just forwards the same data that it received, we see the same object we sent to it: { sourceData: 'foo' }.

Now suppose we have a bug in our code in MyComponent.js, a syntax error:

Now re-running the test gives us:

Downloading your component

When your component is published, you can always download it back to your local file system. To download the source code of your component, use the appmixer download command:

In our case, the download command would look like:

Working With Flows

Listing Flows

You can list all your flows using the appmixer flow ls command:

If you want to see just one flow and its stage (running/stopped), you can pass the ID of the flow in the flow ls command:

To see the flow descriptor of a flow (i.e. JSON object that represents the entire configuration of the components in the flow and their connections), add the --descriptor or -d flag:

Starting and Stopping Flows

You can start and stop flows using the appmixer flow start and appmixer flow stop commands:

Removing Flows

To remove flows, use the appmixer flow remove command:

Note that the removed flow will be automatically stopped if it was running. Also, note that this action cannot be undone.

Working with Modifiers

You need admin privileges to run these commands. You should not modify Modifiers if there are any running flows using them. Such an operation would break the flows.

Downloading Modifiers

The next command will download modifiers from Appmixer and save them into the modifiers.json file.

Publishing Modifiers

The next command will publish modifiers into Appmixer.

Deleting Modifiers

The next command will delete existing modifiers from Appmixer. The next time Appmixer starts, it will load the default set of Modifiers.

Accounts

Manage accounts authorized by the current user.

Configuration

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

config.el ...

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

Integrations

Manage flows used as integration templates and instances.

Configuration

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

config.el ...

config.options

Type: Object | Default: {}

config.options.customFilter

Type: Object | Default: {}

Filter the integrations with additional parameters:

Instance

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

Custom Component Strings

Appmixer lets you manage the components' inspector fields through the manifest or the strings object.

There are two ways how to customize component's strings:

  • Through a localization object inside the component's manifest

  • Adding custom strings to components namespace inside strings object

Component's manifest localization object

You can include custom strings inside the component's manifest using a localization object. The following is an example how to do it:

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

service.json and module.json localization

Example using localization object in service.json

Example using Strings object

It follows the same pattern as in components, but we use the service/module path as a key for the definition:

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.

While the advanced theme styling gives you the most flexibility in customizing Appmixer UIs, we cannot guarantee that in some cases, the structure and nesting of the selectors might change between Appmixer versions. It is therefore recommended to use instead.

FlowManager Dark Theme
Designer Dark Theme
Insights Logs Dark Theme
Insights Chart Editor
People Tasks

Learn about widget config .

Learn about widget instance .

Learn about widget config .

Learn about widget instance .

Wizard

Learn about widgetconfig .

Learn about widget instance .

Storage

Learn about widget config .

Learn about widget instance .

Insights Dashboard

Learn about widget config .

Learn about widget instance .

The list of API methods can be found .

Download and install NodeJS: (version 18 is required)

First set the Appmixer API URL. This is the URL of your hosted Appmixer engine instance or your own custom URL where the self-managed engine is located. If you have a trial package or local installation, you can use .

Before you publish a component. Your user account has to have property vendor set to a string or an array of strings. The value depends on what vendor is used in component(s) you're about to publish. More about that in . In the examples in this section we use appmixer as a vendor, but you should use your own. If your company is called acme then the vendor property should be set to acme as well. You can use Backoffice for that as shown in the next picture.

Learn about widget config .

Learn about widget instance .

Learn about widget config .

Learn about widget instance .

As you can see, there's a localization object at the end whose keys are language codes. This allows you to support multiple languages. Each value is an object whose keys are paths to the elements that will be customized (label, tooltip, placeholder, etc). The paths follow syntax.

Each key in components object is the path to the component and the value is an object whose keys are the paths to elements (label, tooltip, placeholder, etc). This path follows the syntax. For more information about the Strings Object refer to the section.

Not only you can localize component's strings, but also services and modules. This allows you to change the label and description of the applications in the designer's left-side panel (the one you drag the applications from). To do it we can use either localization object in the service.json or module.json manifest or use the .

theme.variables
here
here
here
here
here
here
here
here
here
here
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 });
/* 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
            });
        }
    }
});
$ appmixer -h
Usage: appmixer [options] [command]

Appmixer command line interface.

Options:
  -v, --version  output the version number
  -h, --help     output usage information

Commands:
  download|d     Download component.
  flow|f         Flow commands.
  init|i         Initialize component.
  login|l        Login into Appmixer API.
  logout|o       Logout from Appmixer API.
  pack|p         Pack component into archive.
  publish|pu     Publish component.
  remove|rm      Remove component.
  test|t         Test component, authentication module, ...
  url|u <url>    Set Appmixer API url.
  help [cmd]     display help for [cmd]

Go to https://docs.appmixer.com/appmixer/ to find more information.
$ appmixer flow -h
Usage: appmixer flow <command>

Flow commands.

Options:
  -h, --help         output usage information

Commands:
  start|s <flowId>   Start flow.
  stop|t <flowId>    Stop flow.
  remove|r <flowId>  Remove flow.
  ls|l [flowId]      Ls flow.
  help [cmd]         display help for [cmd]
$ appmixer url https://api.appmixer.com
$ appmixer login david@client.io
prompt: password:

Login successful.
$ appmixer init example

Usage: appmixer init example <type> [path]

Options:
  --vendor [vendor]  Your vendor name.
  --verbosity        Verbosity level of the generated output log. Number in [0 - 2] range. Defaults to 1.
  -h, --help         output usage information

Examples:
  $ appmixer init example no-auth

Environment Variables:
AM_GENERATOR_COMPONENT_PATH		Components base directory. [path] argument overrides this variable if used.
$ appmixer init example no-auth
Created directory structure ./appmixer/myservice/mymodule/MyComponent
Creating component appmixer.myservice.mymodule.MyComponent
Created component manifest file ./appmixer/myservice/mymodule/MyComponent/component.json.
Created component package file ./appmixer/myservice/mymodule/MyComponent/package.json.
Created component behaviour file ./appmixer/myservice/mymodule/MyComponent/MyComponent.js.
Created service manifest file ./appmixer/myservice/service.json.
Created quota module file ./appmixer/myservice/quota.js.

Example successfully generated.

my-components
└── appmixer
    └── myservice
        ├── mymodule
        │   └── MyComponent
        │       ├── MyComponent.js
        │       ├── component.json
        │       └── package.json
        ├── quota.js
        └── service.json

You can now use appmixer pack appmixer/myservice && appmixer publish commands to upload it.
$ appmixer pack appmixer/myservice
Packing component directory: /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice

Files found in /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice
- mymodule
- quota.js
- service.json

You are in a directory with service.json file.
I'm going to create directory structure based on name in your service.json file.


3866 total bytes
appmixer.myservice.zip
$ appmixer publish appmixer.myservice.zip
Publishing archive: /Users/daviddurman/Projects/appmixer/my-components/appmixer.myservice.zip
Published.
$ appmixer component ls
appmixer.actimo.contacts.CreateContact
appmixer.actimo.contacts.DeleteContact
appmixer.actimo.contacts.GetContact
appmixer.actimo.contacts.GetContacts
appmixer.actimo.contacts.UpdateContact
appmixer.actimo.groups.GetGroups
appmixer.actimo.messages.SendMessage
appmixer.apify.crawlers.Crawl
appmixer.asana.projects.CreateProject
appmixer.asana.projects.NewProject
appmixer.asana.tasks.CreateStory
appmixer.asana.tasks.CreateSubtask
appmixer.asana.tasks.CreateTask
appmixer.asana.tasks.NewComment
...
appmixer remove appmixer.myservice.mymodule.MyComponent
appmixer remove appmixer.myservice
$ appmixer test -h
Usage: appmixer test <command>

Dev tools for testing your files.

Options:
  -h, --help                   output usage information

Commands:
  dump|d <moduleName>          Get stored authentication data from previous commands.
  auth|a <authModuleFile>      Authenticate service.
  component|c <componentFile>  Test component.
  help [cmd]                   display help for [cmd]
$ appmixer test component -h
Usage: appmixer test component [options] [componentDir]

Options:
  -f, --transform [transform]    specify transformer
  -i, --input [input]            input test message object (default: [])
  -m, --mime [mime]              mime type, application/json by default
  -p, --properties [properties]  component properties (JSON format)
  -s, --no-state                 do not show component's state
  -t, --tickPeriod [tickPeriod]  tick period (in ms), default is 10000 ms
  -h, --help                     output usage information

Examples:
  Following example will send input message { "to": "your@email.com" } to component's input port 'in'.
  You always have to specify to which input port you want to send message.
  $ appmixer test component [path-to-your-component-directory] -i '{ "in": { "to": "your@email.com" } }'

  This is how to specify transformer function from transformer file.
  $ appmixer test component [path-to-component] -i '{}' -f './transformers#channelsToSelectArray'

  How to set properties and tick period:
  $ appmixer t c [path-to-component] -p '{ "channelId: "123XYZ" }' -t 2000

  You can send more than one message:
  $ appmixer t c [path-to-component] -i '{ "in": { "to": "first@email.com" }}' -i '{ "in": { "to": "second@email.com" }}'

  You can run appmixer command in your component's directory:
  $ appmixer test c
  Directory has to contain component.json file and component's source code file.

If you're developing component that needs authentication, use 'appmixer test auth' before.
If you're developing Oauth2 component you might need to refresh access token before calling this command.
Use 'appmixer test auth refresh' for such purposes.

Some of the feature (context.store, context.componentStaticCall, ... will work only if you are logged in into Appmixer.
If you want to test them in your component, call appmixer login first.

For more information, checkout our documentation at:
https://docs.appmixer.com/appmixer/component-definition/authentication
https://docs.appmixer.com/appmixer/appmixer-trial/custom-component-helloappmixer
$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:
{"name":"component","hostname":"MacBook-Pro.local","pid":26397,"level":30,"msg":"{\"properties\":{\"correlationId\":\"8f2ce09e-3f6d-48dd-81bd-e80189f70bb4\",\"gridInstanceId\":null,\"contentType\":\"application/json\",\"contentEncoding\":\"utf8\",\"sender\":{\"componentId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"type\":\"appmixer.myservice.mymodule.MyComponent\",\"outputPort\":\"out\"},\"destination\":null,\"correlationInPort\":null,\"componentHeaders\":{},\"signal\":false},\"content\":{\"sourceData\":\"foo\"},\"scope\":{\"_walkthrough\":[{\"targetId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"links\":[]}]}} { componentId: '70eb49e9-88df-4d0e-9549-d4e4f0f755c0',\n  flowId: 'c5d05118-13b5-4ec8-a8fe-2e6ef51fdd52',\n  userId: '5da735715abc4a671dfb592f',\n  componentType: 'appmixer.myservice.mymodule.MyComponent',\n  type: 'data',\n  portType: 'out',\n  port: 'out',\n  inputMessages: { in: [ [Object] ] },\n  annotatedMsg: { sourceData: 'foo' } }","time":"2019-10-16T15:21:22.169Z","v":0}

Component sent a message to its output port: out
{ sourceData: 'foo' }

Component's receive method finished in: 45 ms.

Return value from receive method:
undefined

Component's state at the end:
State is empty, component did not store anything into state.

Stopping component.

Destroying component.
module.exports = {
    receive(context) {
        myBadError
        context.sendJson(context.messages.in.content, 'out');
    }
}
$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:

[ERROR]: myBadError is not defined
$ appmixer download -h
Usage: appmixer download selector

Options:
  -o, --out [dir]  Where you want save it.
  -h, --help       output usage information

Examples:
  Download all files for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail

  Download only package.json file for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail/package.json

  Download main SendEmail.js file only:
  $ appmixer download vendor.google.gmail.SendEmail/SendEmail.js

  Download all gmail components:
  $ appmixer download vendor.google.gmail

  Download auth.js file for gmail:
  $ appmixer download vendor.google.gmail/auth.js

  Download quota.js file for gmail:
  $ appmixer download vendor.google.gmail/quota.js
$ appmixer download appmixer.myservice
$ ls
appmixer.zip
$ unzip appmixer.zip
$ tree appmixer/
appmixer
└── myservice
    ├── mymodule
    │   └── MyComponent
    │       ├── MyComponent.compiled.js
    │       ├── MyComponent.js
    │       ├── component.json
    │       ├── package-lock.json
    │       ├── package.json
    │       └── sourcemap-register.js
    └── service.json

3 directories, 7 files
$ appmixer flow ls
[Get Current Weather] : [a5769b32-8835-44ad-82e1-ece2874ea3e3] : [stopped]
[Uptime Monitor] : [5b5fd3a0-0ef2-4fc5-9a60-164f5e44c660] : [stopped]
[Daily Rainy Day Alert] : [ec1103e5-c66c-41c4-9223-029d2b328f5c] : [stopped]
$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow: a5769b32-8835-44ad-82e1-ece2874ea3e3
Stage: stopped
$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3 --descriptor
{
    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
        "type": "appmixer.utils.controls.OnStart",
        "label": "OnStart",
        "source": {},
        "config": {},
        "x": 88,
        "y": 110
    },
    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
        "type": "appmixer.utils.weather.GetCurrentWeather",
        "label": "GetCurrentWeather",
        "source": {
            "location": {
                "5ba2740c-929b-4599-b09e-fc8f8d5dac82": [
                    "out"
                ]
            }
        },
        "config": {
            "transform": {
                "location": {
                    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
                        "out": {
                            "type": "json2new",
                            "lambda": {
                                "city": "Prague",
                                "units": "metric"
                            }
                        }
                    }
                }
            }
        },
        "x": 286,
        "y": 110
    },
    "ab6a22f8-916d-4aab-a5e3-2abedc03917c": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
            "in": {
                "0f366972-08fe-4cd4-80c0-227cfb6db54b": [
                    "weather"
                ]
            }
        },
        "config": {
            "transform": {
                "in": {
                    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
                        "weather": {
                            "type": "json2new",
                            "lambda": {
                                "from_email": "info@appmixer.com",
                                "subject": "Appmixer: Current Weather",
                                "text": "Temperature: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.temp}}} dgC\nPressure: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.pressure}}} hPa\nHumidity: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.humidity}}}%\nCloudiness: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.clouds.all}}}%",
                                "to": ""
                            }
                        }
                    }
                }
            }
        },
        "x": 484,
        "y": 110
    }
}  
$ appmixer flow start a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully started.

$ appmixer flow stop a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully stopped.
$ appmixer flow remove 2058a1ee-9c19-4e94-bd7a-0da7f9bed973
Flow 2058a1ee-9c19-4e94-bd7a-0da7f9bed973 successfully removed.
$ appmixer modifiers get
$ appmixer modifiers publish file-with-your-modifiers.json
$ appmixer modifiers delete
associated with a "vendor."
const accounts = appmixer.ui.Accounts(config)

accounts.set(key, value)
accounts.get(key)
accounts.state(name, value)
accounts.on(event, handler)
accounts.on('flow:open', flowId => {/* ... */})
const accounts = appmixer.ui.Accounts({
    el: '#accounts'
})

accounts.open()
const integrations = appmixer.ui.Integrations(config)

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

// Will use the strings under 'cs' key
appmixer.set('lang', 'cs')

// Will switch the strings to the ones under 'sk' key
appmixer.set('lang', 'sk')
var appmixer = new Appmixer();

var mySkStrings = { /* Strings definition for sk language */ };
var myCsStrings = { /* Strings definition for cs language */ };

// This function will be called when the user clicks on some
// "Switch to sk" button
function setLangToSk() {
    appmixer.set('lang', 'sk');
    appmixer.set('strings', mySkStrings);
}

// This function will be called when the user clicks on some
// "Switch to cs" button
function setLangToCs() {
    appmixer.set('lang', 'cs');
    appmixer.set('strings', myCsStrings);
}
{
    "components": {
	"appmixer.twilio.sms.SendSMS": {
	    "inPorts[0].inspector.inputs.body.label": "Textová zpráva",
    	    "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
            "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo"
        }
    }
    // Other namespaces (designer, storage, accounts...)
}
{
    "name": "appmixer.twilio",
    "label": "Twilio",
    "category": "applications",
    "description": "Twilio is an easy tool for developers to send and receive SMS and voice calls.",
    "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMj...",
    "localization": {
        "cz": {
            "label": "Modul Twilio",
            "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
        },
        "sk": {
            "label": "Modul Twilio",
            "description": "Twilio je ľahký nástroj pre vývojárov na odosielanie a prijímanie SMS a hlasových hovorov."
        }
    }
}
"appmixer.twilio": {
    "label": "Modul Twilio",
    "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
}
appmixer.set('strings', {
    ui: {
        designer: {
            stencil: {
                groups: {
                    applications: 'Connectors',
                    utilities: 'Tools'
                }
            }
        }
    }
});
Custom Component Strings
here
Accounts
Integrations
https://nodejs.org
http://localhost:2200
JSON path
JSON path
Custom Strings
Strings Object
here
here
here
here
here
Enabling Users to Publish Custom Components
Enabling Users to Publish Custom Components
Forgot Password Service

Authentication Hub

The Authentication Hub is enabled for all hosted Appmixer tenants. In self-managed installations, the Authentication Hub has to be explicitly set.

Basic Setup

To start using the Authentication Hub, the system plugin auth-hub has to be turned on. This is automatically turned on for Hosted Appmixer tenants and can be turned on by adding the auth-hub plugin ID to the SYSTEM_PLUGINS ENV variable.

# Turning on the plugin
SYSTEM_PLUGINS=auth-hub

# Required variables
AUTH_HUB_URL=             # URL of the Authentication Hub
AUTH_HUB_TOKEN={token}    # Provided by Appmixer

# Optional variables
AUTH_HUB_AUTOMATIC=true|false    # true by default

Additional Configuration

You can redirect all OAuth requests to the Authentication Hub, or you can use it just for a selected set of connectors.

If the AUTH_HUB_AUTOMATIC is set to true then all OAuth requests for all installed connectors that do not have clientId/clientSecret (OAuth 2), or consumerKey/consumerSecret (OAuth 1) configured through the Backoffice (Connector Configuration), will be redirected to the Authentication Hub. This is useful, if you want to use all the available connectors, but don't want to create all the necessary OAuth apps.

If the AUTH_HUB_AUTOMATIC is set to false, Appmixer checks the connector configuration. If the connector has clientId/clientSecret (OAuth 2), or consumerKey/consumerSecret (OAuth 1) configured through the Backoffice (Connector Configuration), Appmixer will not use the Authentication Hub (all OAuth requests will go directly from your Appmixer tenant to the relevant OAuth 3rd party). If you add authHubUrl to the connector configuration, all the authentication requests will be redirected to the Authentication Hub URL provided:

Installation Docker Compose

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

Prerequisites

Installation

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

Install and Start Appmixer

docker-compose --project-name appmixer up

Stop Appmixer

docker-compose --project-name appmixer stop

Stop and Clean Up

Stop Appmixer and remove all containers and images:

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

Using Webhook Components

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

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

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

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

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

with the URL from ngrok:

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

Now restart Appmixer:

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

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

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

But this command will set the GRIDD_URL to https://568284c4.ngrok.io as well.

Creating an Admin User

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

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

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

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

Enabling Users to Publish Custom Components

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

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

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

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

System Configuration

Please be aware that certain configuration changes may not take immediate effect without restarting the Appmixer engine. For customers with a Self-Managed Appmixer installation, restarting the engine can be done at your convenience to apply the new settings. For those with a hosted Appmixer tenant, it's advisable to reach out to our support team at support@appmixer.com. Our team can provide guidance on how to effectively set these configuration options and assist with any necessary engine restarts to ensure your configurations are applied as intended.

Configuration options

Below is a list of available configuration options, accompanied by a brief explanation for each and their default values. These defaults are used by Appmixer in instances where no specific value is provided:

Key
Detail
Default value
Needs restart

API_USER_CREATE_SCOPE

By default, the POST /user API is open to enable the sign-in feature for everyone. This option can restrict the access to this endpoint. It takes a list of scopes (comma-separated). If the value is not null, then a JWT token has to be used to call this API. Typically, the value is set to admin.

null

APP_NAME

This will for example appear in the head title of a sign-in popup for Api Key services.

Appmixer

AUTH_HUB_AUTOMATIC

If the auth-hub system plugin is on and this value is true, all OAuth requests for unconfigured services will go through the Authentication Hub. It means that if you install Slack, for example, and do not configure the clientId and clientSecret the engine will use the Appmixer Authentication Hub for Slack authorization.

true

DEFAULT_USER_VENDOR

Vendor assigned to newly created users.

No value

AUTH_POPUP_DISPLAY_ERR

true

AUTH_POPUP_TIMEOUT_ERR

How many seconds before automatically closing the Connecting Account Failed popup window.

5

BROKER_MESSAGE_ACK_TIMEOUT

Timeout for message processing.

1500000

COMPONENT_FACTORY_TIMEOUT

An attempt to create a component will fail after this timeout.

300000

COMPONENT_RECEIVE_TIMEOUT

A message will be retried if the receive() function does not return within this timeout.

1380000

LIMIT_FLOW_UPDATE_BYTES

The max size in bytes of a flow descriptor to be able to be saved.

2097152

LIMIT_CC_ARCHIVE_MAX_BYTES

Maximum size in bytes for custom components.

10485760

LIMIT_WEBHOOK_BYTES

Maximum payload size in bytes for webhook components.

1048576

WEBHOOK_REQUEST_TIMEOUT

Timeout in milliseconds for webhook component requests.

10000

LIMIT_COMPONENT_STATIC_CALL_MAX_BYTES

Maximum size in bytes of the payload for component static calls.

104857600

PUBLIC_FILES_PREFIX

RETRY_INIT_EXP

In case of an error, message for a Component is rescheduled for another attempt. Backoff strategy is used. This retryInitExpiration is the initial value for the backoff, it says after how many milliseconds the first attempt after a failure is going to happen.

60000

RETRY_MAX_COUNT

How many times does the engine try to process the message before rejecting it forever (and moving into the Unprocessed Messages collection).

30

RETRY_MAX_EXP

Maximum interval in milliseconds between retry attempts.

3600000

DISPATCHER_PREFETCH_COUNT

The maximum number of Rabbit messages being dispatched at the same time.

500

INPUT_QUEUE_PREFETCH_COUNT

The maximum number of outgoing Rabbit messages waiting for aknowledgement at the time in the Input Queue. Subsequent incoming messages will not be sent until pending messages are aknowledged.

300

WEBHOOK_PREFETCH_COUNT

This is for webhooks from Appmixer to registered URLs. This is the amount of webhook messages that will be processing at a time.

50

WEBHOOK_RETRY_COUNT

Number of times that Appmixer will retry sending a webhook. Applies for all webhooks.

20

WEBHOOK_RETRY_INTERVAL

Initial interval in milliseconds for retries. Subsequent retries will take longer (multiplied by an internal factor).

30000

WEBHOOK_RETRY_MAX

Maximum interval in milliseconds between retries.

1800000

WEBHOOK_USER_CREATED

URL that will be called when new user is created (sign-up).

No value

WEBHOOK_FLOW_COMPONENT_ERROR

URL that will be called when a running flow encounters an error.

No value

WEBHOOK_FLOW_COMPONENT_ERROR_INCLUDE_QUOTA

Include quota errors among the errors sent to the registered webhook URL.

false

WEBHOOK_FLOW_COMPONENT_ERROR_INCLUDE_RETRY

Include retry attempts among the errors sent to the registered webhook URL. If false, the error will be sent, when all 30 attempts to process a message fail. If true, every failed attempt will be sent.

false

WEBHOOK_FLOW_STOPPED

URL that will be called when a flow is stopped due to an incompatible module upgrade.

No value

STRICT_COOKIES

false

GARBAGE_COLLECTOR_CONTINUITY_SCOPES_TTL

The maximum time in days before continuity scopes are garbage collected. A continuity scope is a state of a flow including data from the flow runtime needed to continue the flow from a certain component onwards. For example, the Plivo.SendSMSAndWaitForReply sends an SMS and waits for an event (webhook) from Plivo that contains an SMS with a reply. The continuity scope contains all the data needed to continue the flow at a later time (when the webhook from Plivo is received which can take hours to days.

100

Appmixer Deployment Models

Appmixer supports all cloud deployment models:

  • Public cloud (AWS, GCP, Azure, ...)

  • 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 (e.g. local MongoDB vs MongoDB Atlas, AWS DocumentDB, ...). 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 system 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 (support@appmixer.com).

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.

Appmixer Architecture

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 deployment of custom connectors. It can also be used to manage, export and import flows or automatically generate connectors from OpenAPI specification.

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

  • Component States

  • Dead-letter collection

  • Component Behaviour (code)

  • Modifiers (code)

  • Files (GridFS)

  • Data Stores

  • Telemetry

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.

Getting Started

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

Additional Configuration

Logging

LOG_LEVEL

By default set to info. It 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

Configure an HTTP proxy. All HTTP(S) requests from Appmixer will be redirected to the proxy URL.

Token Encryption

Most of the connectors in Appmixer require user authentication. That can be represented as OAuth access tokens, API keys, or username/password combinations.

To enable token encryption, set the ENCRYPTION_ENABLED environment variable to true. With that, also set the ENCRYPTION_SECRET environment variable to a secret string (see below for an example on how to generate it).

# Turning the encryption on
ENCRYPTION_ENABLED=true
# Setting the encryption secret (generate your own!)
ENCRYPTION_SECRET=0EC0136A5187A09E21D03819A0EFFD259070CE23213B260A1444412EFD910503
# Generate the encryption secret
openssl enc -aes-256-gcm -k secret -P -md sha1
# copy the 'key' and put it to ENCRYPTION_SECRET

If you lose the encryption secret, you will not be able to recover the encrypted tokens.

MinIO/S3

The plugin cannot migrate existing files from MongoDB to MinIO. It has to be turned on when Appmixer is installed before the files are created. If you already have files in MongoDB and want to start using MinIO, you have to migrate the data.

To enable the plugin, add minio to the SYSTEM_PLUGINS (comma-separated list of plugins) ENV variable (this variable cannot be set dynamically through the Backoffice - System Configuration):

# Turning on the plugin
SYSTEM_PLUGINS=minio

# Required variables
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=secretKey
MINIO_ENDPOINT=192.168.1.8   # s3.amazonaws.com to connect to AWS S3

# Optional variables
# Default value set to 80 for HTTP and 443 for HTTPS.
MINIO_PORT=9000
# Set this value to 'true' to enable secure (HTTPS) access
MINIO_USE_SSL=true
MINIO_REGION=eu-central-1
# All files will be stored by default in the 'appmixer-files' bucket
MINIO_BUCKET_NAME=appmixer-files  

If you want to use AWS S3, use the following permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucketMultipartUploads",
                "s3:ListBucketVersions",
                "s3:ListBucket",
                "s3:ListMultipartUploadParts",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::appmixer-files/*",
                "arn:aws:s3:::appmixer-files"
            ]
        }
    ]
}

Forgot Password Service

In order to create a link that can be sent to the user, the Appmixer engine needs to know the frontend URL, there are two variables that can be set for that:

Key
Detail
Default value
Required

APPMIXER_FE_URL

The Frontned URL

http://localhost:8080

RESET_PASSWORD_FE_URL_SUFFIX

URL path with the reset password form

reset-password

Without any changes, the link will be http://localhost:8080/reset-password?code={{code}}.

That link has to be then delivered to the user. There are two ways this can be done:

Webhook

You can register a system webhook that will be triggered every time a user requests to change their password. The webhook URL can be registered under the key WEBHOOK_USER_FORGOT_PASSWORD and the JSON object sent to that URL will be:

{
  "code": 'unique code generated for identifying forgot password request',
  "email": 'email address of the user',
  "userId": 'User Id',
  "created": 'date when a user requested for forgot password',
  "link": 'Link to access forgot password page on the frontend'
}

You can use Appmixer to create a simple flow, that would send emails with the reset password link.

SMTP

The other way is to configure the SMTP server, Appmixer will then send an email with the reset password link to the user's email address.

Key
Detail
Default value
Required

MAIL_SMTP_HOST

SMTP server address

MAIL_SMTP_PORT

SMTP server port

465

MAIL_SMTP_USER

username

MAIL_SMTP_PASS

password

MAIL_FROM_NAME

Sender name

Appmixer

MAIL_FROM_EMAIL

Sender email

info@appmixer.com

FORGOT_PASSWORD_MAIL_SUBJECT

Reset password email subject.

Reset your password

FORGOT_PASSWORD_MAIL_BODY

The reset password email body.

See below

The default email body:

<p>Hi,</p>
<p>You have requested to reset your password. Please click on the link below to reset your password.</p>
<p><a href="{{link}}">Reset Password</a></p>
<p>If you are unable to click on the link, please copy and paste the following link into your browser:</p>
<p>{{link}}</p>

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

Context Quotas

Components produce messages using the context.sendJson() function. An internal quota mechanism controls how many messages a user can produce.

This is the default configuration:

// QUOTA_CONTEXT_SEND
[
    {
        # 'user' can generate 100 messages per second (across all their flows)
        "limit": 100,
        "scope": "user",
        "windowInSeconds": 1,
        "name": "send:1s"
    },
    {
        # 'user' can generate 1000 messages per minute (across all their flows)
        "limit": 1000,
        "scope": "user",
        "windowInSeconds": 60,
        "name": "send:60s"
    },
    {
        # 'user' can generate 20000 messages per day (across all their flows)
        "limit": 20000,
        "scope": "user",
        "windowInSeconds": 86400,
        "name": "send:1d"
    }
]
// QUOTA_CONTEXT_SLOW_QUEUE
[
    {
        # 'user' can generate 100000 messages per minute (across all their flows)
        # everything that exceeds this limit will be thrown away
        "limit": 100000,
        "scope": "user",
        "windowInSeconds": 60,
        "name": "slowQueue:60s"
    },
    {
        # 'user' can generate 5000000 messages per day (across all their flows)
        # everything that exceeds this limit will be thrown away
        "limit": 5000000,
        "scope": "user",
        "windowInSeconds": 86400,
        "name": "slowQueue:1d"
    }
]

Each call to context.sendJson() increases the quota. If a limit is reached, the message is placed in a Slow Queue. Messages in the Slow Queue are processed at a much slower rate and only when sufficient resources are available. This ensures that one user’s flows do not consume excessive resources and block other users.

If you want to change the default values, you can use the Env variables QUOTA_CONTEXT_SEND and QUOTA_CONTEXT_SLOW_QUEUE, you can set them in the Backoffice.

QUOTA_CONTEXT_SEND default value, you can copy&paste this to the Backoffice and modify.

[{"limit":100,"scope":"user","windowInSeconds":1,"name":"send:1s"},{"limit":1000,"scope":"user","windowInSeconds":60,"name":"send:60s"},{"limit":20000,"scope":"user","windowInSeconds":86400,"name":"send:1d"}]

QUOTA_CONTEXT_SLOW_QUEUE default value, you can copy&paste this to the Backoffice and modify.

[{"limit":100000,"scope":"user","windowInSeconds":60,"name":"slowQueue:60s"},{"limit":5000000,"scope":"user","windowInSeconds":86400,"name":"slowQueue:1d"}]

Installation AWS ECS

Appmixer can be installed in an AWS account via our public Terraform module.

Prerequisities

  • Account in Appmixer Docker registry [registry.appmixer.com]. This account has been given to you during your sign-up process to the Appmixer Self-Managed package.

Installation Types

There are two types of installations:

  • Default installation

  • Custom installation

Default Installation

This type of installation assumes that you don't have any preferences regarding AWS Networking (VPC) and want leave all attributes in default. This kind of installation will create a new VPC and all required network parts (Internet Gateway, NAT Gateway, subnets, route tables, etc.). Then it will provision all the four required AWS managed services:

  • Amazon DocumentDB (MongoDB)

  • ElastiCache (Redis)

  • Opensearch (Elasticsearch)

  • Amazon MQ (RabbitMQ)

Next, it will provision AWS ECS (Elastic Container Service) with all Appmixer applications (Engine, Frontend, Backoffice, Quota and Logstash).

Custom Installation

By configuring external_* values, you have a choice where to install the Appmixer stack and choose external services like:

  • external_vpc: your specific VPC

  • external_redis: external Redis (running either in AWS or somewhere else)

  • external_rabbitmq: external RabbitMQ (running either in AWS or somewhere else)

  • external_elasticsearch: external Elasticsearch (running either in AWS or somewhere else)

  • external_documentdb: external MongoDB (running either in AWS or somewhere else)

Preparation

$ git clone https://github.com/clientIO/appmixer-module-aws.git
$ cd appmixer-module-aws/

This repository contains examples for both dev and production use cases. Also, both of these examples include autoscaling configuration. Let's start with dev example:

$ cd examples/development

This example defines a new VPC and it's CIDR block. You will need to configure:

  • zone_id (line #44) -> if set AWS Route53 will be used and you should also update aws_route53_zone (line #33) and set the domain name to be used.

  • ecs_autoscaling_config (line #52) -> Docker registry credentials, base64 encoded string.

  • input_init_user (line #54) -> here you can configure credentials for the initial user (Appmixer admin).

Installation

You should now be ready to provision the Appmixer stack.

$ terraform init
$ terraform plan

In the ouptput of the above commands, you can see the exact resources that will be provisioned. If the output looks right we can proceed:

$ terraform apply

Again, you'll have a chance to check what exactly will be provisioned. If it looks correct, you can approve. After the Terraform run, you'll get the so called Terraform output, where you can find a lot of useful information like Loadbalancer name, service URLs, managed services, etc.

OpenAPI Connector Generator

The Appmixer OpenAPI generator provides a tool to automatically generate Appmixer components from the OpenAPI v3 specification.

The tool generates an Appmixer connector from an OpenAPI specification. The spec can be extended with special x-connector-... extensions to either provide details that the OpenAPI spec does not define (but are necessary for the Appmixer connector) or to make the resulting connector more user friendly (automatic pagination, dynamic select with options instead of providing hardcoded values, ...).

Getting started

In the simplest form, you can run the generator by passing an OpenAPI spec as a parameter (both .json and .yaml file formats are accepted) together with a directory where the Appmixer connector will be generated:

Outputs:

The resulting connector is in the output directory:

This generates a complete Appmixer connector that can be packed and published to an Appmixer tenant:

Patching the OpenAPI specification

If you use a 3rd party OpenAPI specification and want to make changes to it or enrich it with extensions, it would not be practical to edit this JSON/YAML file directly. Editing the file directly would make it hard to keep track of changes or update the original spec file when needed. To avoid direct editing of the spec, the Appmixer OpenAPI generator accepts a separate JSON Patch file (.json-patch) that defines changes to the OpenAPI spec. This way, the original OpenAPI spec can be left intact and only changes to it (together with extensions) can be defined separately.

Consider the following example that removes the clientId parameter from all the parameters in the OpenAPI spec because the clientId is already part of the authentication screens. In other words, we don't need this parameter to be part of the configuration of each Appmixer component (Inspector UI) since we got the value once when the user authenticated to the connector (created a Connection). Since our example OpenAPI spec includes the clientId parameter in all the paramters section for each path item, we want to remove all the occurences. If we were to use the standard JSON Pointer format, we would have to list all the occurences of the clientId inside the parameters section for each path item such as:

As you can see, this is not exactly user friendly. Instead, the Appmixer OpenAPI generator introduces the jsonpath parameter for operations that accept the JSON Pointer-based path parameter and therefore []allows us to use a single remove operation instead:

To patch the OpenAPI spec with a JSON Patch file, pass the path to the patch file to the --patch argument:

Artifacts

Sometimes, it is useful to be able to see byproducts of the preprocessing of the OpenAPI spec and the patch file. To generate these byproducts, use the --artifacts argument:

This will create a special directory artifacts/ under the output directory with the following files:

  • checksum.json contains a SHA-256 digests for all the files generated by the generator. This is useful for a quick check whether manual changes have been made to any of the generated files or whether a re-generation of the connector produced a different result (possibly due to changes in the OpenAPI spec).

  • openapi.json is the original OpenAPI spec file.

  • openapi.normalized.json-patch is the JSON Patch file withe "normalized" jsonpath attributes. See Patching OpenAPI specification.

  • openapi.original.json-patch is the original JSON Patch file.

  • openapi.patched.json is the final OpenAPI spec, normalized, dereferenced (i.e. with resolved $ref references) and possibly patched.

Array vs Single Object Outputs

The Appmixer OpenAPI generator automatically checks for the operation success response JSON schema and if it detects type: array, it adds an "Output Options" select box to the resulting generated component configuration. This allows the end-user to select, whether they are interested in outputting all items at once (array output) or one item at a time (object output). The same logic applies when the x-connector-pagination extension is defined on the operation.

OpenAPI specification to Appmixer Connector mapping

The following table describes how the OpenAPI specification fields/features are mapped to Appmixer connectors.

File Uploads

Input fields that are of the JSON schema type string and format binary are considered binary file inputs. These input fields are mapped to the Appmixer filepicker inspector field type. For such inputs, the Appmixer OpenAPI generator automatically generates component code that loads the file from the Appmixer Files storage and streams it to the API endpoint. The multipart-form-data; boundary=FORM_BOUNDARY Content-Type HTTP header is automtically set on these requests.

Selecting Operations for Conversion

When using an external OpenAPI spec that you do not want to edit, use the JSON Patch with the following operation to select OpenAPI operations that you want to convert to Appmixer components:

In the example above, only the meetings and meetingCreate operations will be considered by the OpenAPI generator for conversion.

Supported Authentication Types

Only OAuth 2 and API key based (with keys in all query/header/cookies) authentication is supported. The generated authentication module can be controlled with the x-connector-connection, x-connector-connection-check and x-connector-connection-profile extensions.

OpenAPI Extensions

The Appmixer OpenAPI generator accepts multiple extensions that can be used to define constructs that the standard OpenAPI specification does not provide. Without these constructs, the final generated connectors would not be as user friendly.

x-connector-version

Description

Define the version of the connector. The connector is versioned using a major.minor.patch versioning scheme. The major.minor portion of the version string (for example 3.1) shall designate the connector feature set. .patch versions address errors in, or provide clarifications to, the connector, not the feature set.

Note that, unfortunately, we can't use info.version as the connector version since info.version can be an abitrary string while the connector REQUIRES the use of semantic versioning.

Location

  • Info Object

Value

A version string using the major.minor.patch versioning scheme. If not provided, the default value is 1.0.0.

Example

x-connector-icon

Description

Define an icon for the connector.

Location

  • Info Object

Value

A URL to an image (png, jpeg or SVG). A Data URI is also accepted.

Example

x-logo

Description

Alternatively to the x-connector-icon, the Appmixer OpenAPI generator also accepts the more common x-logo extension.

Location

  • Info Object

Value

Either a URL to an image (png, jpeg or SVG; with data URI also accepted) or an object with a url field that points to an image.

Example (URL directly)

Example (nested url field)

x-connector-service

Description

Define a name for the Appmixer connector service of the service/module/component hierarchy.

Location

  • Info Object

Value

A string. If not provided, the default value is the info.title in lower case with removed spaces.

Example

x-connector-module

Description

Define a name for the Appmixer connector module of the service/module/component hierarchy.

Location

  • Info Object

Value

A string. Deault value is core.

Example

x-connector-connection-check

Description

An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. In the most common sense, this request is called to check whether the api key or OAuth access token is valid. Typically, this is a call to a /me-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The check is valid if the response is a success (HTTP 2xx status code).

Location

  • Security Scheme Object

Parameters

  • method ... HTTP method (GET, POST, ...)

  • url ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI servers section).

  • headers ... HTTP headers object.

  • query ... HTTP query object (key-value pairs representing the query string parameters).

The values of the url, headers, and query parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (accessToken in case of OAuth).

Examples

A common use in API keys-based authentication:

A common use in Oauth-based authentication:

x-connector-connection-profile

Description

An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). Typically, this is a call to a /me-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The returned value is then used to get the display name for the Connection. Alternatively, it can also be a string that can contain parameters in curly braces. For security schemes with "type": "apiKey", the {apiKey} paramater will be replaced with the API key provided by the user.

Location

  • Security Scheme Object

Parameters

  • method ... HTTP method (GET, POST, ...)

  • url ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI servers section).

  • headers ... HTTP headers object.

  • query ... HTTP query object (key-value pairs representing the query string parameters).

  • transform ... JSONata expression to retrieve the value from the response payload that will be used as a display name for the Connection.

The values of the url, headers, and query parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (accessToken in case of OAuth).

Examples

A common use in API keys-based authentication:

A common use in API keys-based authentication with no /me endpoint:

A common use in Oauth-based authentication:

x-connector-connection

Description

For API-key based authentication, the x-connector-connection extension is an alternative to the OpenAPI-native definition. The x-connector-connection is more compact and contains all the necessary parameters for the entire Connection definition (including checks and profile requests). Also, the standard OpenAPI apiKey security scheme does not have a mechanism to describe how exactly is the api key passed in the request. It only allows to define where it is passed (header, query). However, some APIs require the value of the api key parameter to be more than just the key. For example, the OpenAI API uses the following in the HTTP headers: { Authorization: "Bearer {apiKey}" }. The Voys API even combines more api keys into one with: { Authorization: "token {username}:{apiKey}" }. If the x-connector-connection extension is used, it overrides other authentication definitions from the security section.

Location

  • Root.

Parameters

  • type ... The type of the authentication scheme. Only "apiKey" is currently accepted.

  • in ... The location of the authentication credentials (api key(s)) in all authentication requests. Can be "header", "query" or "cookie".

  • name ... Name of the header, query parameter or cookie.

  • value ... The template for the value of the authentication credential. It can use any property from the schema object enclosed in curly brackets. These will be replaced with real values at runtime.

  • schema ... JSON schema describing the authentication parameters. The schema is used to generate the authentication screen for the user where the parameter values will be requested from the user in a HTML form. Only a flat object type of JSON-schema structure is supported at this point and only the string type is supported for the properties.

  • check ... An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. The same parameters as in the x-connector-connection-check extension are supported.

  • profile ... An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). The same parameters as in the x-connector-connection-profile extension are supported. Alternatively, the profile can also be a template string that can contain parameters defined in the schema in curly brackets.

Examples

Two API keys (one username, one api key, custom value for Authorization header)

One API key (custom value for authorization header)

x-connector-label

Description

A custom label for the generated Appmixer component. By default, the label of the component is the operationId defined in the Operation Object.

Location

  • Operation Object

Value

A string.

Example

x-connector-description

Description

A custom description for the generated Appmixer component. By default, the description of the component is of the form <label>summary</label></br>description.`

Location

  • Operation Object

Value

A string.

Example

x-connector-field-index

Description

In OpenAPI specification, parameters and requestBody fields are typically not ordered by their importance from the perspective of a user filling a form that contains those fields. Therefore, in many cases, required fields or important fields may end up at the bottom of such a form. This is obviously not a great user experience. The x-connector-field-index allows you to give fields an order that you'd prefer when the fields are rendered in a form for the user to configure (Appmixer Inspector panel).

Location

  • Schema Object

Value

An integer.

Example (parameters)

Example (requestBody)

x-connector-field-options

Description

Since not all options can be defined using the JSON schema, the x-connector-field-options makes it possible to add additional options to the generated Appmixer inspector field. Also note that the field options are processed after all other heuristics used to automatically convert JSON schemas to Appmixer inspector field took place. Therefore, if the generated inspector field is not what you expect, you can use the x-connector-field-options to override the generated setting.

Location

  • Schema Object

Value

An object.

Example

x-connector-transform

Description

Location

  • Request Body Object

Parameters

  • language ... the language of the expression. Currently only JavaScript is supported.

  • expression ... the expression to transform the request body. The request body is available with the requestBody variable inside the expression. For example, the expression requestBody = {} empties the request body object completely, ignoring everything the user has set in the inspector.

Example

x-connector-source

Description

Turn a property into a select box with options loaded from another source.

Location

  • Schema Object (Any property in either requestBody or parameters).

Parameters

  • operationId ... the ID of the operation within the OpenAPI spec that will be called to retrieve the data for the options of the select box. The resulting data will be read from the out output port.

  • parameters ... [optional] A map of parameters that will be propagated to the source operation. An object with keys representing parameters of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the x-connector-source extension is defined.

  • requestBody ... [optional] A map of request body parameters that will be propagated to the source operation. An object with keys representing request body parameters (nested parameters are expressed using the / character) of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the x-connector-source extension is defined.

Example (typical use)

Consider an OpenAPI specification for Connectwise (examples/openapi/connectwise/openapi.json) for an endpoint that updates a service ticket (putServiceTicketsById). This endpoint takes board.id parameter in the request body to identify the service board the ticket should be placed in. If we left the OpenAPI spec without any modifications, the generated component would request the board.id from the user on an as-is basis, i.e. it would ask the user to provide the ID of the service board in a text input field. As you can imagine, this is not very helpful to the user since the user does not know that value. One way for the user to get around this would be to use another component such as getServiceBoards to search all the service boards and use the id output of the board found as an input of our board.id. However, in this case, it is much more user friendly to just show a list of service board names to the user right inside our putServiceTicketsById component and use the selected board's id automatically without the user having to deal with plain IDs.

Example (parameters propagation)

Sometimes, the source operation needs some parameters that are defined by the user in the design phase. To specify which parameters are propagated to the source operation, define the parameters and requestBody sections. In the example below, the userId is a parameter of the source operation and { form: { type: ... } } is part of the request body of the source operation. Assume that both parameters are required and without which the source component cannot return any results (and fails for missconfiguration). We can simply provide a mapping of the parameters and request body data that will be propagated to the source component like this:

x-connector-pagination

Description

Define a pagination method. This is especially useful for components that call endpoints that return an array of values (as opposed to a single object). In such cases, we may want to let the user configure how many items to return. At the same time, this limit on the number of items can be higher than the endpoint allows to return in one call (i.e. higher than the common limit paramater often used in the offset-limit type of pagination). If the x-connector-pagination is defined, the generated component will be smart enough to call the endpoint with varying parameters multiple times to retrieve the desired number of items.

Location

  • Operation Object

Parameters

  • type ... the type of the pagination used. One of "page", "cursor", "link-header" and "once".

  • parameters ... the parameters of the pagination specific to each type.

page type pagination parameters

  • offset ... The name of the query parameter of the 3rd party endpoint that represents the (zero-based) offset of the first item returned in the collection.

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint. This number will be added to the offset parameter whith each subsequent call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • count ... [optional] A JSONata expression that is evaluated on the response data that returns the total number of items in the collection. This is a hint to the component to stop calling the endpoint when all results have been received. The most common value just points to the field from the response data that contains the total count of items. Example: resultSet.count.

  • more ... [optional] A JSONata expression that is evaluated on the response data that returns a boolean value that tells teh component to stop calling the endpoint in order to get more results. In other words, if more evaluates to false, the component knows it has collected all the results. The most common value points to the boolean field from the response data that contains informatoin on where there are more items. Example: hasMore.

cursor type pagination parameters

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • next ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.

  • cursor ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

link-header type pagination parameters

This pagination expects the response to contain the Link HTTP header that contains at least one URL that, if requested, returns the next batch of items. This URL must have the rel="next" parameter set. For example:

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in the first call of the endpoint. (Subsequent calls are assumed to have this parameter automatically added by the server returning the next URL in the Link HTTP header.)

once type pagination parameters

This special type of pagination makes it easy to introduce paginated "features" (Limit inspector field, result output variable) on endpoints that do not support pagination but return an array of items. This is typically the case for endpoints that, even though return an array of items, do not return "much" of the items so no pagination is defined and necessary.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

x-connector-webhook

Description

Define a webhook trigger. Two common types of webhooks are supported:

  • A "subscription" type of webhook requires the 3rd party API to provide two endpoints for dealing with webhooks: subscribe and unsubscribe. The "subscribe" endpoint accepts a URL that will be called by the 3rd party to notify of new events occuring. The "unsubscribe" endpoint makes it possible to tell the 3rd party to remove the subscribed webhook from its registry and therefore stop receiving events on that URL.

  • A "static" type of webhook allows to manually register a global webhook URL with the 3rd party that the 3rd party will notify of new events occuring. In this case, both the subscribing and unsubscribing of the webhook is done manually (usually as part of the OAuth app configuration).

Location

  • Path Item Object of the webhooks section.

Parameters

type="subscription" webhook parameters

  • type ... The type of the webhook. For the subscription type, use "subscription".

  • subscribe ... An object defining the HTTP request to be sent to subscribe a webhook with the 3rd party to receive events to.

  • subscribe.url ... The URL of the request.

  • subscribe.method ... The HTTP method of the request.

  • subscribe.headers ... The HTTP headers of the request.

  • subscribe.body ... The data payload of the request.

  • unsubscribe ... An object defining the HTTP request to be sent to unsubscribe a webhook from the 3rd party in order to stop receiving events to the previously subscribed URL.

  • unsubscribe.url ... The URL of the request. A special template parameters can be used to

  • unsubscribe.method ... The HTTP method of the request.

  • unsubscribe.headers ... The HTTP headers of the request.

  • unsubscribe.body ... The data payload of the request.

  • outputCondition ... [optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.

  • outputTransform ... [optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.

Special placeholders can be used in the url, headers and body fields of the subscribe and unsubscribe parameters. These will be replaced at runtime and include:

  • {$baseUrl} ... This will be replaced by the base URL of the 3rd party service (see the servers section of the OpenAPI specification). Usable both in subscribe and unsubscribe objects.

  • {$webhookUrl} ... The Webhook URL of the trigger component. Usable both in subscribe and unsubscribe objects.

  • {$response.transform#JSONATA_EXPRESSION} ... The response transformed using the JSONata expression language. This gives you the most expressive power - if needed. Sometimes, the {$response.body#/foo/bar} that uses the JSON Pointer does not have enough expressive power to extract the ID of the webhook to unsubscribe. In these cases, use this parameter together with a JSONata expression to query the data you need to unsubscribe the webhook.

  • {$parameters.parameter_name} ... The value of the specified parameter from the parameters section of the OpenAPI operation. parameters make it possible to parametrize the webhook trigger (request information from the user). Use the {$parameters.parameter_name} placeholder to reference the parameter values in both the subscribe and unsubscribe objects.

  • {$connection.profile#/foo/bar} ... A portion of the connection profile object (see x-connector-connection-profile). This is especially useful to get values for data such as user ID, account ID or other, that are returned from the /me-type of endpoint. Use JSON Pointer after the # character to get to any nested values.

Example: using of parameters, output transformation

Example: a more complex unsubscribe URL with transforms

type="static" webhook parameters

  • type ... The type of the webhook. For the static type, use "static".

  • path ... The endpoint of the static webhook. The entire URL will be {APPMIXER_API_URL}/plugins/appmixer/{SERVICE}{PATH}. For example: https://api.appmixer.com/plugins/appmixer/zoom/events.

  • pattern ... A JSONata expression that is evaluated on the incoming webhook request (with payload, headers, method and query fields). If the topic parameter matches the evaluated pattern, the component triggers and outputs the incoming request payload (considering outputCondition is met).

  • topic ... The value of the evaluated pattern for which the component is interested in receiving events.

  • outputCondition ... [optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.

  • outputTransform ... [optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.

  • crc ... The definition of the Challenge-Response check. Some APIs use the challenge-response check to confirm the ownership and the security of the webhook notification endpoint URL. In this scenario, the API sends a POST request to the webhook URL with a challenge (a token) and the webhook endpoint must return the challenge hashed with a secret token known to both sides.

  • crc.condition ... A JSONata expression evaluated on the incoming request (with payload, headers, method and query fields) that must evaluate to true for the incoming request to be considered as the challenge request (i.e. to ignore all non-CRC requests).

  • crc.alg ... The hash function used to calculate HMAC. Typically "sha256".

  • crc.key ... The name of the configuration value of the connector that contains a secret token known to both sides, that will be passed to the HMAC to calculate the final token for the response. This points to the configuration key that can be set in the Appmixer Backoffice -> Configuration for the service appmixer:SERVICE.

  • crc.challenge ... A JSONata expression evaluated on the request returning the challenge token (that will be used together with the key to calculate the HMAC).

  • crc.digest ... The encoding of the HMAC. Supported values are "hex", "base64" and "utf8".

  • crc.response ... A JSONata expression evaluated on an object of the form { responseToken, challenge } containing both the original challenge and the final calculated HMAC (responseToken). The expression should return the payload to be send as a response to the CRC request.

cursor type pagination parameters

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • next ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.

  • cursor ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

x-connector-rel-link-base-url

Description

Define a URL base path for any link with a relative URL in the connector. Applies only to relative URL links in markdown formatted descriptions. When provided, it will convert [link text](/documentation#endpoints-price-range) to <a href="https://www.example.com/documentation#endpoints-price-range">link text</a>.

Location

  • Info Object

Value

A string.

Example

Examples

BoredAPI

  • No JSON patch, no OpenAPI extensions are used.

  • OpenAPI Spec

Jotform

  • A custom written single OpenAPI spec with extensions right inside the OpenAPI document.

  • Pagination, connection check/profile, source, custom API-key based authentication, webhooks.

  • OpenAPI Spec

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-webhook

  • x-connector-source

  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

Zoom

  • OpenAPI with a separate JSON Patch file with changes and fixes, separate OpenAPI spec for webhook definitions externally referenced from the OpenAPI spec.

  • File upload (multipart/form-data), OAuth 2, pagination (cursor), inspector field index, request body JavaScript transformation.

  • OpenAPI Spec

  • OpenAPI Webhooks Spec

  • JSON Patch

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-webhook

  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

  • x-connector-field-index

  • x-connector-transform

OpenAI

  • OpenAPI in YAML format with a separate JSON Patch file with changes and fixes.

  • File upload (multipart/form-data), custom API key-based auth, source, pagination (once), inspector field index.

  • OpenAPI Spec

  • JSON Patch

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-pagination

  • x-connector-connection

  • x-connector-field-index

  • x-connector-source

Supported OpenAPI versions

Also note that the tool accepts both YAML and JSON files as inputs for the OpenAPI specification.

Notes & Limitations

  • Fields in parameters and requestBody are mixed together to form a flat list of configuration fields of the connector. Conflicting names are not accepted at this point.

  • Only the first subschema of the oneOf JSON schema composition keyword is used.

  • allOf subschemas are merged into one using the (json-schema-merge-allof)[https://www.npmjs.com/package/json-schema-merge-allof] library.

  • Only the first subschema of the anyOf JSON schema composition keyword is use.d

  • Only the first subschema of the oneOf JSON schema composition keyword is used.

  • The output JavaScript code is formatted using (ESLint)[https://eslint.org/].

Access Control

This tutorial will show you how to set ACL rules for different groups of users and for different types of resources.

Components

If you open Appmixer Backoffice, you can see an ACL section in the left menu under the "System" menu. You can then choose between settings for routes and components.

The default ACL setting you will see loos like this:

If you want to set different ACL rules for different connectors, start with deleting the general rule that allows all users to access all components. If you did not do this step, the rule user - * - * would overrule any other rule you would create for the user scope.

After deleting the above mentioned generic rule, you'll notice that the Appmixer Flow Designer does not contain any connectors (for users with only the regular user scope).

We can make all the connectors from the appmixer vendor accessible again with the following rule:

When you refresh the Appmixer Flow Designer now, you will see all the Appmixer connectors back.

Let's break down those four properties you can set for each ACL rule.

  • role: admin | user - those are the default roles/scopes in the system. You can also use an email address or a domain. It means you can define ACL(s) for a single users (email address) or for all users from certain email domain. Let's say your company is called acme and your employees all have an email address their-name@acme.com. Then the domain for ACL rule would be acme.com.

  • resource: component type prefix (appmixer.google.gmail* for example). This allows to create rules for components belonging to certain vendor, service or module. In the example above we created a rule for all appmixer components. The resource string was appmixer* which will cover all appmixer components.

  • action: action the rule is for. In case of components the only action is use. You can keep it to *. There are more actions when it comes to rules for API routes.

  • attributes: private or non-private. If set to non-private the rule will apply to component that do not have private: true set in component.json. If set to private it will allow users to see private components as well.

Let's try more examples. Show only utils, google and slack components. We're going to add the following three rules:

When you refresh Appmixer Designer you can see this:

Because we used attribute non-private for all our rules we can see only components where private: true is not set in their manifest. Most appmixer component modules user private auxiliary components for listing items into menus (slack channels for example). Therefore we don't see those private components in this menu:

Let's change the ACL rule for slack module to see those private components as well.

User can see private components now.

When you start writing your own components you should use your own vendor (not appmixer). We are adding Hello World component for example under vendor acme. Its component type is acme.custom.component.HelloWorld. Then we have to publish the component into Appmixer and create new ACL rule that will make it accessible to users.

At this point we can find the new Hello World component among other components.

Another example will show how you can user resource property to display only gmail module.

Routes

If you want to limit users from certain role, first you need to delete the general rule. We will show it on user role.

Those rules are store in mongo DB in the following form:

Attributes

where flow.customFields.category = "test-example". This can be done through API. But we want only admin users to be able to do so. The following set of ACL rules will allow admins to create custom fields, but it will allow users to only read those fields.

So if you try to send an API request to create new flow with the following body (as a user with user scope).

The custom field category won't be created. Property customFields will be an empty object. Although if you create the same flow with admin user that customFields property with category will be created and your user with user role will be able to read it (API will return it).

With the following set of rules, users with user role won't be able to create and read property customFields.

The same way you restrict access to each property on flows collection.

The next JSON shows different syntax with similar functionality. User with role user can read/create flows with properties flow, name, flowId, stage, sharedWith, but they won't be able to create customFields property.

You can also use nested objects as attributes. Let's say the customFields object contains two properties:

And we want the users to be able to see only visible property, the rules would look like this:

Users are not able to create customFields, they are allowed to read customFields.visible property, but not the customFields.not-visible.

Flows Metadata & Filtering

Appmixer flows can contain custom metadata specific to your application. This is especially useful for filtering flows and displaying different flows for different use cases.

Setting Custom Metadata

Each flow can have assigned a custom metadata JSON object the customFields field. In the examples below, we show how to define custom metadata on a flow using each of the available Appmixer interfaces (REST API, Appmixer SDK).

Setting Custom Metadata using the REST API

Use the customFields object when creating new flows:

... or when updating existing flows:

Setting Custom Metadata using the Appmixer SDK

Create new flows with customFields:

... or update existing flows with:

Search Flows Based on Custom Metadata

Now when you have flows with your own custom metadata defined, you can use filters to search for flows based on values of your metadata.

Filter Flows using the REST API

Moreover, you can use the following operators in your filter queries: !, ^, $, ~, >, <, $in. For example, to filter flows that do not fall into our "healthcare" category, we can use:

Filter Flows using the Appmixer SDK

Appmixer UI SDK FlowManager with Custom Metadata

The main power of custom metadata is that you can display multiple FlowManager UI instances on your pages for different categories of flows. A typical example is to have a custom field "template" and split all flows in Appmixer to "template" flows and actual flow instances. You can achieve that by having two lists of flows in your UI displayed with the FlowManager UI widget, one for flow templates and one for actual flow runs. The flow templates would only display flows with "template:true" filter and the other list will display flows with "template:!true" filter. Based on where the user actually created the new flow, you would assign either template:true or template:false custom field to the flow object.

Filter Flows with FlowManager

Use the customFilter option when creating an instance of your flow manager. The FlowManager widget will then use this filter whenever requesting list of flows to display from the API:

You can have another instance of FlowManager with non-template flows:

Appmixer Virtual Users

This tutorial shows you how to create Appmixer virtual users that shadow your own user accounts in your own user management system. Moreover, we also show how to group users into teams and how to limit the set of components each team has access to.

Introduction

When you embed Appmixer functionality/UIs into your product, you need to create Appmixer virtual users. These user accounts are only internal, your end-users don't need to sign-up anywhere and won't even notice. The purpose of virtual user accounts in Appmixer is to associate Appmixer entities (flows, data stores, files, connected 3rd party accounts, logs) with your own user accounts.

Usually, you create Appmixer virtual user accounts automatically and on the fly, at the time your end-user requests to see a page where an Appmixer functionality is embedded. Therefore, not all your users need a shadow Appmixer virtual user account but only those that will interact with the embedded Appmixer widgets (Integrations, Workflow Designer, ...).

Creating Virtual User accounts using the SDK

The first thing you need to do is to provide an HTML container element inside which you want to render the FlowManager:

Note that you also need to include the Appmixer SDK on your page:

And then initialize the SDK by passing your Appmixer base URL (the Appmixer Engine location):

At this point, you're ready to authenticate your user to Appmixer in the background or create a new virtual Appmixer user if your user account has not yet been associated with one. Let's assume that your client-side code has an information about your user profile stored in profile object that might look something like this:

If this is the first time your user is about to interact with Appmixer in your app, then your user profile does not have any Appmixer virtual user credentials stored. Therefore, you should generate a secret that we'll use as the user's password to Appmixer and create a new Appmixer virtual user. If your user profile already contains Appmixer virtual user credentials, you can simply authenticate the user to Appmixer and get a token back that you can use to initialize the SDK:

This is it. Now your web app creates Appmixer virtual users as needed and your users can start interacting with Appmixer widgets.

One disadvantage of the client-side approach is that (for security reasons), you cannot change the user scope (a string that can be used to group users into teams in Appmixer and therefore set common rules e.g. for accessing different set of components for different teams). If you don't require this functionality, you're fine with the client-side approach. If you do, you need to create your virtual users from your backend application using the Appmixer REST API. The next section shows you how you can do that.

Creating Virtual User Account using the REST API

Authenticate with admin user to get admin access

The response contains the user object together with their token:

Create a virtual Appmixer user

To create a new virtual user, initiate a POST request to the /user endpoint:

The response contains the newly created user token:

Use the token to request the user profile:

The response contains the user object together with the user ID:

You can also manually validate that a user has been created in the Appmixer Backoffice interface:

Change the user scope

As you can see, all newly created users have the default user scope. This scope can be replaced by any other string (e.g. ID or slug of a team that you want to group users under). Having a custom scope grouping multiple users allows you to define rules that e.g. limit the particular scope to only a selected set of modules/components (see below). To change the user scope, call the following endpoint passing a list of scopes that you want the user to have. Note that you need to use the admin user token to change a user's scope (for security reasons so that regular users can't change their own scope). You also need to use the user ID that we retrieved from the user profile endpoint in the previous step:

Again, you can validate in the Appmixer Backoffice that the user's scope has indeed changed:

Or you can search users by their username or part of it using the pattern query parameter:

Configure ACL rules for a scope

By default, Appmixer ships with a set of ACL rules that apply for the admin and user scopes. If you don't need to create new scopes to group users, you're usually fine with the default setting. However, since we create a brand new scope acme1, users with this scope won't be able to read and create flows and won't have access to any modules/components.

ACL rules in Appmixer are divided into two categories: routes and components. You can see the two categories when you visit the ACL page of your Appmixer Backoffice interface:

The routes category defines ACL rules that apply to the routes of the Appmixer Engine API (the endpoints). By default, all users with the admin and user scope have access to all the endpoints:

In order for users with our new scope acme1 to be able to access all the endpoints as regular users can, we need to add a new rule. This can be done either via the Backoffice interface ("Add" button):

Or programatically via the Appmixer REST API. A common practice is to first get the existing set of ACL rules for the routes category and if the newly created scope is not in the list, just add it (note again that you need admin user token to manipulate ACL rules):

The response looks something like this:

Since our acme1 scope is not in this list (look for the role attribute), we can add a new rule that allows users with this scope to access their flows:

Where our routes-acl.json file looks like this (the Appmixer API currently allows replacing all the rules at once, therefore, for convenience, we stored our rules in a file to make our curl command a little bit more readable):

Note that the user can technically access the Appmixer Studio but since our goal is to embed Appmixer to your own product, your virtual users won't be visiting the Appmixer Studio at all. Instead, they'll be interacting with the embedded Appmixer widgets only. The purpose of the above test is to quickly validate (especially the first time you're creating virtual users programatically) what the user has access to.

Also notice that if you try to create a new flow on behalf of the user by clicking on the "Create flow" button, the list of available modules will be empty:

This is because we haven't yet granted users with the acme1 scope access to any modules/components. To add ACL rules for components, we can follow a similar approach as we did for the ACL routes category but this time we'll do that for the components category. Therefore, we can again do it manually in the Appmixer Backoffice by adding a new rule to the components category. Let's say we'd like to give the user access only to the modules in the appmixer.utils and appmixer.twilio namespaces. We can add two new rules in the Backoffice:

Note the wildcard (* ) character after the module namespace. If we didn't do that, although the user will have access to the module itself, they won't be able to see any components within that module (e.g. appmixer.twilio.sms.SendSMS). The wildcard character makes sure all the components within the appmixer.twilio namespace are matched. If you don't use the wildcard, you can give users access to only a particular set of components within modules instead of the entire module.

Once you save those two rules and create a flow on behalf of the user in the Appmixer studio, you should see the utility and twilio modules displayed:

To add the components rules programatically via the Appmixer REST API, again, first request the current set of modules:

where the response looks something like this:

And add a new rule to the list so that users in our acme1 scope can access utilities and twilio modules:

Replace the components ACL rules with the above list:

Sharing Integration Templates with users with a custom scope

Once you Publish an integration template and visit the Sharing dialog:

You'll notice your integration template has been shared with users under the user scope by default:

To share the Integration template with your custom scope, just change "user" to your own scope instead and click on the Update button:

Now users in the acme1 custom scope will be able to use your integration template:

Securing the Create user API endpoint

Summary

In this tutorial, we learned how to create virtual users both using the simple method via the client-side Appmixer JavaScript SDK and also using the more advanced and flexible method via the Appmixer REST API. We learned how to group users, set different access rights for these groups of users, especially access to different set of components and integration templates. You should now be ready to embed Appmixer into your own product and create shadow virtual users for your own user accounts.

Using Backoffice to set vendor property.
Custom component published

Appmixer features an internal Authentication Hub, functioning as an authentication proxy, which simplifies the setup process by eliminating the need to register your own OAuth credentials with third-party services. This means all OAuth-based connectors offered by Appmixer are ready to use right out of the box. However, should you prefer to use your own OAuth credentials for enhanced customization or compliance reasons, you have the flexibility to do so. This can be done for all or selected connectors by specifying your OAuth credentials (clientId and clientSecret) as outlined in the section of this documentation.

To request the AUTH_HUB_TOKEN, contact our customers support by sending email to .

v18.15.0 (only for )

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

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

The information about automatically 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 .

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

Appmixer offers a variety of system configuration options for advanced use cases, allowing you to finely tune the behavior of its underlying workflow and integration engine. To access and set these configuration options, navigate through the interface to the "System -> System Configuration" page.

Whether to display from the authentication modules.

(needed usually for domain verification) can be served from different paths. Path prefixes have to be separated by :

If set to true, the engine will reject any incoming HTTP requests that have cookies that don't comply with the .

Appmixer Deployment Models
Appmixer High-Level Architecture
Appmixer JavaScript SDK
Appmixer Command Line Interface
Flow Design Phase Architecture
Flow Running Phase Architecture

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

Sign-in page
Sign-up page

This page contains additional system configuration options that are provided for self-managed Appmixer installations. See the for the rest of the configuration options. The below options can be set via the environment variables on the instances of the Appmixer engine.

Appmixer contains a MinIO plugin. If this plugin is turned on, Appmixer will store all user files (files created either through Appmixer flows or through the API) in the MinIO/S3 server.

Appmixer provides an to reset forgotten passwords. This works together with the Appmixer Studio interface (not the Appmixer SDK).

First, clone the git project with the Terraform code:

The JSON Patch file follows the () with some small modifications to make it easier to identify values that need to be changed in the OpenAPI spec:

The standard JSON Patch file uses to identify values in a JSON file. However, the JSON Pointer format does not provide the expressive power that one would expect from a format that is to be manually written, especially when identifying multiple places within a JSON file using a single expression.

The jsonpath parameter accepts a . The Appmixer OpenAPI generator pre-processes the JSN Patch file to expand operations that contain the jsonpath parameter by finding all the values in the JSON OpenAPI spec document that the JSON Path expression points to and multiplying the operation for each value found while replacing the jsonpath with path containing the JSON Pointer expression that identifies the value.

OpenAPI field/feature
Appmixer Connector Field/Feature

transform ... A expression that must transform the resulting data into an array of objects with value and label fields. The label field is the visible name of the option to the user. The value field is the value that what will be used as the value for the field for which the x-connector-source extension is defined. [Note that JMESPath is used instead of JSONata - prevalent in other extensions - since the Appmixer cannot be currently asynchronous.]

{$request.body} ... The entire request body. See for more details. Usable mainly in the output parameter.

{$response.body#/foo/bar} ... A portion of the response body specified by a JSON Pointer. See for more details. Usable mainly in the unsubscribe object to point to fields from the payload returned in the response to the subscribe endpoint call. Typically, the response contains the ID of the webhook which we want to use in the request to unsubscribe from the webhook later on.

{$response.header.header_name} ... The value of the specified response header. See for more details.

A minimalistic OpenAPI spec for the single-endpoint API.

All versions starting from version 2 (Swagger) is supported, i.e. 2.0, 3.0.x and 3.1. Note that the 2.0 version is automatically detected and a conversion to version 3.0.x is automatically performed using the npm package.

Using Appmixer ACL feature you can control user or user groups access to connectors. This can be configured both from the or through the .

When you open the components section, you can see the default ACL rules. Users with the scope admin, user and tester can use all non-private components. Components can be hidden using the property defined in the component manifest file. You can decide to make these private components visible to certain user groups using ACL.

You can define ACLs to restrict access to API. The default setting is similar to the one for Components. All roles can access all actions on flows resource.

With this setting, any request to any endpoint will result in 403 response code. The following example will show you how to limit access to API for user scope to read only operations.

The next example is not supported in the Backoffice UI, but can be implemented directly using API. Flows may contain . You can use ACL to restrict access to those metadata. We can create a flow with custom metadata like this:

The easiest way to create virtual user accounts is via the client-side Appmixer JavaScript SDK. The SDK provides methods to authenticate virtual users and to create new virtual users. You can find a full example in the . Let's say you have a page in your web application where you want to display the Appmixer FlowManager UI widget so that your end-users can see the flows they created and create new ones:

To create new or update existing virtual users using the REST API, use the . Since some of the User endpoints cannot be called with a regular user Authentication Bearer token, the first step is to authenticate using any of your admin users and use the admin user token instead:

Note that since we created a brand new scope and it's the only one that our new user has, we also need to configure ACL rules for this user so that they are able to create and read flows and access some modules in order to use them in their flows. See the next section on how to set this up. But before we jump into ACL rules, let's just quickly review how you can search for users using the REST API. To find all users with a certain scope, you can call the endpoint and filter by scope:

At this point, you can even try to sign-in on behalf of this user to the Appmixer Studio interface () to validate that the user is indeed created and that they can use Appmixer:

Now that we have created a new virtual user, defined and configured a custom scope, we can take advantage of this new scope not only to limit the set of available components for users in this scope but we can also share with only users under a certain scope. By default, integration templates are shared with the user scope. This happens when you publish a new integration template in the Wizard builder:

By default, anyone can create a new user. This enables the sign-up feature. The API endpoint can be secured. You may want to restrict access to this endpoint and control how the virtual users are created. To do so, set the API_USER_CREATE_SCOPE to admin.

/:/.well-known
Connector Configuration
support@appmixer.com
Docker
Docker Compose
http://localhost:8080
ngrok
http://localhost:8081
Appmixer Backoffice
http://localhost:8080
System Configuration
/files
AWS account
AWS CLI
Terraform
Cofigured AWS CLI
Appmixer Module AWS
NodeJS
Appmixer CLI
OAuth redirect URL
API
$ appmixer init openapi ./examples/openapi/boredapi/openapi.json ./examples/openapi/boredapi/
OpenAPI specification is valid.
No `security` scheme defined in the root level. Authentication module will not be generated.
API name: Bored API, Version: 1.0.0
Statistics: 1 Operations, 1 Paths, 0 Webhooks, Generated Components 1
Auth scheme used: null
Used extensions:
$ tree ./examples/openapi/boredapi

boredapi/
├── bundle.json
├── core
│   └── GetActivity
│       ├── GetActivity.js
│       ├── component.json
│       └── package.json
├── openapi.json
├── package.json
└── service.json
$ appmixer pack ./examples/openapi/boredapi
$ appmixer publish appmixer.boredapi.zip
{ "op": "remove", "path": "/paths/~1service~1tickets/parameters/2" },
{ "op": "remove", "path": "/paths/~1company~1contacts/parameters/1" },
{ "op": "remove", "path": "/paths/~1user~1emails/parameters/2" },
...
{ "op": "remove", "jsonpath": "$..parameters[?(@.name==\"clientId\" && @.in == \"header\")]" }
$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch ./zoom
$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch --artifacts ./zoom
$ tree zoom/artifacts/

zoom/artifacts/
├── checksum.json
├── openapi.json
├── openapi.normalized.json-patch
├── openapi.original.json-patch
└── openapi.patched.json

0 directories, 5 files

info.title

Service name (lower cased with spaces removed). Can be customized using x-connector-service

info.description

Service description

info.title

Bundle changelog version description

servers[0].url

The first server is used as the base URL for all HTTP calls in the connector. Other servers are ignored.

servers[0].variables

For apiKey based authentication schemes, server variables are requested by the end-user in the authentication flow (i.e. considered in the auth.js form and later used to construct the base URL). Only the first server variables are used. Other servers are ignored.

paths

Each Operation Object is converted to an Appmixer component. The path of the Path Object is used to construct the URL to be called (using the HTTP method from the Path Item Object) by the component as part of its behaviour.

paths...operationId

The operation ID is used as the Appmixer component name after normalization. The normalization consists of camel casing the operation ID if it contains the . (dot) character since the dot character can't be used in the Appmixer component name. Otherwise, the operation ID is used as is. if the operation ID is missing in the OpenAPI spec, the Appmixer component name is constructed from the HTTP method and path (e.g. GET /user) becomes GetUser.

paths...responses

The first JSON-like success response object is heuristically found and used to construct the output of the component. Only one out output port is always created and the response JSON schema is used as to construct the options object of the output port (including JSON schemas for nested objects and arrays). The heuristics to find the first JSON-like response object finds the first response with 2xx HTTP Status Code and Media Type containing the "json" string.

paths...requestBody

The request body object is used to construct the input of the component (together with parameters). Only the following media types are supported: applicatoin/json, application/x-www-form-urlencoded and multipart/form-data. The input of the component is flattened and all nested properties of the original request body are delimited using the `'

paths...parameters

The parameters are used to construct the input of the component (together with requestBody).

{ "op": "remove", "jsonpath": "$.paths.*[?(@.operationId != 'meetings' && @.operationId != 'meetingCreate')]" }
"info": {
  ...
  "x-connector-version": "2.1.1"
  ...
}
"info": {
  ...
  "x-connector-icon": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}
"info": {
  ...
  "x-logo": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}
"info": {
  ...
  "x-logo": {
      "url": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
      ...
  }
  ...
}
"info": {
  ...
  "x-connector-service": "jotform"
  ...
}
"info": {
  ...
  "x-connector-module": "forms"
  ...
}
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-check": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}"
            },
            ...
        }
    },
"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-check": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          }
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}",
                "transform": "content.name"
            },
            ...
        }
    },
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": "{apiKey}"
            ...
        }
    },
"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-profile": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          },
          "transform": "email"
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}
"x-connector-connection": {
    "type": "apiKey",
    "in": "header",
    "name": "Authorization",
    "value": "token {username}:{apiKey}",
    "schema": {
        "type": "object",
        "properties": {
            "username": {
                "type": "string",
                "title": "Username"
            },
            "apiKey": {
                "type": "string",
                "title": "API Key"
            }
        }
    },
    "check": {
        "method": "GET",
        "url": "/callnotification/callnotification/",
        "headers": {
            "Authorization": "token {username}:{apiKey}"
        },
        "expect": {
            "status": 405
        }
    },
    "profile": "{username}"
},
"x-connector-connection": {
  "type": "apiKey",
  "in": "header",
  "name": "Authorization",
  "value": "Bearer {apiKey}",
  "schema": {
      "type": "object",
      "properties": {
          "apiKey": {
              "type": "string",
              "title": "API Key",
              "description": "Log into your OpenAI account and find your API key."
          }
      }
  },
  "check": {
      "method": "GET",
      "url": "/models",
      "headers": {
          "Authorization": "Bearer {apiKey}"
      }
  },
  "profile": "{apiKey}"
}
...
operationId: 'postContacts',
x-connector-label: 'CreateContact'
...
...
x-connector-description: 'Create a new contact or update an existing one.'
...
...
"parameters": {
    "in": "query",
    "name": "accountId",
    "required": true,
    "schema": {
        "type": "string",
        "x-connector-field-index": 1
    }
...
...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "firstName": {
                        "type": "string",
                        "x-connector-field-index": -1
                    }
                }
            }
        }
    }
}
...
...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time",
                        "x-connector-field-options": {
                            "format": "MM-ddTHH:mm:ss"
                        }
                    }
                }
            }
        }
    }
}
...
...
"requestBody": {
    "content": {
        "application/json": {
            "x-connector-transform": {
                "language": "javascript",
                "expression": "requestBody.start_time = requestBody.start_time.replace('Z', ''); requestBody.timezone = requestBody.timezone || 'UTC';",
            },
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            }
        }
    }
}
...
...
"requestBody": {
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "board": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "x-connector-source": {
                  "operationId": "getServiceBoards",
                  "transform": "result[].{value: id, label: name}"
                }
              }
            }
          }
        }
      }
    }
  }
}
...
                        "x-connector-source": {
                            "operationId": "GetUserForms",
                            "transform": "result[].{value: id, label: title }",
                            "parameters": {
                                "userId": "/parameters/userId"
                            },
                            "requestBody": {
                                "form/type": "/requestBody/content/application~1json/schema/properties/formType"
                            }
                        }
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "page",
            "parameters": {
                "offset": "offset",
                "limit": "limit",
                "page": 20,
                "results": "content",
                "count": "resultSet.count"
            }
        }
        ...
    }
}
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}
Link: <https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=4>; rel="next", <https://api.github.com/repositories/1300192/issues?page=515>; rel="last", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "link-header",
            "parameters": {
                "limit": "limit"
            }
        }
        ...
    }
}
"/user/reports": {
    "get": {
        "summary": "Get User Reports",
        "operationId": "GetUserReports",
        "x-connector-pagination": {
            "type": "once",
            "parameters": {
                "results": "content"
            }
        }
        ...
    }
}
"webhooks": {
    "NewContact": {
        "post": {
            "x-connector-webhook": {
                 "type": "subscription",
                 "subscribe": {
                    "url": "{$baseUrl}/system/callbacks",
                    "method": "POST",
                    "body": {
                        "url": "{$webhookUrl}",
                        "objectId": "{$parameters.objectId}",
                        "type": "Contact",
                        "level": "{$parameters.level}"
                    }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/system/callbacks/{$response.body#/id}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$boolean(Action='added')",
                "outputTransform": "$eval(Entity)"
            },
            "operationId": "NewContact",
            "summary": "Triggers when a new company contact was created.",
            "parameters": [{
                "name": "level",
                "in": "body",
                "description": "When set to owner, all ConnectWise PSA contacts are returned. When set to type, all contacts of the specified type are returned. When set to territory, all contacts of the specified territory are returned. When set to company, all contacts of the specified company are returned. When set to contact, the specified contact is returned.",
                "required": false,
                "schema": {
                    "type": "string", "enum": ["Owner", "Type", "Territory", "Company", "Contact"],
                    "default": "Owner"
                }
            }, {
                "name": "objectId",
                "in": "body",
                "description": "The ObjectId should be the Id of whatever record you are subscribing to. This should be set to 1 when using a level of Owner.",
                "required": false,
                "schema": { "type": "integer", "default": 1 }
            }],
            "requestBody": {
                "description": "New Contact Created",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Contact"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "application/json": {}
                }
            }
        }
    }
}
"webhooks": {
    "NewFormSubmission": {
        "post": {
            "operationId": "NewFormSubmission",
            "summary": "Triggers when a form is submitted.",
            "x-connector-webhook": {
                "type": "subscription",
                "subscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks",
                    "method": "POST",
                    "headers": { "Content-Type": "application/x-www-form-urlencoded" },
                    "body": { "webhookURL": "{$webhookUrl}" }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks/{$response.transform#$keys(data.content) ~> $filter(function ($key) { $lookup(data.content, $key) = \"{$webhookUrl}\"})}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$exists(submissionID)"
            },
            "parameters": [{
                "name": "formId",
                "in": "body",
                "description": "Form ID",
                "required": true,
                "schema": {
                    "type": "string",
                    "x-connector-source": {
                        "operationId": "GetUserForms",
                        "transform": "result[].{value: id, label: title }"
                    }
                }
            }],
            "requestBody": {
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/WebhookSubmission"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "content": {
                        "application/json": {
                        }
                    }
                }
            }
        }
    }
}
{
  "webhooks": {
    "meeting.deleted": {
      "post": {
        "operationId": "meetingDeleted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload|object": {
                    "type": "object",
                    "description": "Information about the meeting.",
                    "properties": {
                      "uuid": {
                        "type": "string",
                        "description": "The meeting's universally unique identifier (UUID)."
                      },
                      "topic": {
                        "type": "string",
                        "description": "The meeting's topic."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-connector-webhook": {
          "type": "static",
          "path": "/events",
          "pattern": "payload.event & ':' & payload.payload.account_id",
          "topic": "meeting.deleted:{$connection.profile.account_id}",
          "crc": {
            "condition": "payload.event = \"endpoint.url_validation\"",
            "alg": "sha256",
            "key": "webhookSecretToken",
            "challenge": "payload.payload.plainToken",
            "response": "{ \"encryptedToken\": responseToken, \"plainToken\": challenge }",
            "digest": "hex"
          }
        }
      }
    }
  }
}
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}
"info": {
  ...
  "x-connector-rel-link-base-url": "https://www.example.com"
  ...
}
appmixer init openapi ./boredapi/openapi.json ./connector/
appmixer init openapi ./zoom/ZoomMeetingAPISpec.json --patch ./zoom/openapi.json-patch --artifacts ./connector
appmixer init openapi --artifacts --patch ./openai/openapi.json-patch ./openai/openapi.yaml ./connector
{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "tester",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*"
            ]
        }
    ]
}
{
    "_id" : ObjectId("5f3696f502ea801d405bb112"),
    "userId" : ObjectId("5f3696f102ea801d405bb10e"),
    "flowId" : "c57f1d30-52fc-4a48-97d9-5d1c296064da",
    "stage" : "stopped",
    "name" : "Flow with metadata",
    "btime" : ISODate("2020-08-14T13:51:49.900Z"),
    "mtime" : ISODate("2020-09-01T14:57:43.536Z"),
    "flow" : {
    },
    "mode" : "module",
    "sharedWith" : [],
    "customFields" : {
        "category" : "test-example"
    }
}
{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*   // are able to read all attributes, including custom fields
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", "!customFields"   // but they're not able to create them
            ]
        }
    ]
}
{
    "flow":{},
    "name":"New flow",
    "customFields": {
        "category": "test-category"
    }
}
{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }
    ]
}
{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read", 
                "create"
            ],
            "attributes" : [ 
                "flow", 
                "name", 
                "flowId", 
                "stage", 
                "sharedWith"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "!create"
            ],
            "attributes" : [ 
                "customFields"
            ]
        }
    ]
}
{
    "_id" : ObjectId("5f4e654c2c2d2f4d59ca89f9"),
    "userId" : ObjectId("5f3696f102ea801d405bb10e"),
    "flowId" : "46d80c28-72c3-42f1-94ba-2c161e26f041",
    "stage" : "stopped",
    "name" : "New flow",
    "btime" : ISODate("2020-09-01T15:14:20.999Z"),
    "mtime" : ISODate("2020-09-01T15:14:20.999Z"),
    "flow" : {},
    "mode" : "module",
    "sharedWith" : [],
    "customFields" : {
        "visible" : "test visible",
        "not-visible" : "test non visible"
    }
}
{
    "type" : "routes",
    "acl" : [ 
        {
            "role" : "admin",
            "resource" : "flows",
            "action" : [ 
                "*"
            ],
            "attributes" : [ 
                "*"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "read"
            ],
            "attributes" : [ 
                "*", 
                "!customFields.not-visible"
            ]
        }, 
        {
            "role" : "user",
            "resource" : "flows",
            "action" : [ 
                "create"
            ],
            "attributes" : [ 
                "*", 
                "!customFields"
            ]
        }
    ]
}
$ curl -XPOST "https://api.appmixer.com/flows" \
-H "Content-Type: application/json" \
-d '{ "flow": FLOW_DESCRIPTOR, "name": "My Flow #1", "customFields": { "category": "healthcare" } }'
$ curl -XPUT "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e" \
-H "Content-Type: application/json" \
-d '{ "customFields": { "category": "healthcare" } }'
appmixer.api.createFlow('myFlow', FLOW_DESCRIPTOR, {
    "customFields": { "category": "healthcare" }
});
appmixer.api.updateFlow('9089f275-f5a5-4796-ba23-365412c5666e', {
    "customFields": { "category": "healthcare" }
});
$ curl "https://api.appmixer.com/flows?filter=customFields.category:healthcare" \
-H "Authorization: Bearer [ACCESS_TOKEN]"
$ curl "https://api.appmixer.com/flows?filter=customFields.category:!healthcare" \
-H "Authorization: Bearer [ACCESS_TOKEN]"
appmixer.api.getFlows({
    limit: 20,
    offset: 0,
    projection: "-thumbnail",
    sort: "mtime:-1",
    filter: "customFields.category:healthcare"
})
const flowManagerTemplates = new appmixer.ui.FlowManager({
    el: '#your-flow-manager-templates-div',
    options: {
        customFilter: {
            'customFields.template': 'true'
        }
    }
});
flowManagerTemplates.open();
const flowManagerFlows = new appmixer.ui.FlowManager({
    el: '#your-flow-manager-flows-div',
    options: {
        customFilter: {
            'customFields.template': '!true'
        }
    }
});
flowManagerFlows.open();
customFields
...
<div id="my-am-flow-manager"></div>
...
<script src="https://my.YOURTENANT.appmixer.cloud/appmixer/appmixer.js"></script>
const appmixer = new Appmixer({ baseUrl: 'https://api.YOURTENANT.appmixer.cloud' });
const profile = {
   email: 'john@doe.com',
   name: 'John Doe'
};
let auth;
if (!profile.appmixerUserName) {
   // First time the user interacts with Appmixer. -> Create a virtual 
   // Appmixer user on the fly.
   const appmixerUserPassword = Math.random().toString(36).slice(-8);
   auth = await appmixer.api.signupUser(profile.email, appmixerUserPassword);
   profile.appmixerUserName = profile.email;
   profile.appmixerUserPassword = appmixerUserPassword;
   // Update your user profile with the newly created Appmixer virtual user credentials.
   await callYourAPIToUpdateUser(profile);
} else {
   // We already have a shadow Appmixer virtual user for our user created. ->
   // Simply authenticate.
   auth = await appmixer.api.authenticateUser(profile.appmixerUserName, profile.appmixerUserPassword); 
}
appmixer.set('accessToken', auth.token);
// Now your SDK is initialized and Appmixer virtual user is ensured.
// We can render our FlowManager.
const flowManager = appmixer.ui.FlowManager({ el: '#my-am-flow-manager' });
flowManager.open();
curl -XPOST "https://api.YOURTENANT.appmixer.cloud/user/auth" -H "Content-type: application/json" -d \
'
{ "username": "myadminuser@example.com", "password": "abc123" }
'
{
  "user": {
    "id": "583c06511afb7b0016ef120b",
    "username": "myadmin@example.com",
    "isActive": true,
    "email": "myadmin@example.com",
    "plan": "free",
    "vendor": [
      "appmixer"
    ]
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6...."
}
curl -XPOST "https://api.YOURTENANT.appmixer.cloud/user" -H "Content-type: application/json" -d \
'
{
   "username": "john@doe.com",
   "email": "john@doe.com",
   "password": "test123" 
}
'
{
    token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZC..."
}    
curl -XGET "https://api.YOURTENANT.appmixer.cloud/user" -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZC...'
{
  "id": "583c06511afb7b0016ef120b",
  "username": "john@doe.com",
  "isActive": true,
  "email": "john@doe.com",
  "scope": [
    "user"
  ],
  "plan": "free"
}
curl -XPUT "https://api.YOURTENANT.appmixer.cloud/users/583c06511afb7b0016ef120b" -H "Content-type: application/json" -H 'Authorization: Bearer ADMIN_USER_TOKEN' -d \
'
{
    "scope": ["acme1"]
}
'
curl -XGET "https://api.YOURTENANT.appmixer.cloud/users?filter=scope:acme1&sort=created:-1&limit=30&offset=0" -H 'Authorization: Bearer ADMIN_USER_TOKEN'Configure ACL rules for a scope
curl -XGET "https://api.YOURTENANT.appmixer.cloud/users?pattern=doe&sort=created:-1&limit=30&offset=0" -H 'Authorization: Bearer ADMIN_USER_TOKEN'
curl -XGET "https://api.YOURTENANT.appmixer.cloud/acl/routes" -H 'Authorization: Bearer ADMIN_USER_TOKEN'
[
  {
    "role": "admin",
    "resource": "flows",
    "action": [
      "*"
    ],
    "attributes": [
      "*"
    ]
  },
  {
    "role": "user",
    "resource": "flows",
    "action": [
      "*"
    ],
    "attributes": [
      "*"
    ]
  }
]
curl -XPOST "https://api.YOURTENANT.appmixer.cloud/acl/routes" -H "Content-type: application/json" -H "Content-type: application/json" -H 'Authorization: Bearer ADMIN_USER_TOKEN' -d@routes-acl.json
[
  {
    "role": "admin",
    "resource": "flows",
    "action": [
      "*"
    ],
    "attributes": [
      "*"
    ]
  },
  {
    "role": "user",
    "resource": "flows",
    "action": [
      "*"
    ],
    "attributes": [
      "*"
    ]
  },
  {
    "role": "acme1",
    "resource": "flows",
    "action": [
      "*"
    ],
    "attributes": [
      "*"
    ]
  }
]
curl -XGET "https://api.YOURTENANT.appmixer.cloud/acl/components" -H 'Authorization: Bearer ADMIN_USER_TOKEN'
[
  {
    "role": "admin",
    "resource": "*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  },
  {
    "role": "user",
    "resource": "*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  }
]  
[
  {
    "role": "admin",
    "resource": "*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  },
  {
    "role": "user",
    "resource": "*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  },
  {
    "role": "acme1",
    "resource": "appmixer.utils*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  },
  {
    "role": "acme1",
    "resource": "appmixer.twilio*",
    "action": [
      "*"
    ],
    "attributes": [
      "non-private"
    ]
  }
]  
curl -XPOST "https://api.YOURTENANT.appmixer.cloud/acl/components" -H "Content-type: application/json" -H "Content-type: application/json" -H 'Authorization: Bearer ADMIN_USER_TOKEN' -d@components-acl.json
Public files
private
Integration templates
groups
HTTP cookies RFC specification
RFC 6902
a quick reference
JSON Pointer
JSON Path expression
JMESPath
tranform functions
OpenAPI runtime expressions
OpenAPI runtime expressions
OpenAPI runtime expressions
https://bored.api.lewagon.com/
swagger2openapi
Flows
/flows
/flows
User endpoints
Users
https://my.YOURTENANT.appmixer.cloud
POST /user
system configuration
validation errors
Appmixer Backoffice
API
ACL
custom metadata
SDK Getting Started guide
All appmixer components allowed to be used by all users.
Only utils, google and slack components available.
Read only access to /flows endpoint.