arrow-left
Only this pageAll pages
gitbookPowered by GitBook
triangle-exclamation
Couldn't generate the PDF for 156 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

6.3

Loading...

Getting Started

Loading...

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

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

Loading...

Loading...

Appmixer UI SDK

Loading...

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

Loading...

Appmixer CLI

Loading...

Loading...

Appmixer Self-Managed

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

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.

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

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

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

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.

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

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.

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

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

hashtag
A Step-by-step Guide

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

hashtag
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 Gmail - New Email 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 "On Start" 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 "New Entry" trigger.

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

hashtag
Add Actions

Click Add Step on the side of the New Entry trigger and find Slack. Pick the Send Channel Message action. 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 - Send Prompt action. Configure the model, set the response type to json_object, and define the prompt as follows:

You can copy paste the prompt from here: From the below product feedback on improvements and additional features that was posted by a customer of our product Appmixer, create a user story for developers so that they have a concise description with a test case defining the feature. Format the user story as a JSON object with "title" and "content" properties. The "content" must contain a Github Markdown text with the following sections: "title", "role", "goal", "reason", "acceptance_criteria" and "test_case". Product feedback: <map your data variable from the form here>

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:

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

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

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

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

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

{
    "user": {
        "id": "5c88c7cc04a917256c726c3d",
        "username":"[email protected]",
        "email": "[email protected]"
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cC..."
}

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

Find the full documentation to the REST API .

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:

circle-info

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.

hashtag
Triggering OnAppEvent using Appmixer 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.

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.

You can also trigger the event by using the built-in UI in Appmixer Studio:

  1. Open the Integration Designer in Studio and create a new integration with an OnAppEvent trigger.

  2. Instead of publishing, click Start Test to run a temporary test instance of your integration.

  3. While the test is running, click Test → Send App Event in the top bar.

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

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.

Install and Update Connectors

Manage the modules available in the system.

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.

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

state

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

private

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

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

author

The author of the component. Example:

{
    "author": "David Durman <[email protected]>"
}

label

(optional)

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

{ "label": "Create Tweet" }

Enter the event name (as configured in your trigger) and any test data you want to send.

  • Submit the form to trigger your integration test and observe the results before publishing.

  • Sign-in endpoint

    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.

    hashtag
    Installing a connector

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

    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 "Custom OAuth Credentials" section of the documentation for guidance.

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

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

    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 System Webhook, to which you can subscribe for notifications. The impact of such failures varies by component type:

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

    • 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 Unprocessed Messages API. Should the connector be re-installed before exhausting all retry attempts, the action component can recover and will continue to process messages.

    hashtag
    Installing Connectors from your filesystem

    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 "Build a Custom Connector" section. Alternatively, you can publish your connector as a zip archive through the Appmixer REST API; for this method, consult the "Apps endpoints" 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.

    circle-info

    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.

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

    circle-info

    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.

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

    Before proceeding with connector upgrades, it's recommended to configure the System Webhook 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.

    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.

    Appmixer Backoffice
    Select a trigger in your Automation
    Authenticate and configure your trigger

    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.

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

    hashtag
    Create an Integration Template

    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.

    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 clicking Add Step on the right side of the On App Event component. Then type in "condition" and select it.

    Configure the Condition component to check whether the hotLead property from the OnAppEvent trigger is set to true. Use the "+" button next to any configuration field. This feature allows you to reference data from any component earlier in the integration flow (data variables), regardless of its depth in the workflow.

    Next, click Add Step on the true output port of the Condition step. Then, find the Slack - Send Channel Message component:

    hashtag
    Select Configuration Fields to Collect from your End-Users

    Our integration template is nearly complete. The next step is to setup the Slack - Send Channel Message component. This component includes three configuration fields: Slack account, Slack channel, and Message. 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 will authenticate with their own Slack account, choose their desired Slack channel, and tailor the message to be sent.

    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 (the final product of this setup) for end-user customization:

    You can make it easier for your users by choosing default values for any field. To do this, simply enter a value into the 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. 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, edit the name of the integration:

    hashtag
    Test your Integration

    At this point, our integration is prepared for publishing. However, it is important to test it to see that it works as expected. 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 can put yourself in the shoes of your users and test it.

    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:

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

    Please note, if Appmixer has not yet been , you can use two quick ways of seeing what the final experience looks like:

    • Go to https://YOUR_TENANT.appmixer.cloud/integrations (replace YOUR_TENANT with the name of your trial account - the one you see in your browser) to quickly see a preview of what the published template can look like in an embedded integration marketplace.

    • Alternatively, go to our demo web app, which also allows you to preview how your embedded integration marketplace will appear in your app once the actual embedding is completed.

    description

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

    Component Description

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

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

    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.

    hashtag
    Install the Appmixer SDK

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

    circle-info

    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.

    The Connector Configuration is available via the interface:

    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.

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

    webhook

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

    httpRequestMethods

    By default, you can send GET, POST, PUT, or DELETE HTTP requests to a component. With the property httpRequestMethods you can define, which HTTP methods are allowed. Additionally, you can add support for the OPTIONS method:

    End User Guide

    hashtag
    Knowledge base

    Please visit the for end-user tutorials.

    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.

    Quick Start

    Refer to the getting started guide to get started quickly.

    icon

    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:

    tick

    When set to true, the component will receive signals in regular intervals from the engine. The tick() Component Virtual method will be called in those intervals (see ). 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.

    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.

    Getting Started

    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:

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

    marker

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

    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:

    appmixer.api.sendAppEvent('contact-created', {
        email: '[email protected]',
        fname: 'David',
        lname: 'Doe'
    });
    curl -XPOST \
               -H 'Content-Type: application/json' \
               -H "Authorization: Bearer VIRTUAL_USER_ACCESS_TOKEN" \
               -d '{ "email": "[email protected]", "fname": "David", "lname": "Doe" }' \
               "https://APPMIXER_TENANT_API_URL/plugins/appmixer/utils/appevents/events/contact-created"
    {
        "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."
        }
    }
    {
        "title": CHATGPT_INFERED_USER_STORY_TITLE,
        "content": CHATGPT_INFERED_USER_STORY_DESCRIPTION_IN_MARKDOWN
    }
    Behaviour
    context.getWebhookUrl()
    Appmixer Knowledge basearrow-up-right
    Embed into Your Application
    Component Behaviour
    [
      {
        "filename": "test.txt"
      }
    ]

    hashtag
    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"'

    hashtag
    Request Body

    Name
    Type
    Description

    filename*

    String

    The name for the file

    file*

    File

    The file to be uploaded

    { "ok": true }

    hashtag
    Removes a public file

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    filename*

    String

    The name of the file you want to remove

    {
        "name": "appmixer.utils.http.WebhookWithOPTIONS",
        "description": "Support for OPTIONS and POST",
        "webhook": true,
        "httpRequestMethods": [ "POST", "OPTIONS" ], 
        ...
    }
    { "name": "appmixer.twitter.statuses.CreateTweet" }
    {
        "icon": "data:image/svg+xml;base64,PD94bWwgdmV..."
    }
    https://en.wikipedia.org/wiki/Data_URI_schemearrow-up-right

    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:

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

    { "ok": true }

    Note that YOUR_TENANTis the name of your tenant. For example, if the URL of your hosted version of Appmixer is https://my.eminent-emu-12345.appmixer.cloudarrow-up-right, then YOUR_TENANTis eminent-emu-12345.

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

    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.

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

    circle-info

    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 Appmixer Virtual Users.

    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 [email protected] 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.

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

    Default - not yet themed - embedded Integration Marketplace

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

    Appmixer Designer Embedded in your SaaS product
    Appmixer Flow Manager Embedded in your SaaS product

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

    Changed UI Strings in embedded Flow Manager

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

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

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

    hashtag
    Demo Applications

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

    • https://github.com/Appmixer-ai/appmixer-demo-embedded-integrationsarrow-up-right

    • https://github.com/Appmixer-ai/appmixer-demo-embedded-designerarrow-up-right

    • https://github.com/Appmixer-ai/appmixer-demo-firebase-vanillaarrow-up-right

    hashtag
    Custom OAuth Credentials
    circle-info

    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.

    circle-info

    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.

    hashtag
    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 facilitate this process, the Appmixer Backoffice 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.

    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.

    Appmixer Backoffice
    {
        "marker": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL..."
    }
    https://en.wikipedia.org/wiki/Data_URI_schemearrow-up-right
    Beta badge
    App Events
    embedded into your product
    http://localhost:8080arrow-up-right
    Sign-in page
    Sign-up page

    auth

    The authentication service and parameters. For example:

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

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

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

    Storage

    Manage records associated with data storage utility components of flows.

    Storage

    hashtag
    Configuration

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

    hashtag
    config.el ...

    circle-info

    Learn about widget config .

    hashtag
    config.storeId

    Type: String | Default: []

    ID of a store to open within the storage.

    hashtag
    Instance

    circle-info

    Learn about widget instance .

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Example

    Insights Chart Editor

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

    Insights Chart Editor

    hashtag
    Configuration

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

    hashtag
    config.el ...

    circle-info

    Learn about widget config .

    hashtag
    Instance

    circle-info

    Learn about widget instance .

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Events

    hashtag
    close

    Close the editor.

    hashtag
    Example

    Config

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

    hashtag
    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

    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.

    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.

    quota

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

    hashtag
    Dynamic values

    The {{}} can be used in any property within the quota definition. And values from two objects - the user's , and the account's

    Accounts

    Manage accounts authorized by the current user.

    hashtag
    Configuration

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

    Installation

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

    hashtag
    Basic Usage

    Load appmixer.js UMD module in your HTML file:

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

    Connectors

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

    hashtag
    Configuration

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

    People Tasks

    Manage tasks created by utility components of flows.

    hashtag
    Configuration

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

    Dependencies

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

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

    circle-info

    firePatterns

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

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

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

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

    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.

    hashtag
    Setting a custom API option

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

    The list of API methods can be found .

    Files

    Manage files for use with components of flows.

    hashtag
    Configuration

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

    Authentication Hub

    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.

    The Authentication Hub is enabled for all hosted Appmixer tenants. In self-managed installations, the Authentication Hub has to be explicitly set.

    hashtag

    Installation AWS ECS

    Appmixer can be installed in an AWS account via our public Terraform module.

    hashtag
    Prerequisities

    <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'
            }
        }
    });
    
    const storage = appmixer.ui.Storage(config)
    
    storage.set(key, value)
    storage.get(key)
    const insightsChartEditor = appmixer.ui.InsightsChartEditor(config)
    
    insightsChartEditor.set(key, value)
    insightsChartEditor.get(key)

    hashtag
    Create a configuration key/value pair

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

    hashtag
    Request Body

    Name
    Type
    Description

    key*

    String

    Configuration key

    value*

    Any

    Configuration value

    hashtag
    Removes a configuration entry

    DELETE https://api.YOUR_TENANT.appmixer.cloud/config/:key

    hashtag
    Path Parameters

    Name
    Type
    Description

    key*

    String

    The key of the configuration to be removed

    [
      {
        "key": "JWTSecret",
        "value": "OQekJ3DH4pRnWFl4wlN0hzhc5UIjdihEwFnwYLYUdXGXk+/f5JieT/1VLPUJnvALIGK014md41rUuarqYZscl2T5azHQmFhQmUKj8dEuoIELWB45wlkxDKcojCQi9Otk76itnmvKrbm/ZokDJxePNv2Edgc7/mLrTHG7l54w44c="
      },
      {
        "key": "WEBHOOK_FLOW_COMPONENT_ERROR",
        "value": "https://example.com/webhook"
      }
    ]
    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.

    More information on the package.json file can be found at https://docs.npmjs.com/files/package.jsonarrow-up-right.

    {
        "name": "appmixer.twilio.sms.SendSMS",
        "version": "1.0.0",
        "private": true,
        "main": "SendSMS.js",
        "author": "David Durman <[email protected]>",
        "dependencies": {
            "twilio": "^2.11.0"
        }
    }
    can be used there. The following example shows how to dynamically select a resource based on the value of the user's
    .tier .

    The quota.js file with the rules for the previous example could look like this:

    The other object that can be used here is the profileInfo.

    hashtag
    quota.manager

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

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

    Dynamic values. Sometimes, different users have different quotas for the same service.

    hashtag
    quota.scope

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

    quota manager
    metadata
    profileInfo
    metadata
    circle-info

    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.

    hashtag
    Advanced Usage

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

    Choose Appmixer UI widgets to include:

    <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>
    https://github.com/clientIO/appmixer-demo-embedded-integrationsarrow-up-right
    https://github.com/clientIO/appmixer-demo-firebase-vanillaarrow-up-right
    hashtag
    config.el ...
    circle-info

    Learn about widget config here.

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Example

    An example how to redefine the flow update request.
    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 });
    here
    /* 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
                });
            }
        }
    });
    hashtag
    config.el ...
    circle-info

    Learn about widget config here.

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message. query

    Type: Object | Default: DefaultQuery

    Defines custom query parameters for retrieving files. Example:

    hashtag
    Events

    hashtag
    flow:open

    Triggered when the user selects a flow associated with a file listed in the widget.

    hashtag
    Example

    AWS CLIarrow-up-right

  • Terraformarrow-up-right

  • Cofigured AWS CLIarrow-up-right

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

  • hashtag
    Installation Types

    There are two types of installations:

    • Default installation

    • Custom installation

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

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

    hashtag
    Preparation

    First, clone the Appmixer Module AWSarrow-up-right git project with the Terraform code:

    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:

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

    hashtag
    Installation

    You should now be ready to provision the Appmixer stack.

    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:

    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.

    AWS accountarrow-up-right
    {
      "key": "myConfigKey",
      "value": "My Custom Value"
    }
    { "ok": true }
    {
         "quota": {
            "manager": "pipedrive",
            "resources": "requests",
            "scope": {
                "userId": "{{userId}}"
            }
        }
    }
    {
         "quota": {
            "manager": "your-service",
            // Before the quota request is created, the system will check the user's
            // metadata.tier value. If set, it will be used as a 'resources' value.
            // If not, the value 'basic' will be used.
            "resources": "{{userMetadata.tier || 'basic'}}",
            "scope": {
                "userId": "{{userId}}"
            }
        }
    }
    module.exports = {
        rules: [
            {
                name: 'basic-tier',
                limit: 10,          // 10 requests per minute
                window: 1000 * 60,  // 1 minute
                throttling: 'window-sliding',
                queueing: 'fifo',
                resource: 'basic',  // the 'basic' resource
                scope: 'userId'
            },
            {
    
                name: 'paid-tier',
                limit: 100,         // or 100 requests per minute
                window: 1000 * 60,  // 1 minute
                throttling: 'window-sliding',
                queueing: 'fifo',
                resource: 'paid',   // the 'paid' resource
                scope: 'userId'
            }
        ]
    };
    {
         "quota": {
            "manager": "your-service",
            // Before the quota request is created, the system will check the user's
            // account profileInfo.tier value. If set, it will be used as a 'resources' value.
            // If not, the value 'basic' will be used.
            "resources": "{{profileInfo.tier || 'basic'}}",
            "scope": {
                "userId": "{{userId}}"
            }
        }
    }
    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(/* ... */)
    const connectors = appmixer.ui.Connectors(config)
    
    connectors.set(key, value)
    connectors.get(key)
    connectors.state(name, value)
    const connectors = appmixer.ui.Connectors({
        el: '#connectors'
    })
    
    connectors.open()
    const files = appmixer.ui.Files(config)
    
    files.set(key, value)
    files.get(key)
    files.state(name, value)
    // Set a custom query.
    files.state('query', {
        pattern: 'my custom pattern',
        sort: { uploadDate: -1 }
    });
    
    // Listen for query changes triggered by user interaction.
    files.on('change:query', query => {
        console.log('Current query:', query);
    });
    files.on(event, handler)
    files.on('flow:open', flowId => {/* ... */})
    const files = appmixer.ui.Files({
        el: '#files'
    })
    
    files.open()
    $ git clone https://github.com/clientIO/appmixer-module-aws.git
    $ cd appmixer-module-aws/
    $ cd examples/development
    $ terraform init
    $ terraform plan
    $ terraform apply
    Inspector panel

    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.

    https://openweathermap.orgarrow-up-right
    Flow
    hashtag
    config.el ...
    circle-info

    Learn about widget config here.

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Events

    hashtag
    flow:open

    Select a flow to open in Designer widget.

    hashtag
    Example

    Accounts
    hashtag
    config.el ...
    circle-info

    Learn about widget config here.

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Example

    People Tasks
    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:

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

    {
        "firePatterns": ['*', 1]
    }
    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.

    To request the AUTH_HUB_TOKEN, contact our customers support by sending email to [email protected]arrow-up-right.

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

    Connector Configuration
    here
    here
    here
    here
    Connected Accounts

    Build and Run an AI agent

    hashtag
    What This AI Agent Does

    In the following sections, you'll learn how to build a support AI agent that can access your data—specifically, support tickets and details about your development teams, including their roles and the contact information of the team lead.

    This AI agent can analyze support tickets (e.g., identifying the biggest pain points your customers faced in the past month) and take action—such as suggesting product improvements based on the most common issues and sending those suggestions to the relevant team lead responsible for that part of your application.

    Sounds useful? Let’s build it!

    hashtag
    Step 1: Setting Up the AI Agent

    To begin, we'll create a new workflow in Appmixer, our no-code AI agent builder.

    ‍

    hashtag
    Choosing a Trigger

    The first step is defining what will activate the agent. In this demo, we use a chat widget as the trigger, but you can also trigger the agent based on other events—for instance, new emails, new tasks, or scheduling the agent to run at a specific date and time.

    Once the chat trigger is selected, Appmixer provides a chat URL and a chat script that allows you to integrate the chat widget into any web application.

    ‍

    hashtag
    Adding the AI Agent

    Now, let’s add the central component—the AI agent itself. Appmixer supports various large language models (LLMs), including OpenAI, Claude, Gemini, and more. For this demo, we'll use an OpenAI model.

    We define the agent's instructions, specifying its role and scope. The key to building a reliable AI agent is providing detailed and structured instructions so that responses are consistent and accurate. Each message from the chat is processed based on these instructions, ensuring contextual understanding.

    ‍

    Instructions: This field defines how the AI agent should behave each time it performs a task. It specifies the agent’s role, scope, and operational constraints, ensuring it follows predefined guidelines while executing its tasks.

    👉 Copy and paste the instructions to build the same AI agent in Appmixer:

    ‍

    In the Prompt field, we'll use the dynamic field from the Chat trigger. This will be the actual message that you or your users type in the chat.

    ‍

    To maintain conversation continuity, we set the Thread ID field, which allows the agent to remember the context of ongoing interactions.

    ‍

    hashtag
    Handling the Agent’s Responses

    The agent has two output ports:

    • Out: This sends the agent's answers to the chat widget (or any other tool, such as sending a message to Slack).

    • Tools: This is where we connect the agent to our data and services, whether first-party or third-party.

    To complete the chat functionality, we add a Chat Reply component, linking it to the thread ID and the agent’s response output.

    ‍

    hashtag
    Step 2: Adding Skills to the AI Agent

    While our agent can now respond to general questions, we want it to perform real tasks, such as analyzing data and sending emails.

    hashtag
    Connecting to External Tools

    Under the Tools port, we can add multiple integrations. Whenever the AI agent receives a request, it checks for relevant tools and uses them as needed.

    In order to add tools to the AI agent, you need to get familiar with two terms:

    Tool description: This serves as a guideline for the AI agent, explaining what a specific tool does. It is set at the Tool Start component level. For example, if a tool sends notifications to Slack, its description could be: "This tool sends notifications to Slack." The AI agent uses this information to determine when to utilize the tool to complete a task.

    ‍

    And second, we use something called Parameters in the Tool Start component.

    Parameters: Dynamic fields that we want the AI agent to figure out from the user prompt and then later use in the tools. For instance, if we want the agent to lookup products in our database, we need to set up Product Name on the Tool Start level, let the agent fill the value from the user prompt and then use it to do the lookup.

    ‍

    Sending Emails

    1. Add a Tool Start component and define it as “Send emails to the product team.”

    2. Define parameters such as recipient, subject, and body, allowing the AI to dynamically generate content.

    3. Use the Send Email step to structure and send the email.

    👉 Copy and paste the tool description to build the same AI agent in Appmixer:

    ‍

    ‍

    Analyzing Support Tickets

    1. Add another Tool Start component and describe its function as retrieving support tickets.

    2. Connect a Google Sheets Get Rows component to fetch past support tickets.

    3. Configure the Tool Output to send ticket data to the agent, enabling it to analyze trends.

    ‍

    👉 Copy and paste the tool description to build the same AI agent in Appmixer:

    ‍

    📄 Copy our Google Sheet with 100 demo support tickets and connect it to Appmixer to build the same AI agent as above:

    ‍

    Understanding Product Teams

    1. Similar to the support tickets, add a Tool Start component for product teams.

    2. Use another Google Sheets Get Rows component to retrieve team details.

    3. Map the data so that the agent can provide insights into which team is responsible for which product features.

    👉 Copy and paste the tool description to build the same AI agent in Appmixer

    ‍

    📄 Clone our Google Sheet team details and connect it to Appmixer to build the same AI agent as above:

    ‍

    hashtag
    Step 3: Testing and Deploying the AI Agent

    Once all components are set up, it’s time to test our AI agent using the Chat URL.

    For example, we can:

    • Ask it about common issues in the mobile app from past support tickets.

    • Request product improvement suggestions based on user feedback.

    • Instruct it to send these suggestions to the mobile app team.

    For deployment, we take the provided HTML embed script from the chat trigger setup and add it to our web application. This allows our AI agent to be accessible wherever it is needed, providing automated insights and executing tasks in real time.

    hashtag
    Expanding Your AI Agent’s Capabilities

    The modular nature of Appmixer's no-code builder allows for easy expansion.

    For instance, you can:

    • Integrate vector database for large-scale data retrieval.

    • Connect additional execution tools like task assignment in Asana, ClickUp, or Jira.

    • Create multi-agent workflows where each AI agent specializes in a different function, such as:

    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.

    Flow

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

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

    Constructor

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

    hashtag
    Configuration

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

    hashtag
    baseUrl

    Type: String | Default: null

    Base URL of your Appmixer engine REST API.

    hashtag
    accessToken

    Type: String | Default: null

    Access token of an authorized user.

    hashtag
    debug

    Type: Boolean | Default: false

    Enable debugger for development purposes.

    hashtag
    theme

    Type: Object | Default: DefaultTheme

    hashtag
    l10n

    Type: Object | Default: DefaultL10N

    Define custom localization texts.

    hashtag
    lang

    Type: String | Default: en

    Specify a language code for the localization of components.

    hashtag
    api

    Type: Object | Default: {}

    Set custom API methods.

    hashtag
    Instance

    hashtag
    appmixer.ui

    Register and create UI Widgets.

    hashtag
    appmixer.api

    Use methods of built-in API Module.

    hashtag
    appmixer.set

    Set configuration property.

    hashtag
    appmixer.get

    Get configuration property.

    hashtag
    appmixer.registerCustomComponentShape

    Register a custom Designer component shape.

    hashtag
    appmixer.registerInspectorField

    Register a custom Designer inspector field.

    Insights Dashboard

    Browse and manipulate charts created by the current user.

    Insights Dashboard

    hashtag
    Configuration

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

    hashtag
    config.el ...

    circle-info

    Learn about widget config .

    hashtag
    Instance

    circle-info

    Learn about widget instance .

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Events

    hashtag
    chart:clone

    Clone chart.

    hashtag
    chart:remove

    Remove chart.

    hashtag
    chart:open

    Open chart in Chart Editor.

    hashtag
    Example

    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.

    circle-info

    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.

    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.

    circle-exclamation

    Only users with admin scope can use these endpoints.

    hashtag
    Get Services Configuration

    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:

    Wizard

    Manage a flow that is used as an integration instance.

    hashtag
    Configuration

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

    UI & Widgets

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

    hashtag
    Configuration

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

    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.

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

    circle-info

    Audit Logs

    hashtag
    Overview

    Audit Logs provide a comprehensive record of all activities and changes within your Appmixer tenant. This feature enables administrators to track user actions, monitor system changes, and maintain security compliance by reviewing who did what and when.

    Installation Helm Chart

    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.

    hashtag
    Prerequisites

    • Kubernetes 1.29+ or OpenShift 4.16+

    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 peopleTasks = appmixer.ui.PeopleTasks(config)
    
    peopleTasks.set(key, value)
    peopleTasks.get(key)
    peopleTasks.state(name, value)
    const peopleTasks = appmixer.ui.PeopleTasks({
        el: '#people-tasks'
    })
    
    peopleTasks.open()
    {
        "firePatterns": [1, 1]
    }
    {
        "firePatterns": [
            ['*', 1],
            [1, 0]
        ]
    }
    # 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
    
    storage.state(name, value)
    const storage = appmixer.ui.Storage({
        el: '#storage'
    })
    
    storage.open()
    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)
    const insightsDashboard = appmixer.ui.InsightsDashboard(config)
    
    insightsDashboard.set(key, value)
    insightsDashboard.get(key)
    GET https://api.YOUR_TENANT.appmixer.cloud/service-config

    Get a list of stored configurations.

    hashtag
    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,
    	}
    ]

    hashtag
    Get Service Configuration

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

    Get the configuration stored for the given service.

    hashtag
    Path Parameters

    Name
    Type
    Description

    string

    The service id. Example: appmixer:google

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

    hashtag
    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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    serviceId

    string

    The service id. Example

    hashtag
    Request Body

    Name
    Type
    Description

    whatever-key

    string

    Any value you need

    hashtag
    Delete Service Configuration

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

    Removes the configuration from the given service.

    hashtag
    Path Parameters

    Name
    Type
    Description

    serviceId

    string

    The service id. Example: appmixer:google

    localization

    An optional object containing localization strings. For example:

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

    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.

    hashtag
    Quota Module Structure

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

    hashtag
    rules

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

    hashtag
    limit

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

    hashtag
    window

    The time window in milliseconds.

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

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

    circle-info

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

    hashtag
    config.el

    Type: String|Element | Default: null

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

    hashtag
    config.theme

    Type: Object | Default: DefaultTheme

    Custom theme definition.

    hashtag
    config.l10n

    Type: Object | Default: DefaultL10N

    Custom localization texts.

    hashtag
    config.lang

    Type: String | Default: en

    Language code for localization of components.

    hashtag
    config.api

    Type: Object | Default: DefaultAPI

    Custom API methods.

    hashtag
    Instance

    hashtag
    widget.open

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

    hashtag
    widget.close

    Unmount the widget instance and hide the el container.

    hashtag
    widget.reload

    Reload the entire widget.

    hashtag
    widget.reset

    Reset the state of the widget to defaults.

    hashtag
    widget.state

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

    chevron-rightExamplehashtag

    hashtag
    widget.set

    Set config property.

    hashtag
    widget.get

    Get config property.

    hashtag
    widget.on

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

    hashtag
    widget.off

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

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

    Add a Tool Output component to send confirmation back to the agent.

    Scraping websites

  • Generating marketing content

  • Analyzing market trends

  • Writing product specifications

  • https://docs.google.com/spreadsheets/d/1zWsRnL4-Sj-Rh-j3wlu9XdoIrj17uPWC7nMvL3vfskA/edit?usp=sharingarrow-up-right
    https://docs.google.com/spreadsheets/d/1JmyOaQxho-mBNS8iLm0uwL-X30G5tO4EK3Ha0MsLu7Q/edit?usp=sharingarrow-up-right
    To manage errors within running integrations or automations, configure the WEBHOOK_FLOW_COMPONENT_ERROR system variable with a custom URL in the Appmixer Backoffice 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.

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

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

    An example email notification to your end-users.

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

    Log viewer in Designer.

    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:

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

    hashtag
    config.el ...
    circle-info

    Learn about widgetconfig here.

    hashtag
    config.flowId

    Type: String | Default: null

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

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Events

    hashtag
    flow:start

    Submit the form and start the flow.

    hashtag
    flow:validation

    Flow validation errors changed.

    hashtag
    cancel

    Click a button to close the form.

    hashtag
    close

    Submit the form and wait for the flow to start.

    hashtag
    Example

    Wizard

    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.

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

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

    hashtag
    Time Localization

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

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

    hashtag
    Pluralization and Strings with Variables

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

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

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

    hashtag
    Accessing Audit Logs

    Navigate to System > Audit Logs to view the audit log interface for your tenant.

    hashtag
    Using the Audit Logs Interface

    hashtag
    Filtering Audit Logs

    The Audit Logs page provides multiple filtering options to help you find specific events:

    Search

    Use the search bar to find audit log entries by keyword. This searches across action descriptions and related identifiers.

    Date Range

    Filter logs by selecting a specific date range. Click the Date Range selector to choose:

    • A custom date range

    Event Type

    Filter logs by the type of action performed. Select from:

    • Create - Events related to creating new resources

    • Update - Events related to modifying existing resources

    • Delete - Events related to removing resources

    User

    Filter logs by specific user to see all actions performed by that user.

    Flow ID

    Filter logs to show only events related to a specific flow by entering the Flow ID.

    hashtag
    Applying Filters

    1. Set your desired filter criteria using any combination of the options above

    2. Click the FILTER button to apply your filters

    3. Use RESET to clear all filters and return to the full log view

    4. Click RELOAD to refresh the audit log data

    hashtag
    Viewing Audit Log Details

    The audit log table displays:

    • Action - Description of the event that occurred

    • User - The user who performed the action (displayed as email address)

    • Created - Timestamp when the event occurred

    • Details icon (eye icon) - Click to view additional details about the specific event including a diff table

    hashtag
    Logged Events

    Appmixer tracks the following events across different areas of the system:

    hashtag
    System Configuration

    • Configuration Updated

    • Configuration Deleted

    hashtag
    Access Control

    • ACL Rule Created/Updated

    • Component Access Assigned

    hashtag
    Flow Management

    • Flow Created

    • Flow Updated

    • Flow Started/Stopped

    • Flow Deleted

    • Flow Cloned

    hashtag
    Integration Template Management

    • Integration Template Created

    • Integration Template Updated

    • Integration Template Published

    • Published Integration Template Updated

    • Integration Template Deleted

    hashtag
    Integration Template Testing

    • Integration Template Test Started

    • Integration Template Test Stopped

    • Integration Template Test Deleted

    hashtag
    Connector Management

    • Connector Installed

    • Connector Updated

    • Connector Uninstalled

    hashtag
    User Management

    • User Created

    • User Updated

    • User Deleted

    • User Logged In

    • Login Failed

    • Password Changed

    hashtag
    Use Cases

    hashtag
    Security Compliance

    Review audit logs to maintain compliance with security standards and policies. Track who accessed what resources and when changes were made.

    hashtag
    Troubleshooting

    Identify when changes were made that may have caused issues. Track the sequence of events leading up to a problem.

    hashtag
    User Activity Monitoring

    Monitor user actions to ensure proper usage of the platform and identify any unusual activity patterns.

    hashtag
    Change Tracking

    Maintain a historical record of all modifications to flows, templates, and system configurations for audit purposes.

    Helm 3.19+

  • MongoDB (deployed automatically by this chart)

  • Elasticsearch 8.x (via ECK operator)

  • Docker registry credentials


  • hashtag
    Features

    • Full Stack Deployment: Appmixer engine, API, backoffice, frontend, and quota service

    • Multiple Exposure Options: OpenShift Route or Kubernetes Ingress support

    • Production-Ready Components:

      • MongoDB ReplicaSet with automatic initialization (3 replicas)

      • Redis Sentinel for high availability (3 replicas)

      • RabbitMQ cluster for message brokering (3 replicas)

      • Elasticsearch with Kibana for logging and monitoring (ECK managed)

    • Auto-Scaling: Horizontal Pod Autoscaler (HPA) for engine components

    • Security: TLS/ACME support, configurable security contexts, private registry authentication

    • Customizable: All resources, replicas, and configurations adjustable via values.yaml


    hashtag
    Install the chart

    1. Create namespace

    1. Create AWS ECR credentials secret:

    1. Log in to AWS ECR registry:

    1. Pull the latest Appmixer Helm chart:

    4a. Pull the Appmixer Helm chart with version specified:

    1. (Optional) Untar the chart:

    1. Install required Elasticsearch CRDs

    1. Update the repository:

    1. Modify the values.yaml file to suit your needs. Change the expose.method from routes to ingress for Kubernetes. And set the domain.

    2. Install the chart:

    1. Access the Appmixer UI

    hashtag
    How to Access Appmixer

    If using OpenShift Routes:

    If using Kubernetes Ingress:

    hashtag
    Upgrading Appmixer Version

    Update the version in Chart.yaml:

    Then upgrade the release:

    here
    here

    Variables

    hashtag
    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

    Name
    Value

    Body

    Name
    Type
    Description

    Response

    hashtag
    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 } }'

    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.

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

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

    hashtag
    Request Body

    Name
    Type
    Description

    hashtag
    Create User

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

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

    hashtag
    Request Body

    Name
    Type
    Description

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

    Appmixer Embed Integrations Demodemo-integrations.appmixer.comchevron-right

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

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

    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.

    hashtag
    Get messages

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

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

    MCP Servers

    This guide will help you create and integrate a custom MCP (Model Context Protocol) server into Appmixer so that it can be used with the AI Agent component or as part of regular flows.

    hashtag
    Understanding MCP Server Integration in Appmixer

    In Appmixer, MCP servers can only be connected to dedicated mcp ports of AI Agent components. This restriction ensures that AI Agents can communicate with MCP servers through a controlled and consistent interface.


    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.

    hashtag
    Prerequisites

    You are a helpful assistant specializing in Product Insights. As an AI assistant you analyze support tickets to identify patterns and generate product improvement suggestions. You have access to two data sources: a support tickets database containing customer feedback and issues, a product teams directory with information about team responsibilities.
    
    When responding to queries, first analyze the ticket data to identify common themes. Then, when asked for recommendations, match issues to the appropriate product teams, and generate specific, actionable product improvement suggestions. For each suggestion, identify the responsible team lead's email address, format your suggestion professionally, and include data-backed reasoning. Always be concise and specific in your responses, providing relevant statistics from the ticket data and clear implementation recommendations.
    
    Finally, when asked to send an email with recommendations to a team lead, send the generated suggestions to the team lead of the relevant product team. Always reply confirming that you have sent the email and say to which email address you have sent it.
    Send emails to the product team.
    Read the support tickets submitted by users last month.
    Read the scopes and responsbilities and contact details of product teams.
    {
        "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": "[email protected]",
                                "type": "to"
                            }
                        ],
                        "from_email": "[email protected]"
                    },
                    "scope": {
                        "bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
                            "out": {
                                "started": "2020-12-14T11:20:41.858Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-12-14T11:20:41.858Z"
                    }
                }
            ]
        }
    }
    const wizard = appmixer.ui.Wizard(config)
    
    wizard.set(key, value)
    wizard.get(key)
    wizard.state(name, value)
    wizard.on(event, handler)
    wizard.on('flow:start', flowId => {/* ... */})
    wizard.on('flow:validation', errors => {/* ... */})
    wizard.on('cancel', () => {/* ... */})
    wizard.on('close', () => {/* ... */})
    const wizard = appmixer.ui.Wizard({
        el: '#wizard',
        flowId: 'your-integration-id'
    })
    
    wizard.on('flow:start', async flowId => {
        await appmixer.api.startFlow(flowId)
        wizard.close()
    })
    
    wizard.open()
    var appmixer = new Appmixer({ baseUrl: BASE_URL });
    appmixer.set('strings', STRINGS);
    appmixer.set('strings', {
        ui: {
            flowManager: {
                search: 'Search flows',
                header: {
                    buttonCreateFlow: 'Create new Flow'
                }
            }
        }
    });
    wget https://my.appmixer.com/appmixer/package/strings-en.json
    appmixer.set('strings', {
      time: {
        months: [...],
        monthsShort: [...],
        weekDaysShort: [...],
        ordinal(number){ ... },
        relativeTime: {...}
      }
    });
    ordinal(number) {
       const b = number % 10;
       const output = (~~(number % 100 / 10) === 1) ? 'th'
           : (b === 1) ? 'st'
               : (b === 2) ? 'nd'
                   : (b === 3) ? 'rd' : 'th';
       return number + output;
    }
    appmixer.set('strings', {
        ui: {
            flowManager: {
                pagination: '{{range}} of {{total}} flow|{{range}} of {{total}} flows'
            }
        }
    });
    kubectl create namespace $NAMESPACE
    kubectl create secret generic aws-ecr-credentials \
      --from-literal=AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY \
      --from-literal=AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY \
      --from-literal=AWS_DEFAULT_REGION=eu-central-1 \
      -n $NAMESPACE
    aws ecr get-login-password --region eu-central-1 | \
    helm registry login --username AWS --password-stdin 874193467787.dkr.ecr.eu-central-1.amazonaws.com
    helm pull oci://874193467787.dkr.ecr.eu-central-1.amazonaws.com/appmixer-helm
    helm pull oci://874193467787.dkr.ecr.eu-central-1.amazonaws.com/appmixer-helm --version <CHART_VERSION>
    helm pull oci://874193467787.dkr.ecr.eu-central-1.amazonaws.com/appmixer-helm --version <CHART_VERSION> --untar
    kubectl create -f https://download.elastic.co/downloads/eck/2.16.1/crds.yaml
    kubectl apply -f https://download.elastic.co/downloads/eck/2.16.1/operator.yaml
    helm repo update
    helm upgrade --install appmixer . --namespace $NAMESPACE
    kubectl get routes -n $NAMESPACE
    kubectl get ingress -n $NAMESPACE
    appVersion: "6.2.1"
    helm upgrade --install appmixer . -n $NAMESPACE
    {
        "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]}}}"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    insightsDashboard.state(name, value)
    insightsDashboard.on(event, handler)
    insightsDashboard.on('chart:clone', chartId => {/* ... */})
    insightsDashboard.on('chart:remove', chartId => {/* ... */})
    insightsDashboard.on('chart:open', chartId => {/* ... */})
    const insightsDashboard = appmixer.ui.InsightsDashboard({
        el: '#insights-dashboard'
    })
    
    insightsDashboard.open()
  • Logstash for log processing pipeline

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

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

    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.

    password*

    string

    Password.

    email*

    string

    Email address.

    username*

    string

    Username.

    system configuration
    {
        "user": {
            "id": "5c88c7cc04a917256c726c3d",
            "username":"[email protected]",
            "isActive": false,
            "email": "[email protected]", 
            "plan":"free"
        },
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    {
      "id": "58593f07c3ee4f239dc69ff7",
      "username": "[email protected]",
      "isActive": true,
      "email": "[email protected]",
      "scope": [
        "user"
      ],
      "plan": "beta"
    }
    [
        "routes",
        "components"
    ]

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    type

    string

    components | routes

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

    hashtag
    Update ACL rules

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    type

    string

    components | routes

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    type

    string

    components | routes

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    type

    string

    components | routes

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    type

    string

    components | routes

    resource

    string

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

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

    hashtag
    Get message

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

    Get a single message.

    hashtag
    Path Parameters

    Name
    Type
    Description

    messageId

    string

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

    Delete a message.

    hashtag
    Path Parameters

    Name
    Type
    Description

    messageId

    string

    hashtag
    Retry a message

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

    Put the message back into Appmixer engine.

    hashtag
    Path Parameters

    Name
    Type
    Description

    messageId

    string

    hashtag
    MCP Server Connector Components

    Every MCP server connector in Appmixer is composed of three distinct components:

    Component

    Purpose

    Where it’s used

    MCPServer

    Core component that provides the actual MCP server connection and tool definitions.

    Can be connected only to AI Agent component’s mcp port.

    ListTools

    Helper component that lists all available tools exposed by the MCP server.

    Used internally by MCPServer (not standalone).

    CallTool

    Helper component to invoke a specific tool from the MCP server.

    Used internally by MCPServer and can also be used standalone in regular flows to call a selected tool, similar to an action-type Appmixer component.

    Important:

    • The AI Agent component mcp port can connect only to MCPServer components.

    • ListTools and CallTool are helper components for the MCPServer, but CallTool has the added ability to be used independently in non-AI Agent flows.


    hashtag
    How to Implement a Custom MCP Server Connector

    To integrate your own MCP server, you will create an Appmixer connector that wraps your server code, following the structure of existing MCP server connectors in our public repository.

    hashtag
    Step 1 – Start from an Existing Example

    Use one of our open-source MCP server connectors as a template:

    • Repository: appmixer-connectors/src/appmixer/mcpserversarrow-up-right

    • Example: GitLab MCP Server Connectorarrow-up-right

    The complete list of files of our Gitlab MCP Server connector looks like this:

    Note: The GitLab example assumes that your MCP server uses the Stdio-style implementation of the MCP protocol. If your MCP server uses a different communication method, you may need to adjust the connector logic accordingly.

    hashtag
    Step 2 – Files to Modify

    From the example connector, most files are generic and can be reused. You only need to modify:

    1. lib.js

      • Points to the NPM package containing your MCP server. Your MCP server must be NodeJS based. If you do not have a public NPM package with your MCP server, you can also package the MCP server directory directly in the Appmixer connector. Just make sure to reflect this in the paths.

      • Replace GitLab-specific logic with your own (if you used a different existing MCP server as a foundation, replace that MCP's specific logic with your own).

      • Ensure your MCP server package is listed in package.json dependencies (Unless you package your MCP server directly in the Appmixer connector directory, see above.)

    2. auth.js

      • Define any required environment variables (API keys, credentials) for your MCP server.

      • These will be shown to the user in Appmixer Designer when they configure the connector.

    3. Manifest & Metadata Files

      • package.json

        • Name following convention: appmixer.mcpservers.<your_mcp_server_name>

    You generally do NOT need to edit:

    • MCPServer/MCPServer.js

    • ListTools/ListTools.js

    • CallTool/CallTool.js

    These files contain generic logic that works for all MCP servers.


    hashtag
    Packaging and Publishing

    Once you have updated the connector, follow the standard instructions to pack and publish your connector to your Appmixer tenant as described here.


    hashtag
    Summary Checklist

    Before publishing:

    • Updated package.json with correct name and NPM MCP server dependency.

    • Updated module.json, bundle.json with correct name, metadata and icons.

    • Edited lib.js to point to your MCP server code.

    • Configured auth.js with required authentication fields.

    • Updated component.json files for all three components.

    • Tested CallTool both as helper and standalone in a flow.

    • Connected MCPServer to an AI Agent mcp port and verified tool discovery.

    Docker Composearrow-up-right

  • NodeJSarrow-up-right v24.6.0 (only for Appmixer CLI)

  • hashtag
    Installation

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

    hashtag
    Install and Start Appmixer

    hashtag
    Stop Appmixer

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

    hashtag
    Stop and Clean Up

    Stop Appmixer and remove all containers and images:

    hashtag
    Using Webhook Components

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

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

    with the URL from ngrok:

    Now restart Appmixer:

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

    But this command will set the GRIDD_URL to https://568284c4.ngrok.io as well.

    circle-info

    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 OAuth redirect URL.

    hashtag
    Creating an Admin User

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

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

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

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

    circle-info

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

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

    From now on, my "[email protected]" user will be able to publish new custom components using the appmixer CLI tool.

    Dockerarrow-up-right

    Basic Structure

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

    Services, modules and components hierarchy

    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:

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

    hashtag
    Service Manifest File

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

    Available fields are:

    hashtag
    Module Manifest File

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

    Available fields are:

    Insights Logs

    Browse logs of messages that passed through flows.

    Insights Logs

    hashtag
    Configuration

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

    hashtag
    config.el ...

    circle-info

    Learn about widget config .

    hashtag
    config.flowId

    ID of a flow to filter the logs by.

    hashtag
    config.options.showHistogram

    Hides the histogram section of the UI when set to false.

    hashtag
    Instance

    circle-info

    Learn about widget instance .

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message. filterLayout

    Type: String | Default: 'expanded'

    Controls whether the left filter panel is shown. Options: 'collapsed' or 'expanded'. Example: Set the initial state and toggle it programmatically at runtime.

    query

    Type: Object | Default: {}

    The current query of the widget. Changing it reloads both the histogram data and the logs table. Example: Retrieving the current query from the widget's state.

    hashtag
    Basic Usage

    Set an initial query in the widget's state:

    hashtag
    Reading and Updating the Query with Nested Filters

    When modifying the query, always merge it with the current query object.

    hashtag
    Tracking Query Changes

    Listen for changes triggered by UI interactions:

    hashtag
    Working with Nested Query Keys

    You can directly read or set nested query properties using a /-separated path.

    hashtag
    Example

    Integrations

    Manage flows used as integration templates and instances.

    Integrations

    hashtag
    Configuration

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

    hashtag
    config.el ...

    circle-info

    Learn about widget config .

    hashtag
    config.options

    Type: Object | Default: {}

    hashtag
    config.options.customFilter

    Type: Object | Default: {}

    Filter the integrations with additional parameters:

    The customFilter option can also be an array for templates and instances:

    hashtag
    Updating the Custom Filter Dynamically

    You can change the custom filter after the widget has already been initialized. For example, when a user selects a new category, you can update the filter and reload the widget:

    hashtag
    Instance

    circle-info

    Learn about widget instance .

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    Events

    hashtag
    integration:create

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

    hashtag
    integration:edit

    Click a button to edit integration.

    hashtag
    integration:remove

    Click a button to remove integration.

    hashtag
    integration:start

    Click a button to start integration.

    hashtag
    integration:stop

    Click a button to stop integration.

    hashtag
    Example

    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.

    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.

    hashtag

    Files

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

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

    ['*', 'flows']
    ['*', 'read', '!read', 'create', '!create', 'update', '!update', 'delete', '!delete']
    {
            "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...
        }
    {}
    {}
    mcpservers/modelcontextprotocol_server_gitlab/
    ├── CallTool
    │   ├── CallTool.js
    │   └── component.json
    ├── ListTools
    │   ├── ListTools.js
    │   └── component.json
    ├── MCPServer
    │   ├── MCPServer.js
    │   └── component.json
    ├── auth.js
    ├── bundle.json
    ├── lib.js
    ├── module.json
    └── package.json
    appmixer pack appmixer/mcpservers/your_mcp_server
    appmixer publish appmixer.mcpservers.your_mcp_server.zip
    docker-compose --project-name appmixer up
    docker-compose --project-name appmixer stop
    docker-compose --project-name appmixer down --volumes --remove-orphans --rmi all
    $ ngrok http 2200
    ngrok by @inconshreveable                                                                                                                                                                                                                     (Ctrl+C to quit)
    
    Session Status                online
    Account                        (Plan: Basic)
    Version                       2.3.35
    Region                        United States (us)
    Web Interface                 http://127.0.0.1:4040
    Forwarding                    http://568284c4.ngrok.io -> http://localhost:2200
    Forwarding                    https://568284c4.ngrok.io -> http://localhost:2200
    
    Connections                   ttl     opn     rt1     rt5     p50     p90
                                  0       0       0.00    0.00    0.00    0.00
      engine:
        ...
        environment:
          - APPMIXER_API_URL=${APPMIXER_API_URL:-http://localhost:2200}
        ...
    engine:
        ...
        environment:
          - APPMIXER_API_URL=https://568284c4.ngrok.io
        ...
    docker-compose --project-name appmixer down # or kill existing with Ctrl-c
    docker-compose --project-name appmixer up
    APPMIXER_API_URL=https://568284c4.ngrok.io docker-compose --project-name appmixer up
    docker-compose -p appmixer exec mongodb mongo appmixer --quiet --eval 'db.users.update({"email": "[email protected]"}, { $set: {"scope": ["user", "admin"]}})'
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    const insightsLogs = appmixer.ui.InsightsLogs(config)
    
    insightsLogs.set(key, value)
    insightsLogs.get(key)
    const integrations = appmixer.ui.Integrations(config)
    
    integrations.set(key, value)
    integrations.get(key)

    Add your public MCP server NPM package under dependencies.

  • module.json, bundle.json

    • Update name (use the same one as above, i.e. in the package.json file), label, description, and icons.

  • CallTool/component.json, ListTools/component.json, MCPServer/component.json

    • Update the name to follow the convention above (e.g., appmixer.mcpservers.your_mcp.CallTool).

    • Update auth.service section to match your connector name (e.g., appmixer:mcpservers:your_mcp).

    • Replace all occurrences of the GitLab connector path with your connector path.

    • Set your custom icon.

  • },
    {
    "role": "tester",
    "resource": "*",
    "action": [
    "*"
    ],
    "attributes": [
    "non-private"
    ]
    }
    ]
    "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...
    }
    ]

    Description of your app.

    icon

    App icon in the Data URI format.

    Description of your app.

    icon

    App icon in the Data URI format.

    Field

    Description

    name

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

    label

    The label of your app.

    category

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

    categoryIndex

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

    index

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

    Field

    Description

    name

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

    label

    The label of your app.

    category

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

    categoryIndex

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

    index

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

    Google modules
    Twilio service
    Modules as separate apps.
    A single app type of service.
    Service manifest fields meaning.

    description

    description

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

    hashtag
    Install Appmixer CLI

    First, you need to install the Appmixer CLI 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 associated with a "vendor." 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.

    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.

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

    hashtag
    Define Input and Output

    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 Bored API endpointarrow-up-right.

    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:

    The schema.properties section defines a JSON Schemaarrow-up-right for our input while the inspector.inputs section defines the form to collect input from the user.

    Type input
    Output of GetActivity consumed in the connected SendEmail component

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

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

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

    Appmixer Github repositoryarrow-up-right
    hashtag
    Path Parameters
    Name
    Type
    Description

    fileId

    string

    The UUID of the required file.

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

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

    hashtag
    Headers

    Name
    Type
    Description

    Content-type

    string

    Must be set to multipart/form-data

    uploader-file-id

    string

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

    uploader-file-name

    string

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

    uploader-chunk-size

    string

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

    hashtag
    Request Body

    Name
    Type
    Description

    file

    string

    The file/chunk to be uploaded

    hashtag
    Get user files

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

    Example: https://api.appmixer.com/files?limit=10&filter=filename:~invoicearrow-up-right&sort=filename:1

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    limit

    number

    offset

    number

    sort

    String

    projection

    String

    hashtag
    Get number of files

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

    Used for paging.

    hashtag
    Query Parameters

    Name
    Type
    Description

    includeComponentSourceFiles

    Boolean

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

    filter

    String

    hashtag
    Remove file.

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

    hashtag
    Delete all user files.

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

    Example: DELETE https://api.appmixer.com/files?filter=filename:~invoicearrow-up-right will delete all user's files with 'invoice' in the name.

    hashtag
    Query Parameters

    Name
    Type
    Description

    filter

    String

    Can be used to filter files.

    here
    here
    here
    here

    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:

    Variables Picker

    An example of an outPorts definition can look like this:

    hashtag
    JSON Schema

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

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

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

    You will see the item properties among other variables.

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

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

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

    hashtag
    outPort.source

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

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

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

    Here is an example of the UpdatedRow output port definition.

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

    Charts

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

    hashtag
    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

    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.

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

    {
        "name": "[vendor].[service]",
        "label": "My App Label",
        "category": "applications",
        "categoryIndex": 2,
        "index": 1,
        "description": "My App Description",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
    }    
    {
        "name": "[vendor].[service].[module]",
        "label": "My App Label",
        "category": "applications",
        "categoryIndex": 2,
        "index": 3,
        "description": "My App Description",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
    }    
    $ npm install -g appmixer
    $ appmixer url https://api.[YOURTENANT].appmixer.cloud
    $ appmixer login [email protected]
    Password:
    $ appmixer init component appmixer.boredapi.core.GetActivity \
    --description "Get a random activity to do when I am bored." \
    --author "Appmixer Team <[email protected]>" \
    --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 <[email protected]>",
        "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 <[email protected]>",
        "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 = 'http://www.boredapi.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
    {
      "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"
    }
    [
        {
            "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"
        }
    ]
    {
        count: 2
    }
    {
        // Response
    }
    {
        // Response
    }
    // Initialize with histogram hidden
    const insightsLogs = appmixer.ui.InsightsLogs({
      options: { showHistogram: false }
    });
    
    // Later: show histogram dynamically
    insightsLogs.set('options', {
      ...insightsLogs.get('options'), // keep other options unchanged
      showHistogram: true
    });
    insightsLogs.state(name, value)
    const insightsLogs = appmixer.ui.InsightsLogs({
        state: { filterLayout: 'collapsed' }
    });
    
    insightsLogs.state('filterLayout', 'expanded');
    // Initial query: last 30 days
    const insightsLogs = appmixer.ui.InsightsLogs({
      state: {
        query: {
          query: {
            range: {
              from: {
                endOf: null,
                startOf: 'day',
                subtract: [30, 'day']
              }
            }
          }
        }
      }
    });
    // Get the current query
    const currentQuery = insightsLogs.state('query');
    
    // Merge in new filter values
    insightsLogs.state('query', {
      ...currentQuery,             // keep existing top-level query state
      query: {
        ...currentQuery.query,     // keep existing nested filters
        targets: {
          // Keys are Flow IDs
          // Values are optional arrays of component IDs
          // (empty array = all components in that flow)
          '8a47ab76-b90c...': ['component-1', 'component-2'],
          'f0e1d2c3-b90c...': []   // no component filter
        }
      }
    });
    insightsLogs.on('change:query', queryAfterUserInput => {
      console.log(queryAfterUserInput);
    });
    const currentFlowType = widget.state('query/flowType');
    
    // flowType filter
    widget.state('query/flowType', 'automation');      // single value
    widget.state('query/flowType', [
      'integration-test',
      'integration-instance',
      'automation'
    ]);                                                 // multiple values
    
    // userId filter
    widget.state('query/userId', 'A');                  // single value
    widget.state('query/userId', ['A', 'B']);           // multiple values
    const insightsLogs = appmixer.ui.InsightsLogs({
        el: '#insights-logs'
    })
    
    insightsLogs.open()
    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',
        }
      }
    }
    appmixer.ui.Integrations({
        /* ... */
        options: {
            customFilter: [
                { 'customFields.category': 'your-category-for-templates' },
                { 'customFields.category': 'your-category-for-instances' }
            ]
        }
    });
    // Get the current widget options.
    const currentOptions = integrations.get('options') || {};
    
    // Override the custom filter and keep the rest of the options unchanged.
    integrations.set('options', {
        ...currentOptions,
        customFilter: {
            'customFields.category': 'finance'
        }
    });
    
    // Reload the widget to apply the updated filter.
    integrations.reload();
    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()
    {
        "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" }
                ]
            }
        ]
    }

    uploader-chunk-number

    string

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

    uploader-chunks-total

    string

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

    includeComponentSourceFiles

    Boolean

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

    filter

    String

    SDK component should be used to build Charts.

    hashtag
    Request Body

    Name
    Type
    Description

    traces

    object

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

    query

    object

    Object representing time range for the chart.

    options

    object

    Object with the visualization options for the chart.

    index

    number

    The position of the chart in the dashboard.

    {
        chartId: '5defb3901f17d98d974fbb00'
    }

    hashtag
    Update Chart

    PUT https://api.YOUR_TENANT.appmixer.cloud/charts/:chartId

    The same properties as in Create Chart API endpoint.

    hashtag
    Path Parameters

    Name
    Type
    Description

    string

    hashtag
    Get Charts

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

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

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

    hashtag
    Get One Chart

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id

    string

    ID of the chart to return.

    hashtag
    Delete a Chart

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id

    string

    ID of a chart.

    {
        "categories": {
            "object": {
                "label": "Object",
                "index": 1
            },
            "list": {
                "label": "List",
                "index": 2
            },
            ...
        },
        "modifiers": {
            "g_stringify": {
                "name": "stringify",
                "label": "Stringify",
                "category": [
                    "object",
                    "list"
                ],
    

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

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

    hashtag
    Delete Modifiers

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

    Delete all modifiers. Restricted to admin users only.

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

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

    JSON Schemaarrow-up-right
    this section
    properties
    Dynamic output port options.

    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

    hashtag
    Component's manifest localization object

    You can include custom strings inside the component's manifest using a localization object. The following is an example how to do it:

    As you can see, there's a localization object at the end whose keys are language codes. This allows you to support multiple languages. Each value is an object whose keys are paths to the elements that will be customized (label, tooltip, placeholder, etc). The paths follow syntax.

    You can customize the component's label and description. You can customize the component's input/output port labels, the inspector input field labels, and output variables as well.

    There's a slightly different specification when localizing output variables. As you can see in the example, after outPorts[0].options the next path fragment is the option's value, instead of the index. This is because the component could have a dynamic output instead and different output variables can share the same index, so we use the value to specify them instead.

    To switch the language in UI, you call the Appmixer instance set method:

    Note that if you want to customize the whole UI, you must use this in conjunction with the strings object. Here's an example:

    hashtag
    Strings object's component namespace

    The alternative way to customize the component's strings is using the Strings Object. There is a root namespace components which contains all the custom strings definitions for components:

    Each key in components object is the path to the component and the value is an object whose keys are the paths to elements (label, tooltip, placeholder, etc). This path follows the syntax. For more information about the Strings Object refer to the section.

    hashtag
    service.json and module.json localization

    Not only you can localize component's strings, but also services and modules. This allows you to change the label and description of the applications in the designer's left-side panel (the one you drag the applications from). To do it we can use either localization object in the service.json or module.json manifest or use the .

    hashtag
    Example using localization object in service.json

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

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

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

    inPorts

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

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

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

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

    hashtag
    inPort.schema

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

    hashtag
    inPort.inspector

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

    hashtag
    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

    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:

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

    hashtag
    Optional query parameters for the source call

    hashtag
    ignoreAuth=true

    The target component being called by the source construct may require authentication. With this option, you can invoke it without requiring authentication—in that case, context.auth will be an empty object.

    hashtag
    silentAuth=true

    The target component being called by the source construct may require authentication. However, there are cases where you might want to call the component before the user is authenticated. In such scenarios, the component can return different results depending on whether the user is authenticated or not. If the authentication is now available, the context.auth and context.profileInfo will be an empty object.

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

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

    Data Stores

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

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

    User

    API for users

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

    {
        chartId: "5defa30cbd1ca06288202346"
        index: 1
        mtime: "2019-12-10T13:52:12.288Z"
        name: "Updated Chart"
        options: {,…}
        query: {,…}
        traces: {,…}
        type: "bar"
        userId: "5dee76c19462fe6b3fd42d79"   
    }
    [
        {
            "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"
        }
    ]
    {
            "chartId": "5defa30cbd1ca06288202346",
            "userId": "5dee76c19462fe6b3fd42d79",
            "name": Chart 1",
            "index": 0,
            "type": "bar",
            "query": { ... },
            "options": { ... },
            "traces": { ... },
            "mtime": "2019-12-10T13:52:12.288Z"
    }
    {
        "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":[]
                    }
                }
            }
        }
    }
    {
        "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            }"
            },
            ...
        }
        
    }
    {}
    4
    {
        "statusCode": 400,
        "error": "Bad Request",
        "message": "The function returned \"undefined\""
    }
    {
        "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"
                        }
                    }
                }
            }
        ]
    }
    
    {
        "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
                        }
                    }
                }
            }
        ]
    }
    "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 }"
    },
    ...
    }
    }

    type

    string

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

    name

    string

    Name of the chart.

    projection

    string

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

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id

    string

    Store ID.

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    storeId

    string

    Store ID.

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

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

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

    hashtag
    Request Body

    Name
    Type
    Description

    name

    string

    Name of the store.

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id

    string

    Store ID.

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id

    string

    Store ID.

    hashtag
    Request Body

    Name
    Type
    Description

    name

    string

    New name of the store.

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    key

    string

    Key under which the posted value will be stored.

    id

    string

    Store ID.

    hashtag
    Request Body

    Name
    Type
    Description

    string

    Value to store under the key.

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    id*

    String

    Store ID

    key*

    String

    Key under which the updates are required

    hashtag
    Request Body

    Name
    Type
    Description

    key

    String

    New key

    value

    String

    New Value

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

    hashtag
    Request Body

    Name
    Type
    Description

    items

    array

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

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    format

    String

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

    {
        "count": 681
    }
    [{
        "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"
    }]
    {
        "storeId": "5c7f9bfe51dbaf0007f08db0"
    }
    {
        "oldName":"My Old Store Name",
        "storeId":"5c7f9bfe51dbaf0007f08db0"
    }
    {
        "key":"mykey",
        "value":"myvalue",
        "createdAt":"2019-03-06T10:17:58.796Z",
        "updatedAt":"2019-03-06T10:17:58.796Z"
    }
    {
        "key": "New Key",
        "value": "New Value"
        "createdAt": "2021-09-01T11:34:00.258+0000",
        "updatedAt": "2021-09-01T11:34:00.258+0000",
    }
    {
        "deletedCount": 1
    }
    {
        // Response
    }
    hashtag
    Request Body
    Name
    Type
    Description

    password*

    string

    Password.

    username*

    string

    Username, has to have an email format.

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

    hashtag
    Create User

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

    Create user. By default, this endpoint is open (does not require authentication). This can be changed by setting the API_USER_CREATE_SCOPE system configuration. 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": "[email protected]", "email": "[email protected]", "password": "abc321" }'

    hashtag
    Request Body

    Name
    Type
    Description

    password*

    string

    Password.

    email

    string

    Email address.

    username

    string

    Email address.

    metadata

    object

    Optional metadata.

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

    hashtag
    Get User Information

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

    Admin token required.

    hashtag
    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 whose usernames include a pattern:

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

    hashtag
    Get the number of users

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

    Admin token required

    hashtag
    Update user

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

    Admin token required.

    hashtag
    Request Body

    Name
    Type
    Description

    scope

    Array

    Array of scopes.

    vendor

    String|Array

    One or more vendors.

    metadata

    Object

    Optional metadata.

    email

    String

    Email address.

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

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

    hashtag
    Change user password

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

    User token required.

    hashtag
    Request Body

    Name
    Type
    Description

    oldPassword*

    String

    Old password

    newPassword*

    String

    New password

    hashtag
    Reset user password

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

    Admin token required.

    hashtag
    Request Body

    Name
    Type
    Description

    email*

    String

    User email address

    password*

    String

    New password

    hashtag
    Forgot Password

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

    See the Forgot Password Service configuration for more details.

    hashtag
    Request Body

    Name
    Type
    Description

    email*

    String

    Email address

    hashtag
    Reset forgotten password

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

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

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

    JSON patharrow-up-right
    JSON patharrow-up-right
    Custom Strings
    Strings Object
    Properties Schema
    Properties Inspector
    Input Port Configuration using Variables

    Apps

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

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    hashtag
    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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    Flow Manager

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

    hashtag
    Configuration

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

    Designer

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

    hashtag
    Configuration

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

    API Module

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

    Name
    Description

    Additional Configuration

    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 options below can be set via the environment variables on the instances of the Appmixer engine.

    hashtag
    API and Worker nodes

    Appmixer 6.2 introduces the ability to easily start a pod as either an API node or a worker node. If you deployed Appmixer using the Helm Chart, no action is required—the chart already supports separate API and worker nodes.

    Custom Theme

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

    hashtag
    Basic usage

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

    and/or use the option with individual widgets:

    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    {
      "id": "58593f07c3ee4f239dc69ff7",
      "username": "[email protected]",
      "isActive": true,
      "email": "[email protected]",
      "scope": [
        "user"
      ],
      "metadata": {},
      "plan": "beta"
    }
    {
        "ticket": "830639e3-c53a-42d6-ad43-0276674236b4"
    }
    {
        "status": "in-progress | completed | failed | cancelled",
        "stepsDone": 4,       // can be used to display a progress bar
        "stepsTotal": 10      // can be used to display a progress bar
    }
    [
        {
            "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...
        }
    ]
    {}
    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <[email protected]>",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVp...",
        "description": "Send SMS text message through Twilio.",
        "private": false,
        "auth": {
            "service": "appmixer:twilio"
        },
        "outPorts": [
            {
                "name": "sent",
                "options": [
                    { "label": "Message Sid", "value": "sid" }
                ]
            }
        ],
        "inPorts": [
            {
                "name": "message",
                "schema": {
                    "type": "object",
                    "properties": {
                        "body": { "type": "string" },
                        "to": { "type": "string" },
                        "from": { "type": "string" }
                    },
                    "required": [
                        "from", "to"
                    ]
                },
                "inspector": {
                    "inputs": {
                        "body": {
                            "type": "text",
                            "label": "Text message",
                            "tooltip": "Text message that should be sent.",
                            "index": 1
                        },
                        "from": {
                            "type": "select",
                            "label": "From number",
                            "placeholder": "Type number",
                            "tooltip": "Select Twilio phone number.",
                            "index": 2,
                            "source": {
                                "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                                "data": {
                                    "transform": "./transformers#fromNumbersToSelectArray"
                                }
                            }
                        },
                        "to": {
                            "type": "text",
                            "label": "To number",
                            "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                            "index": 3
                        }
                    }
                }
            }
       ],
       "localization": {
           "cs": {
               "label": "Pošli SMS",
               "description": "Pošli SMS pomocí Twilia",
               "inPorts[0].name": "Zpráva",
               "inPorts[0].inspector.inputs.body.label": "Textová zpráva",
               "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
               "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo",
               "outPorts[0].name": "Odesláno",
               "outPorts[0].options[sid].label": "Sid zprávy"
           },
           "sk": {
               "label": "Pošli SMS",
               "description": "Pošli SMS pomocou Twilia",
               "inPorts[0].name": "Správa",
               "inPorts[0].inspector.inputs.body.label": "Textová správa",
               "inPorts[0].inspector.inputs.from.label": "číslo volajúceho",
               "outPorts[0].name": "Odoslané",
               "outPorts[0].options[sid].label": "Sid správy"
           }
       }
    }
    // Create an SDK instance
    var appmixer = new Appmixer()
    
    // Will use the strings under 'cs' key
    appmixer.set('lang', 'cs')
    
    // Will switch the strings to the ones under 'sk' key
    appmixer.set('lang', 'sk')
    var appmixer = new Appmixer();
    
    var mySkStrings = { /* Strings definition for sk language */ };
    var myCsStrings = { /* Strings definition for cs language */ };
    
    // This function will be called when the user clicks on some
    // "Switch to sk" button
    function setLangToSk() {
        appmixer.set('lang', 'sk');
        appmixer.set('strings', mySkStrings);
    }
    
    // This function will be called when the user clicks on some
    // "Switch to cs" button
    function setLangToCs() {
        appmixer.set('lang', 'cs');
        appmixer.set('strings', myCsStrings);
    }
    {
        "components": {
    	"appmixer.twilio.sms.SendSMS": {
    	    "inPorts[0].inspector.inputs.body.label": "Textová zpráva",
        	    "inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
                "inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo"
            }
        }
        // Other namespaces (designer, storage, accounts...)
    }
    {
        "name": "appmixer.twilio",
        "label": "Twilio",
        "category": "applications",
        "description": "Twilio is an easy tool for developers to send and receive SMS and voice calls.",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMj...",
        "localization": {
            "cz": {
                "label": "Modul Twilio",
                "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
            },
            "sk": {
                "label": "Modul Twilio",
                "description": "Twilio je ľahký nástroj pre vývojárov na odosielanie a prijímanie SMS a hlasových hovorov."
            }
        }
    }
    "appmixer.twilio": {
        "label": "Modul Twilio",
        "description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
    }
    appmixer.set('strings', {
        ui: {
            designer: {
                stencil: {
                    groups: {
                        applications: 'Connectors',
                        utilities: 'Tools'
                    }
                }
            }
        }
    });
    City: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[name]}}}
    Humidity: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.humidity]}}}
    Pressure: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.pressure]}}}
    Temperature: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.temp]}}}
    {
    ...
        "inPorts": [
            {
                "name": "in",
                "schema": {
                    "type": "object"
                },
                "source": {
                    // The ListColumns component can return an array of columns in a
                    // Worksheet.
                    "url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
                    "data": {
                        // The ListColumns component needs two properties in order
                        // to get the list of columns, the Spreasheet Id and the
                        // Worksheet Id. Both will be taken from properties of the
                        // CreateRow component (the caller).
                        "properties": {
                            // Appmixer will replace 'properties/sheetId' with
                            // the actual value before making the call
                            "sheetId": "properties/sheetId",
                            "worksheetId": "properties/worksheetId"
                        },
                        // A transformer function 'columnsToInspector' from the 
                        // ListColumns.js will be executed in order to transform a list
                        // of columns to the Appmixer Inspector.
                        "transform": "./ListColumns#columnsToInspector"
                    }
                }
            }
        ]
    }
    "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."
            },
            ...
        }
    }
    ...
    "inspector": {
        "inputs": {
            "inputWithoutAuth": {
                "type": "select",
                "label": "Input without Auth",
                "index": 2,
                "source": {
                    "url": "/component/appmixer/test/staticAuth/StaticAuth?outPort=out&ignoreAuth=true",
                    "data": {
                        "transform": "./StaticAuth#getOutputOptions"
                    }
                }
            }
        }
    }
    ...
    ...
    "inspector": {
        "inputs": {
            "inputWithSilentAuth": {
                "type": "select",
                "label": "Input with Silent Auth",
                "index": 1,
                "source": {
                    "url": "/component/appmixer/test/staticAuth/StaticAuth?outPort=out&silentAuth=true",
                    "data": {
                        "transform": "./StaticAuth#getOutputOptions"
                    }
                }
            }
        }
    }
    ...
    {
        "variablesPipeline": {
            "scopeDepth": 1,
            "rawValue": true
        }
    }
    "plan":"free",
    "metadata": {}
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }

    app

    string

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

    manifest

    string

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

    ticket

    string

    Ticket that you got from the POST /component request.

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

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

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

    hashtag
    api.authenticateWithEmailAndPassword

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

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

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

    hashtag
    api.deleteFlow

    Delete an existing flow identified by flowId.

    hashtag
    api.getFlow

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

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

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

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

    hashtag
    api.startFlow

    Start a flow.

    hashtag
    api.stopFlow

    Stop a flow.

    hashtag
    api.cloneFlow

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

    hashtag
    api.getUser

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

    hashtag
    api.getStores

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

    hashtag
    api.getStore

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

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

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

    hashtag
    api.createStore

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

    hashtag
    api.deleteStore

    Delete a store.

    hashtag
    api.renameStore

    Rename an existing store.

    hashtag
    api.createStoreItem

    Create a new record in a store.

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

    hashtag
    api.createAccount

    Create a custom account.

    hashtag
    api.getAccounts

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

    hashtag
    api.getComponentAccounts

    Get a list of accounts connected to a specific component.

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

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

    hashtag
    api.getLogs

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

    Get logs of a specific flow:

    hashtag
    api.getLog

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

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

    hashtag
    api.getPeopleTasksCount

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

    hashtag
    api.getPeopleTask

    Return one task identified by id.

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

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

    hashtag
    api.getCharts

    Returns all the Insights charts of the user.

    hashtag
    api.getChart

    Return one Insights chart identified by chartId.

    hashtag
    api.deleteChart

    Delete an Insights chart identified by chartId.

    hashtag
    api.getFlowAuthentication

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

    hashtag
    Events

    hashtag
    error

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

    hashtag
    warning

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

    api.set(name, value)

    Set configuration property.

    api.get(name)

    Get configuration property.

    api.on(event, handler)

    Add event listener.

    api.off(event, handler)

    Remove event listener.

    [
        {
            "name": "appmixer.twilio.sms.SendSMS",
            "author": "David Durman <[email protected]>",
            "icon": "data:image/png;base64,iVBORw...gg==",
            "description": "Send SMS text message through Twilio.",
            "auth": { "service": "appmixer:twilio" },
            "inPorts": [
                {
                    "name": "message",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "body": { "type": "string" },
                            "to": { "type": "string" }
                        },
                        "required": [ "to" ]
                    },
                    "inspector": {
                        "inputs": {
                            "body": {
                                "type": "text",
                                "label": "Text message",
                                "tooltip": "Text message that should be sent.",
                                "index": 1
                            },
                            "to": {
                                "type": "text",
                                "label": "To number",
                                "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                                "index": 2
                            }
                        }
                    }
                }
            ],
            "properties": {
                "schema": {
                    "properties": {
                        "fromNumber": { "type": "string" }
                    },
                    "required": [ "fromNumber" ]
                },
                "inspector": {
                    "inputs": {
                        "fromNumber": {
                            "type": "select",
                            "label": "From number",
                            "tooltip": "Select Twilio phone number.",
                            "index": 1,
                            "source": {
                                "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                                "data": {
                                    "transform": "./transformers#fromNumbersToSelectArray"
                                }
                            }
                        }
                    }
                }
            }
        },
        {
            "name": "appmixer.twilio.calls.NewCall",
            "author": "David Durman <[email protected]>",
            "icon": "data:image/png;base64,iVBORw...gg==",
            "description": "Receive a call through Twilio.",
            "auth": { "service": "appmixer:twilio" },
            "webhook": true,
            "webhookAsync": true,
            "outPorts": [
                {
                    "name": "call",
                    "options": []
                }
            ],
            "properties": {
                "schema": {
                    "properties": {
                        "generateInspector": { "type": "boolean" },
                        "url": {}
                    }
                },
                "inspector": {
                    "inputs": {
                        "url": {
                            "source": {
                                "url": "/component/appmixer/twilio/calls/NewCall?outPort=call",
                                "data": {
                                    "properties": {
                                        "generateInspector": true
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    ]
    [
        "appmixer.asana.projects.CreateProject",
        "appmixer.asana.projects.NewProject",
        "appmixer.asana.tasks.CreateStory",
        "appmixer.calendly.events.InviteeCanceled",
        "appmixer.calendly.events.InviteeCreated",
        "appmixer.clearbit.enrichment.FindCompany",
        "appmixer.clearbit.enrichment.FindPerson"
    ]
    {
        "ticket":"a194d145-3768-4a8a-84a4-4f1e4e08c4ad"
    }
    // 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'"
        }
      ]
    }
    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 => { ... }
    methods:

    hashtag
    config.el ...

    circle-info

    Learn about widget config here.

    hashtag
    config.options

    Type: Object | Default: DefaultOptions

    hashtag
    config.options.menu

    Type: Object[] | Default: []

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

    circle-info

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

    Flow Manager Menu

    hashtag
    config.options.shareTypes

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    hashtag
    config.options.sharePermissions

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    hashtag
    config.options.filters

    Type: Object[] | Default: []

    Create dropdown inputs with built-in query filters:

    Flow Manager Filters

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

    hashtag
    config.options.sorting

    Type: Object[] | Default: []

    Create dropdown inputs with built-in sorting:

    Flow Manager Sorting

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    hashtag
    layout

    Type: String | Default: grid

    Change layout of the widget.

    hashtag
    query

    Type: Object | Default: DefaultQuery

    Set custom query parameters.

    hashtag
    Events

    hashtag
    flow:open

    Select a flow to open in Designer widget.

    hashtag
    flow:create

    Click Create Flow button.

    hashtag
    flow:start

    Toggle flow stage button.

    hashtag
    flow:stop

    Toggle flow stage button.

    hashtag
    flow:clone

    Click menu item to clone a flow.

    hashtag
    flow:share

    Click menu item to open sharing of a flow.

    hashtag
    flow:rename

    Click menu item to rename flow.

    hashtag
    flow:remove

    Click menu item to remove a flow.

    hashtag
    Sharing

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

    Flow Manager Sharing

    hashtag
    Example

    Flow Manager
    hashtag
    config.el ...
    circle-info

    Learn about widget config here.

    hashtag
    config.flowId

    Type: String | Default: null

    ID of a flow that is opened in the editor.

    hashtag
    config.componentId

    Type: String | Default: null

    ID of a component that is opened in the editor.

    hashtag
    config.shareTypes

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    hashtag
    config.sharePermissions

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    hashtag
    config.options.showHeader

    Type: Boolean | Default: true

    Toggle visibility of the header. config.options.validation

    Type: Object | Default: {}

    Controls validation panel settings. Properties:

    • show (Boolean, default: false). Toggles visibility of the validation panel.

    Example:

    circle-info

    Additional validation options may be added in future versions.

    hashtag
    config.options.menu

    Type: Object[] | Default: []

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

    circle-info

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

    hashtag
    config.options.toolbar

    Type: Array[] | Default: []

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

    circle-info

    Specify Vue ComponentOptionsarrow-up-right under widget to create a custom toolbar button.

    hashtag
    config.options.autoOpenLogs

    Type: Boolean | Default: true

    Automatically open logs view when the flow is running.

    hashtag
    config.options.triggerSelector

    Type: Object | Default: null

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

    hashtag
    Instance

    circle-info

    Learn about widget instance here.

    hashtag
    State

    hashtag
    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    hashtag
    error

    Type: String | Default: null

    Toggle a custom error message.

    stencilLayout

    Type: String | Default: 'default'

    Sets the stencil panel layout to 'default' (expanded) or 'collapsed'.

    validationLayout

    Type: String | Default: 'default'

    Sets the validation panel layout to 'default' (expanded) or 'collapsed'.

    hashtag
    Events

    hashtag
    flow:start

    Toggle stage button to start the flow.

    hashtag
    flow:stop

    Toggle stage button to stop the flow.

    hashtag
    flow:share

    Click menu item to open sharing of the flow.

    hashtag
    flow:rename

    Click menu item to rename the flow.

    hashtag
    flow:export-svg

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

    hashtag
    flow:export-png

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

    hashtag
    flow:print

    Click menu item to print diagram of the flow.

    hashtag
    flow:validation

    An event containing an array with flow validation errors. If the array is empty, there are no validation errors in the flow.

    hashtag
    flow:wizard-builder

    Click menu item to open a wizard builder dialog.

    hashtag
    component:add

    Add a new component to the flow.

    hashtag
    component:open

    Open component inspector.

    hashtag
    component:close

    Close component inspector.

    hashtag
    component:rename

    Rename a component.

    hashtag
    component:update-type

    Use selection input to change component type.

    hashtag
    navigate:validation

    Click a button to show validation errors.

    hashtag
    Example

    Designer
    hashtag
    Running an API node only

    To run a node as an API node only, pass the --no-worker argument in your deployment configuration (engine.yaml):

    hashtag
    Running a worker node only

    To run a node as a worker only, do not use the --http argument. The pod must be started without any arguments.

    You will need to create a separate deployment configuration for the worker node. This can be a copy of engine.yaml with the arguments removed:

    hashtag
    Logging

    hashtag
    LOG_LEVEL

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

    hashtag
    LOG_COMPONENT_DATA_MESSAGE

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

    circle-info

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

    hashtag
    APPMIXER_HTTPS_PROXY and APPMIXER_HTTP_PROXY

    Configure an HTTP proxy. All HTTP(S) requests from Appmixer will be redirected to the proxy URL.

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

    triangle-exclamation

    If you lose the encryption secret, you will not be able to recover the encrypted tokens.

    hashtag
    MinIO/S3

    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 /files API) in the MinIO/S3 server.

    circle-exclamation

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

    If you want to use AWS S3, use the following permissions:

    hashtag
    Forgot Password Service

    Appmixer provides an API to reset forgotten passwords. This works together with the Appmixer Studio interface (not the Appmixer SDK).

    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:

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

    You can use Appmixer to create a simple flow, that would send emails with the reset password link.

    hashtag
    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

    The default email body:

    circle-info

    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.

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

    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.

    QUOTA_CONTEXT_SLOW_QUEUE default value, you can copy&paste this to the Backoffice and modify.

    hashtag
    Garbage Collectors

    hashtag
    Files

    The garbage collector for files can be turned off with GC_FILES_ENABLED (set to false). The behavior can be controlled by a set of rules GC_FILES_RULES.

    This is the default set:

    The value of GC_FILES_RULES is a stringified array of rules. There can be a different rule for each scope of users. Each rule has to have exactly 3 properties - scope, ttl and hardLimit.

    Example:

    In this example, the admin users can store files up to 90GB and their files will be deleted after 1440 hours (60 days).

    When the users exceed the hardLimit, they he will not be able to create more files. To get unblocked, they must delete some of their files and get under the limit.

    circle-info

    The garbage collector does not remove files uploaded via the Files API, or files marked as archived.

    System Configuration
    hashtag
    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:

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

    hashtag
    Shapes

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

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

    action

    action-vertical

    action-dark

    action-vertical-dark

    trigger

    trigger-vertical

    trigger-dark

    trigger-vertical-dark

    selection

    selection-vertical

    selection-dark

    selection-vertical-dark

    hashtag
    action/trigger

    hashtag
    action-vertical/trigger-vertical

    hashtag
    action-dark/trigger-dark

    Special version of actions and triggers that works better on dark backgrounds.

    hashtag
    action-vertical-dark/trigger-vertical-dark

    Special version of vertical actions and triggers that works better on dark backgrounds.

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

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

    circle-info

    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 theme.variables instead.

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

    hashtag
    Font

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

    hashtag
    Complete Theme Object

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

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

    FlowManager Dark Theme
    Designer Dark Theme
    Insights Logs Dark Theme
    Insights Chart Editor

    Insights

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

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

    hashtag
    Query Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

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

    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()
    const designer = appmixer.ui.Designer(config)
    
    designer.set(key, value)
    designer.get(key)
    appmixer.ui.Designer({
      /* ... */
      options: {
        validation: { show: true }
      }
    })
    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()
    ...
        spec:
          containers:
            - args:
                - -c
                - node gridd.js --http --no-worker       ## API node only
              command:
                - /bin/sh
    ...            
    ...
        spec:
          containers:
            - args:
                - -c
                - node gridd.js         ## Worker node only
              command:
                - /bin/sh
    ...            
    # 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
    # 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  
    {
        "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"
                ]
            }
        ]
    }
    {
      "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'
    }
    <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>
    // 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"
        }
    ]
    [{"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"}]
    [{"limit":100000,"scope":"user","windowInSeconds":60,"name":"slowQueue:60s"},{"limit":500000
    [
        {
            scope: 'user',
    
            /**
             * The GC will never delete files created in the past fileTTL (in minutes). By default,
             * set to 720 hours. The reason is to prevent the GC from removing files that could be needed by the
             * flows.
             */
            ttl: 720,   // 30 days
    
            /**
             * The user will be blocked to save any files if the limit is reached.
             */
            hardLimit: 1000 * 1000 * 1000 * 2   // 2 GB in bytes
        }
    ]
    [{"scope":"user","ttl":720,"hardLimit":2000000000},{"scope":"admin","ttl":1440,"hardLimit":90000000000}]
    const appmixer = new Appmixer({
        theme: {
            variables: {
                font: {
                    family: "sans-serif"
                },
                colors: {
                    neutral: "orange"
                }
            }
        }
    });
    const flowManager = appmixer.ui.FlowManager({
        el: "#my-flow-manager",
        theme: {
            variables: {
                colors: {
                    neutral: "purple"
                }
            }
        }
    });
    // change the theme of all widgets
    appmixer.set('theme', {
        variables: {
            font: {
                family: 'serif'
            }
        }
    })
    
    // or/and change the theme of a single widget
    widget.set('theme', {
        variables: {
            colors: {
                neutral: 'green'
            }
        }
    });
    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'
            }
        }
    })
    appmixer.set('theme', {
        ui: {
            shapes: {
                action: "action",
                trigger: "trigger",
                selection: "selection"
            }
        }
    })
    appmixer.set('theme', {
        ui: {
            charts: {
                legendFontSize: "12px",
                legendFontFamily: "sans-serif",
                legendFontColor: "black",
                tickFontSize: "black",
                tickFontFamily: "monospaced",
                tickFontColor: "black",
                gridColor: "lightgray",
                colorway: [
                    '#493843',
                    '#61988E',
                    '#A0B2A6',
                    '#CBBFBB'
                ]
            }
        }
    })
    appmixer.ui.FlowManager({
        el: '#app',
        theme: {
            ui: {
                '#FlowManager': {
                    background: 'lightblue',
                    '#header': {
                        padding: '0 0 24px 0',
                        '#buttonCreateFlow': {
                            color: 'yellow',
                            '@hovered': {
                                color: 'white'
                            }
                        }
                    }
                }
            }
        }
    });
    wget  https://my.appmixer.com/appmixer/package/theme-light.json
    wget  https://my.appmixer.com/appmixer/package/theme-dark.json

    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.

    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.

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

    excludes

    string

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

    includes

    string

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

    logId

    string

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

    logIndex

    string

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

    flowId

    string

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

    excludes

    string

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

    includes

    string

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

    query

    string

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

    aggs

    object

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

    to

    string

    To date.

    from

    string

    From date.

    to

    string

    To date.

    from

    string

    From date.

    userId

    string

    A user ID.

    {
      "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"
        }
      ]
    }
    {
      "_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"
      }
    }
    {
        "aggregations": {
            "avg_price": {
                "value": 10
            },
            "sum_income": {
                "value": 2000
            }
        },
        "hits": [
            { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
            { "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
            ...
        ]
    }
    {
      "messageCounts": {
        "from": "2018-03-17",
        "to": "2018-04-17",
        "count": 348,
        "userId": "58593f07c3ee4f239dc69ff7"
      },
      "runningFlows": {
        "userId": "58593f07c3ee4f239dc69ff7",
        "count": 4
      },
      "activeConnectors": {
        "userId": "58593f07c3ee4f239dc69ff7",
        "count": 8
      },
      "usedApps": [
        "appmixer.utils",
        "appmixer.slack",
        "appmixer.asana",
        "appmixer.salesforce",
        "appmixer.twilio"
      ]
    }
    {
      "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"
        }
      ]
    }
    {
      "totalCount": 10,
      "totalSize": 9220
    }

    username

    MAIL_SMTP_PASS

    password

    MAIL_FROM_NAME

    Sender name

    Appmixer

    MAIL_FROM_EMAIL

    Sender email

    [email protected]

    FORGOT_PASSWORD_MAIL_SUBJECT

    Reset password email subject.

    Reset your password

    FORGOT_PASSWORD_MAIL_BODY

    The reset password email body.

    See below

    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.

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

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

    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.

    hashtag
    Service Manifest (service.json)

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

    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.

    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:

    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.

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

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

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

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

    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

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

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

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

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

    hashtag
    Packing and Publishing our Service

    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:

    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.

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

    Example: twilio.SendSMS

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

    hashtag
    Directory Structure

    hashtag
    Component Behaviour (sms/SendSMS/SendSMS.js)

    Defines how the component reacts on incoming messages.

    hashtag
    Component Manifest (sms/SendSMS/component.js)

    Defines the component properties and metadata.

    hashtag
    Component Dependencies (sms/SendSMS/package.json)

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

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

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

    hashtag
    Service Authentication Module Dependencies (package.json)

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

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

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

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

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

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

    twilio/
    ├── auth.js
    ├── package.json
    ├── service.json
    └── sms
        ├── ListFromNumbers
        │   ├── ListFromNumbers.js
        │   ├── component.json
        │   ├── package.json
        │   └── transformers.js
        └── SendSMS
            ├── SendSMS.js
            ├── component.json
            └── package.json
  • { event: "todo-updated", todo: { label: String, id: String } }

  • . 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:
    [vendor].[service]
    (or
    [vendor].[service].[module]
    in case the
    auth.js
    authentication module is defined under the module directory (which is not our case).

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

    https://docs.appmixer.com/appmixer/appmixer-self-managed/installation#enabling-users-to-publish-custom-componentsarrow-up-right
    https://en.wikipedia.org/wiki/Data_URI_schemearrow-up-right
    https://docs.appmixer.com/appmixer/component-definition/authentication#authentication-module-structurearrow-up-right
    https://github.com/axios/axios#request-configarrow-up-right
    https://docs.appmixer.com/appmixer/component-definition/behaviourarrow-up-right
    https://docs.appmixer.com/appmixer/appmixer-cli/appmixer-cliarrow-up-right
    file-archive
    7KB
    appmixer.tododemo.zip
    archive
    arrow-up-right-from-squareOpen
    Variables dynamically populated at design time with available Twilio phone numbers.
    tododemo
    ├── core
    │   └── NewTodo
    │       ├── NewTodo.js
    │       └── component.json
    ├── auth.js
    └── service.json
    {
        "name": "appmixer.tododemo",
        "label": "Todo Demo",
        "description": "Appmixer Todo Demo Connector",
        "category": "applications",
        "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA..."
    }
    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}}'
                }
            }
        }
    };
    {
        "name": "appmixer.tododemo.core.NewTodo",
        "author": "Appmixer <[email protected]>",
        "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" }
                ]
            }
        ]
    }
    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
                }
            });
        }
    };
    $ 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 [email protected] # note the user must have the "appmixer" vendor set in Backoffice
    $ appmixer pack tododemo/
    $ appmixer publish appmixer.tododemo.zip
    const twilio = require('twilio');
    
    module.exports = {
    
        receive(context) {
    
            let { accountSID, authenticationToken } = context.auth;
            let client = twilio(accountSID, authenticationToken);
            let message = context.messages.message.content;
    
            return client.messages.create({
                body: message.body,
                to: message.to,
                from: message.from
            }).then(message => {
                return context.sendJson(message, 'sent');
            });
        }
    };
    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <[email protected]>",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PGcgZmlsbD0iI0NGMjcyRCI+PHBhdGggZD0iTTEyNy44NiAyMjIuMzA0Yy01Mi4wMDUgMC05NC4xNjQtNDIuMTU5LTk0LjE2NC05NC4xNjMgMC01Mi4wMDUgNDIuMTU5LTk0LjE2MyA5NC4xNjQtOTQuMTYzIDUyLjAwNCAwIDk0LjE2MiA0Mi4xNTggOTQuMTYyIDk0LjE2MyAwIDUyLjAwNC00Mi4xNTggOTQuMTYzLTk0LjE2MiA5NC4xNjN6bTAtMjIyLjAyM0M1Ny4yNDUuMjgxIDAgNTcuNTI3IDAgMTI4LjE0MSAwIDE5OC43NTYgNTcuMjQ1IDI1NiAxMjcuODYgMjU2YzcwLjYxNCAwIDEyNy44NTktNTcuMjQ0IDEyNy44NTktMTI3Ljg1OSAwLTcwLjYxNC01Ny4yNDUtMTI3Ljg2LTEyNy44Ni0xMjcuODZ6Ii8+PHBhdGggZD0iTTEzMy4xMTYgOTYuMjk3YzAtMTQuNjgyIDExLjkwMy0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODUgMTEuOTAzIDI2LjU4NSAyNi41ODUgMCAxNC42ODQtMTEuOTAyIDI2LjU4Ni0yNi41ODUgMjYuNTg2LTE0LjY4MyAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4Nk0xMzMuMTE2IDE1OS45ODNjMC0xNC42ODIgMTEuOTAzLTI2LjU4NiAyNi41ODYtMjYuNTg2IDE0LjY4MyAwIDI2LjU4NSAxMS45MDQgMjYuNTg1IDI2LjU4NiAwIDE0LjY4My0xMS45MDIgMjYuNTg2LTI2LjU4NSAyNi41ODYtMTQuNjgzIDAtMjYuNTg2LTExLjkwMy0yNi41ODYtMjYuNTg2TTY5LjQzMSAxNTkuOTgzYzAtMTQuNjgyIDExLjkwNC0yNi41ODYgMjYuNTg2LTI2LjU4NiAxNC42ODMgMCAyNi41ODYgMTEuOTA0IDI2LjU4NiAyNi41ODYgMCAxNC42ODMtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDMtMjYuNTg2LTI2LjU4Nk02OS40MzEgOTYuMjk4YzAtMTQuNjgzIDExLjkwNC0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODYgMTEuOTAyIDI2LjU4NiAyNi41ODUgMCAxNC42ODQtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4NiIvPjwvZz48L3N2Zz4=",
        "description": "Send SMS text message through Twilio.",
        "auth": {
            "service": "appmixer:twilio"
        },
        "outPorts": [
            {
                "name": "sent",
                "options": [
                    { "label": "Message Sid", "value": "sid" }
                ]
            }
        ],
        "inPorts": [
            {
                "name": "message",
                "schema": {
                    "type": "object",
                    "properties": {
                        "body": { "type": "string" },
                        "to": { "type": "string" },
                        "from": { "type": "string" }
                    },
                    "required": [
                        "from", "to"
                    ]
                },
                "inspector": {
                    "inputs": {
                        "body": {
                            "type": "text",
                            "label": "Text message",
                            "tooltip": "Text message that should be sent.",
                            "index": 1
                        },
                        "from": {
                            "type": "select",
                            "label": "From number",
                            "tooltip": "Select Twilio phone number.",
                            "index": 2,
                            "source": {
                                "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                                "data": {
                                    "transform": "./transformers#fromNumbersToSelectArray"
                                }
                            }
                        },
                        "to": {
                            "type": "text",
                            "label": "To number",
                            "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                            "index": 3
                        }
                    }
                }
            }
        ]
    }
    
    {
        "name": "appmixer.twilio.sms.SendSMS",
        "version": "1.0.0",
        "main": "SendSMS.js",
        "author": "David Durman <[email protected]>",
        "dependencies": {
            "twilio": "^3.14.0"
        }
    }
    {
        "name": "appmixer.twilio",
        "label": "Twilio",
        "category": "applications",
        "description": "Twilio is a cloud communications platform as a service.",
        "icon": "data:image/png;base64,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 <[email protected]>",
        "description": "When triggered, returns a list of numbers from user.",
        "private": true,
        "auth": {
            "service": "appmixer:twilio"
        },
        "inPorts": [ "in" ],
        "outPorts": [ "numbers" ]
    }
    {
        "name": "appmixer.twilio.sms.ListFromNumbers",
        "version": "1.0.0",
        "description": "Appmixer component for twilio to get a list of phone numbers of a user.",
        "main": "ListFromNumbers.js",
        "author": "David Durman <[email protected]>",
        "dependencies": {
            "twilio": "^3.14.0"
        }
    }
    module.exports.fromNumbersToSelectArray = (numbers) => {
        let transformed = [];
        if (Array.isArray(numbers)) {
            numbers.forEach(number => {
                transformed.push({
                    label: number['phoneNumber'],
                    value: number['phoneNumber']
                });
            });
        }
        return transformed;
    };

    Accounts

    Authentication to apps.

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Query Parameters

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    Example of filtering certain accounts:

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

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

    hashtag
    Query Parameters

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

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

    The profileInfo object is optional. If you provide it it will be used. If you do not provide it then the from the authentication module will be used to get the profile info. Slack access tokens do not expire, therefore there is neither an expiration date nor a refresh token in the request.

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

    An example, this time an API Key account:

    Another example with a PWD account type:

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Query Parameters

    Name
    Type
    Description

    hashtag
    Get Authentication Status

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

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    Monitoring and Observability

    hashtag
    Overview

    When deploying Appmixer in a self-managed environment, implementing comprehensive monitoring and observability is crucial for maintaining system health, performance, and reliability. This guide provides recommendations for monitoring both the infrastructure components and the Appmixer application itself.

    Appmixer is a Node.js-based application that depends on several infrastructure components:

    • MongoDB - Primary data store

    • Redis - Caching

    • RabbitMQ - Message queue for asynchronous processing

    • Elasticsearch - Logs storage

    • Logstash - Log processing

    This document is organized into two main sections:

    • Application Monitoring - Appmixer-specific metrics and health indicators

    • Infrastructure Monitoring - Guidelines for monitoring the underlying services

    Note: We are actively working on expanding this documentation. Future updates will include additional metrics, detailed dashboards, and downloadable configuration files (Grafana dashboards, Prometheus configs, alerting rules) to help you set up monitoring faster. Check back regularly for updates or contact support if you need assistance with your monitoring setup.

    hashtag
    Application Monitoring

    hashtag
    Appmixer Application Metrics

    This section covers monitoring specific to the Appmixer Node.js application. The following areas should be monitored to ensure optimal application performance:

    hashtag
    Application Health Endpoints

    Appmixer provides two primary health check endpoints for monitoring application availability and system health:

    Root Endpoint: GET /

    The root endpoint provides a basic liveness check and returns basic API information. This endpoint does not require authentication and can be used for:

    • Kubernetes/OpenShift liveness probes - To detect if the pod is responsive

    • Load balancer health checks - To verify the service is accepting connections

    • Basic availability monitoring - To ensure the HTTP server is running

    Response Format:

    Response Fields:

    • name - Application name

    • version - Appmixer version

    • url - API endpoint URL

    Monitoring Recommendations:

    • Use this endpoint for basic availability monitoring

    • Expected response time: < 100ms

    • Expected HTTP status: 200 OK

    System Health Endpoint: GET /system/health

    The system health endpoint provides detailed metrics about the Appmixer application's internal state and performance. This endpoint is designed for deep health monitoring and diagnostics.

    Authentication:

    • Requires API key authentication OR

    • JWT authentication with admin scope

    Response Format:

    Response Fields:

    • inputQueue.messageCount - Number of messages waiting in the main input queue for processing

      • This is the most critical metric for monitoring system load

      • Normal range: 0-1000 messages

    Monitoring Recommendations:

    Critical Metrics to Monitor:

    • inputQueue.messageCount - Alert if > 2000 (warning) or > 10000 (critical)

    • HTTP status - Alert if not 200 OK

    Warning Indicators:

    • slowQueue.count increasing over time

    • events count growing continuously

    • Response time degradation

    Alert Examples:

    • Critical: Input queue > 1000 messages for > 5 minutes

    • Warning: Input queue > 500 messages for > 10 minutes

    • Warning: Slow queue count > 10 flows

    Dashboard Visualization:

    • Line chart: inputQueue.messageCount over time

    • Gauge: Current input queue size vs thresholds

    • Table: Top slow queue flows

    Example cURL Request:

    Integration with Monitoring Tools:

    Prometheus scraper configuration example:

    hashtag
    Flow Execution Metrics

    This section will be expanded in a future update to include metrics for workflow execution times, success/failure rates, and active flow monitoring. For now, we recommend monitoring the inputQueue.messageCount and slowQueue metrics from the /system/health endpoint as primary indicators of flow execution health.

    hashtag
    Component Performance

    Detailed component-level performance monitoring guidance is planned for a future documentation update. In the meantime, standard Node.js application monitoring practices apply, and errors from individual connectors will appear in your Elasticsearch logs.

    hashtag
    Infrastructure Monitoring

    hashtag
    General Principles

    For each infrastructure component, we recommend monitoring:

    • Availability - Is the service up and responding?

    • Performance - Response times, throughput, and latency

    • Resource Utilization - CPU, memory, disk, and network usage

    hashtag
    MongoDB Monitoring

    MongoDB is the primary data store for Appmixer. Monitor the following metrics:

    Key Metrics

    Database Performance

    • Query execution time (slow queries)

    • Operations per second (reads/writes)

    • Document scan rates

    • Index usage and efficiency

    Replication (if using replica sets)

    • Replication lag

    • Oplog window

    • Member health status

    • Election events

    Resource Usage

    • Memory utilization (resident and virtual)

    • Disk I/O (read/write operations)

    • Disk space usage and growth rate

    Recommended Tools

    • MongoDB Atlas (for managed MongoDB)

    • MongoDB Cloud Manager / Ops Manager

    • Prometheus with MongoDB exporter

    Alert Thresholds (Examples)

    • Replication lag > 10 seconds

    • Disk usage > 80%

    • Connection pool exhaustion > 90%

    • Slow queries > 1000ms

    hashtag
    Redis Monitoring

    Redis is used for caching. Monitor the following:

    Key Metrics

    Availability

    • Uptime

    • Master-slave sync status

    • Connection success rate

    Recommended Tools

    • Redis INFO command

    • Prometheus with Redis exporter

    • RedisInsight

    • Cloud-native monitoring (if using managed Redis)

    hashtag
    RabbitMQ Monitoring

    RabbitMQ handles asynchronous message processing for Appmixer workflows and tasks.

    Key Metrics

    Queue Health

    • Queue length (messages ready)

    • Messages unacknowledged

    • Message rate (publish/deliver/ack)

    • Queue growth rate

    Connection and Channels

    • Failed connection attempts

    Node Health

    • Memory usage (high/low watermarks)

    • Disk space (free/used)

    Cluster Health (if clustered)

    • Node availability

    • Network partition events

    • Mirror queue synchronization

    Recommended Tools

    • RabbitMQ Management Plugin

    • Prometheus with RabbitMQ exporter

    • Datadog, New Relic, or similar APM tools

    Alert Thresholds (Examples)

    • Queue length growing beyond normal capacity

    • Memory usage > 80% of high watermark

    • No consumers on critical queues

    hashtag
    Elasticsearch Monitoring

    Elasticsearch provides search capabilities and stores operational data.

    Key Metrics

    Cluster Health

    • Cluster status (green/yellow/red)

    • Number of nodes

    Resource Usage

    • JVM heap usage

    • JVM garbage collection time

    • CPU usage per node

    • Disk I/O per node

    Storage

    • Total disk space used

    • Index size growth rate

    Recommended Tools

    • Kibana Monitoring

    • Elasticsearch Monitoring API

    • Prometheus with Elasticsearch exporter

    • Cloud-native monitoring (if using managed Elasticsearch)

    Alert Thresholds (Examples)

    • Cluster status = red or yellow for > 5 minutes

    • JVM heap usage > 85%

    • Disk usage > 85%

    hashtag
    Logstash Monitoring

    Logstash processes and transforms log data before sending it to Elasticsearch.

    Key Metrics

    Pipeline Performance

    • Events received/filtered/sent

    Recommended Tools

    • Logstash Monitoring API

    • Kibana Monitoring

    • Prometheus with Logstash exporter

    Alert Thresholds (Examples)

    • JVM heap usage > 85%

    • Pipeline events duration increasing

    • Dead letter queue growing

    • Plugin errors increasing

    hashtag
    OpenShift Platform Monitoring

    Since Appmixer runs on OpenShift, leverage OpenShift's built-in monitoring capabilities:

    Key Aspects

    Pod Health

    • Pod status (Running/Failed/Pending)

    • Restart counts

    • Container resource usage vs limits

    Resource Quotas

    • Namespace CPU/memory usage

    • Storage usage

    • Pod count vs limits

    Network

    • Service availability

    • Ingress/route response times

    • Network policy effectiveness

    Persistent Volumes

    • Volume usage

    • Volume performance metrics

    • Volume mount issues

    Tools

    • OpenShift Monitoring (Prometheus-based)

    • OpenShift Web Console

    • oc CLI monitoring commands

    hashtag
    Recommended Monitoring Stack

    hashtag
    Option 1: Prometheus + Grafana (Open Source)

    • Prometheus for metrics collection

    • Grafana for visualization and dashboards

    • Alertmanager for alert routing

    hashtag
    Option 2: Commercial APM Solutions

    • New Relic

    • Datadog

    • Dynatrace

    • AppDynamics

    hashtag
    Option 3: Hybrid Approach

    • Use OpenShift's built-in Prometheus for infrastructure

    • Add custom Grafana dashboards

    • Integrate with existing enterprise monitoring tools

    hashtag
    Alerting Strategy

    hashtag
    Alert Severity Levels

    Critical - Immediate action required, service degradation or outage

    • Production outage

    • Data loss risk

    • Security breach

    Warning - Attention needed, potential issues developing

    • Resource usage approaching limits

    • Performance degradation

    • Increased error rates

    Info - Informational, no immediate action needed

    • Deployment notifications

    • Configuration changes

    • Capacity planning indicators

    hashtag
    Alert Best Practices

    • Define clear runbooks for each alert

    • Avoid alert fatigue by tuning thresholds

    • Use alert aggregation to reduce noise

    hashtag
    Logging Strategy

    hashtag
    Log Aggregation

    • Set appropriate log retention policies based on compliance requirements and storage capacity

    hashtag
    Log Levels

    Use appropriate log levels:

    • ERROR - Application errors requiring attention

    • WARN - Warning conditions

    • INFO - Informational messages

    hashtag
    Key Logs to Monitor

    • Application startup/shutdown events

    • Authentication and authorization failures

    • API request/response logs (with sampling)

    hashtag
    Performance Tuning

    Based on monitoring data, consider these tuning areas:

    hashtag
    Node.js Application

    • Adjust worker thread pool size

    • Optimize memory limits and heap size

    • Enable clustering for horizontal scaling

    hashtag
    Kubernetes Resources

    • Right-size CPU and memory requests/limits

    • Configure horizontal pod autoscaling (HPA)

    • Implement pod disruption budgets

    hashtag
    Capacity Planning

    Regular capacity planning should review:

    hashtag
    Growth Trends

    • User/tenant growth rate

    • Flow execution volume trends

    • Data storage growth

    hashtag
    Resource Utilization

    • Average and peak CPU/memory usage

    • Database storage growth

    • Network bandwidth utilization

    hashtag
    Performance Baselines

    • Establish performance baselines

    • Track deviation from baselines

    • Plan scaling activities before limits are reached

    hashtag
    Compliance and Security Monitoring

    • Monitor access logs for suspicious activity

    • Track failed authentication attempts

    • Review audit logs for compliance requirements

    hashtag
    Additional Resources

    System Configuration

    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.

    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 [email protected]. 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.

    hashtag

    profileInfo

    object

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

    componentType

    string

    Component Type.

    componentId

    string

    Component ID.

    filter

    string

    You can filter accounts.

    accountId

    string

    The ID of the account to update.

    string

    Human-readable name of the account.

    validateScope

    string

    If false, then the scope of the token from the body won't be validated against components installed in Appmixer.

    requestProfileInfo

    string

    If false, then the auth module requestProfileInfo function won't be called.

    displayName

    string

    Display name property of the account. This overrides the name of the account in the frontend.

    name

    string

    Name of the account, the authentication will determine the name of the account using the accountNameFromProfileInfo property.

    service

    string

    ID (vendor:service) of the service - `appmixer:google` for example.

    token

    object

    The structure of this object depends on the authentication type (Oauth1, Oauth2, API Key).

    accountId

    string

    Account ID.

    accountId

    string

    Account ID.

    accountId

    string

    Account ID.

    ticket

    string

    Authentication ticket.

    componentType

    string

    Component type.

    string

    Component ID.

    componentId

    string

    Component ID.

    accountId

    string

    Account ID.

    componentId

    string

    Component ID.

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

    studioUrl - Studio UI URL

  • integrationsUrl - Integrations marketplace URL

  • Alert if response time > 500ms or status != 200
  • Configure as liveness probe in OpenShift/Kubernetes

  • Warning threshold: > 2000 messages

  • Critical threshold: > 10000 messages

  • High queue length indicates processing bottleneck or insufficient resources

  • eventsListeners - Statistics about registered event listeners (webhooks, triggers)

    • total - Total number of active event listeners

    • byType - Breakdown by listener type

    • Helps monitor integration activity

  • events - Total count of events in the system

    • Represents pending events waiting for listeners to process

    • High count may indicate listener processing issues

  • listeners - Count of flow listeners

    • Number of components waiting for incoming data

  • slowQueue.count - Number of flows currently in the slow queue

    • Flows experiencing repeated failures or slow performance

    • Should be monitored for troubleshooting

  • slowQueue.top - Top 100 flows by slow queue occurrence

    • Identifies problematic flows requiring attention

    • Each entry includes flowId and count

  • systemWebhooks - System webhook statistics (only available on worker nodes)

    • registered - Number of system webhooks configured

    • active - Number of currently active webhooks

  • Info: System webhooks registered != active
    Counter: Total events, listeners, actions
    Error Rates - Connection errors, timeouts, and failures
  • Capacity Planning - Trends for storage, connections, and load

  • Network throughput
    Datadog, New Relic, or similar APM tools
    Disk space < 20% free
    Exporters for each infrastructure component
  • Custom exporters for Appmixer application metrics

  • Implement escalation policies
  • Test alerting channels regularly

  • DEBUG - Detailed diagnostic information (non-production)
    Integration connector errors
  • Database connection issues

  • Message queue processing errors

  • Review and optimize slow database queries
    Optimize persistent volume performance class
    Track SSL certificate expiration dates
    Elasticsearch Monitoring and Observabilityarrow-up-right
    OpenShift Monitoring documentationarrow-up-right
    MongoDB Operations Best Practicesarrow-up-right
    RabbitMQ Monitoring Guidearrow-up-right
    [
      {
        "accountId": "5a6e21f3b266224186ac7d03",
        "name": "U0UFJ0MFG - client IO",
        "displayName": null,
        "service": "appmixer:slack",
        "userId": "58593f07c3ee4f239dc69ff7",
        "profileInfo": {
          "id": "U0UFJ0MFG - client IO"
        },
        "icon": "data:image/png;base64,...rkJggg==",
        "label": "Slack"
      },
      {
        "accountId": "5a7313abb3a60729efe76f1e",
        "name": "[email protected]",
        "displayName": null,
        "service": "appmixer:pipedrive",
        "userId": "58593f07c3ee4f239dc69ff7",
        "profileInfo": {
          "name": "tomas",
          "email": "[email protected]"
        },
        "icon": "data:image/png;base64,...rkJggg==",
        "label": "Pipedrive"
      }
    ]  
    // 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": "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]"
        }
    }'
    { "5a6e21f3b266224186ac7d04": "valid" }
    { "accountId": "5abcd0ddc4c335326198c1b2" }
    [
      {
        "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" }
    {
        "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": "e25dc901-f92a-46a2-8d29-2573d4ad65e5" }
    {
        "accountId":"5a6e21f3b266224186ac7d03",
        "componentId":"e25dc901-f92a-46a2-8d29-2573d4ad65e5"
    }
    {
      "name": "appmixer",
      "version": "6.2.0",
      "url": "http://api.appmixer.com",
      "studioUrl": "https://studio.appmixer.com",
      "integrationsUrl": "https://studio.appmixer.com/integrations"
    }
    {
      "inputQueue": {
        "messageCount": 42
      },
      "eventsListeners": {
        "total": 150,
        "byType": {
          "webhook": 80,
          "scheduled": 70
        }
      },
      "events": 1523,
      "listeners": 245,
      "actions": 890,
      "slowQueue": {
        "count": 5,
        "top": [
          {
            "flowId": "flow-123",
            "count": 3
          }
        ]
      },
      "systemWebhooks": {
        "registered": 12,
        "active": 10
      }
    }
    # Using API key authentication
    curl -H "X-API-Key: your-api-key" https://api.appmixer.com/system/health
    
    # Using JWT token with admin scope
    curl -H "Authorization: Bearer your-jwt-token" https://api.appmixer.com/system/health
    scrape_configs:
      - job_name: 'appmixer-health'
        metrics_path: '/system/health'
        scheme: https
        static_configs:
          - targets: ['api.appmixer.com']
        bearer_token: 'your-api-key'
        scrape_interval: 60s
    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

    GC_FILES_RULES

    hashtag
    Configuring Single Sign-On

    Configuring Single Sign-On (SSO) for Appmixer allows your users to authenticate using their existing identity provider. Appmixer supports OpenID Connect (OIDC) and SAML standards for seamless integration.

    Required Configuration Parameters

    To set up SSO, you'll need to gather the following information from your Identity Provider (IdP):

    • idp_type: This specifies the authentication method. It can be either oidc or saml.

    • authorization_url: This is the URL where users will be redirected to log in via your IdP.

    • client_id: An identifier for your client application.

    Appmixer-Specific Parameters

    The following parameters are typically not part of a standard IdP configuration and will likely need to be added manually:

    • domains: A list of domains for which you want to enable SSO. Any user with an email address from a domain listed here will be redirected to your IdP's authentication page. For example: ["yourcompany.com", "example.org"]

    • role_mapping: This object maps roles from your IdP to Appmixer's roles. Appmixer currently supports admin and user roles. For example, if your IdP uses "administrator" for admins and "member" for users, your mapping might look like: {"admin": "administrator", "user": "member"}

    Parameters Based on Authentication Type

    You'll also need additional parameters depending on whether you're using SAML or OIDC:

    SAML

    • ,certificate: The certificate used to sign SAML requests.

    • reauth_method : What method to use to re-authenticate a user when their session expires. Can be either popup or iframe . While a hidden iframe is the neat way of handling this situation, this method is not allowed by some identity providers, so popup is used instead to ensure wider compatibility. If you are not sure about your provider's policy, you should use popup .

    OIDC

    • issuer: A unique string that identifies your identity provider.

    • client_secret: A secret identifier for your client application.

    • token_url: The URL where Appmixer can obtain a new OIDC token.

    • metadata_url: A publicly accessible URL provided by your IdP that contains essential information for the authentication process.

    Important Considerations Most of these parameters will be provided when you download the configuration from your Identity Provider. You'll primarily need to manually add the domains and role_mapping fields.

    It's crucial that your metadata_url is publicly accessible. This is generally true for cloud-based identity providers like AWS IAM and Google ID. However, if you're self-hosting your IdP, you might need to adjust your network configuration to ensure its public accessibility. Appmixer does not currently support configuring a proxy to access an IdP; this must be handled via your hosting machine's network settings. If you encounter issues, please contact our support center. Configuration Examples Once you've gathered all the necessary information, your configuration JSON should resemble one of the following examples:

    Applying Your Configuration

    Paste your finalized configuration JSON as a new Appmixer System Configuration with the key IDP_CONFIG.

    Additional Parameters

    handle_roles: Identity Providers (IdPs) may manage either Authentication and Authorization or just Authentication. By default, Appmixer delegates only authentication to the IdP. To allow the IdP to manage authorization as well (importing roles from the IdP), set this variable to true . By doing this, the roles in Appmixer will be ignored for all users who sign in through IdP, meaning some users might not be able to sign in if the proper role isn't granted to them. Please check auto_grant_user_role, below.

    auto_grant_user_role: Works in combination with handle_roles. If this is set to true then all users that are allowed to log in will be granted the minimum permissions needed to operate Appmixer. If this is false such users will be denied access until they get the appropriate permissions.

    enforce_sso: By default, we do not disable the legacy email access. If you'd rather have users log in exclusively using Single Sign-On, you can do so by setting this variable to true .

    disable_legacy_signup : This property is similar to enforce_sso, but only applies to new users. If set to true the option to sign up will be removed, and new users will only be able to access through Single Sign-On, while existing users will retain the ability to sign in with email and password. This is a weaker check than enforce_sso, so if both are active, this check will be ignored.

    roles_path: (Only required if handle_roles is true) This is the path within your IdP's authentication response where a user's roles can be found. The value varies between Identity Providers. Refer to the table below for common IdPs:

    Identity Provider
    Roles Path

    Keycloak

    realm_access.roles

    Google ID

    <not supported>

    Google Workspace

    depends on configuration

    Amazon IAM

    -

    SAML

    default_redirect: This should be the URL of your login page. It's used as a fallback if an authentication error prevents Appmixer from retrieving a valid redirect URL from the identity provider.

    expects_signed_assertions : Whether each single assertion will be signed separately. Defaults to false .

    hashtag
    Providers Configuration

    hashtag
    Google Workspace

    This section explains how to set up Single Sign-On using Google Workspace.

    1. First, log in to your Google Admin control panelarrow-up-right

    2. In the menu on the left, select Apps -> Web and Mobile Apps

    3. Now, in the new tab, select Add App -> Add Custom SAML app .

    4. Choose a name for your app and note all the details provided by Google; we will need them later.

    5. On step 3 of the procedure, when prompted to provide the service provider details, please follow the screenshot, replacing <your-tenant-url> with the actual URL of your application.

    6. Leave everything blank on the last step Attribute Mapping for now.

    7. Now that our SAML application is ready, we need to enable it for the users in our tenant. You can do it as you would for any other application (like Gmail, Drive, and so on).

    8. Finally, let's set up the roles needed to log in to Appmixer.

    9. Go to Directory -> Users -> More options -> Manage custom attributes

    10. On the next page, select Add Custom Attribute on the top right.

    11. Configure the attribute as shown in the screenshot. You can use any value you want for Category and Description. You can also change the name of your field.

    12. Choose a user you want to assign roles to and expand the Users Informations tab.

    13. You will find the newly created attribute. If you scroll down, you can write any string here; they will have to match the content of the role_mapping in your configuration on Appmixer (Step 15).

    14. Now go back to the app we created earlier and edit the attribute mapping to expose the newly created attribute. If you decided to use Roles like in the example, you can follow the screenshot in this case too; otherwise, please replace the role in the left drop-down of the screenshot with the one you created before. Keep the value of App attributes to Role .

    15. When creating the configuration JSON for Appmixer, please use the following format:

      (*) <ENTITY ID> is the one provided on point 5 of this guide, not the one shown in the App's metadata. They are called in the same way, and this can cause confusion, but they are completely different values.

    16. Please feel free to choose any value for enforce_sso and disable_legacy_signup and auto_grant_user_role based on your company policies.

    17. Minify the JSON and add it to the Appmixer configuration under the name IDP_CONFIG .

    Appmixer Backoffice
    {
        "idp_type": "oidc",
        "client_id": "a361cbac-a420-42ca-8f07-fc3520d5c36b",
        "client_secret": "0841fb03-55b8-4b0d-aeb0-330374348c09",
        "issuer": "my-idp",
        "domains": ["client.io"],
        "token_url": "https://idp.example.com/auth/realms/apm/protocol/openid-connect/token",
        "authorization_url": "https://idp.example.com/auth/realms/apm/protocol/openid-connect/auth",
        "metadata_url": "https://idp.example.com/auth/realms/apm/.well-known/openid-configuration",
        "role_mapping": {
            "admin": "adm",
            "user": "usr"
        }
    }
    {
        "idp_type": "saml",
        "client_id": "my-client-id"
        "authorization_url": "https://idp.example.com/auth/realms/apm/protocol/saml",
        "certificate": "MIIClTCCAX0CBgGV0[...]6smvQM7VU=",
        "domains": ["localhost", "client.io"],
        "role_mapping": {
            "admin": "adm",
            "user": "usr"
        }
        "default_redirect": "https://myapp.appmixer.ai/login"
    }
     [
        {
            "scope": "user",
    
            /**
             * The GC will never delete files created in the past TTL (in hours). By default,
             * set to 720 hours. The reason is to prevent the GC from removing files that could be needed by the
             * flows.
             */
            "ttl": 720,   // 30 days
    
            /**
             * The user will be blocked from saving any files if the limit is reached.
             */
            "hardLimit": 2000000000   // 2 GB
        }
    ]
    
    You can have different rules for different user scopes:
    [
        { "scope": "user:, "ttl": 720, "hardLimit": 2000000000 },
        { "scope": "admin", "ttl":1440, "hardLimit": 90000000000 }
    ]

    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

    Whether to display validation errors from the authentication modules.

    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

    Public files (needed usually for domain verification) can be served from different paths. Path prefixes have to be separated by :

    RETRY_BACKOFF

    In case of an error, a message for a Component is rescheduled for another attempt. A back-off strategy is used. This value defines the number of attempts and the number of minutes between them.

    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

    If set to true, the engine will reject any incoming HTTP requests that have cookies that don't comply with the HTTP cookies RFC specificationarrow-up-right.

    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

    GC_FILES_ENABLED

    If the Garbage collector for files is enabled.

    true

    GC_FILES_RULES

    Rules for the Gargabge collector.

    See below.

    IDP_CONFIG

    Identity provider configuration. Please see Configuring Single-Sing-On for more informations.

    No value

    IDP_ROLES_PATH

    Identity provider configuration. Please see Configuring Single-Sing-On for more informations.

    No value

    IDP_DEFAULT_REDIRECT

    Identity provider configuration. Please see Configuring Single-Sing-On for more informations.

    No value

    /:/.well-known
    1,5,60,300,720
    {
        "idp_type": "saml",
        "client_id": <ENTITY ID*>
        "authorization_url": <SSO URL>,
        "issuer": "my-idp",
        "certificate": <CERTIFICATE (METADATA)>,
        "domains": [<YOUR EMAIL DOMAIN>],
        "role_mapping": {
            "admin": <AS CONFIGURED ON STEP 13>,
            "user": <AS CONFIGURED ON STEP 13>
        }
        "default_redirect": <YOUR APP LOGIN PAGE>,
        "expects_signed_assertions": false,
        "handle_roles": true,
        "reauth_method": "popup"
    }

    Flows

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

    hashtag
    Query Parameters

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

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

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

    hashtag
    Request Body

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Query Parameters

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Clone Flow

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

    Clone a flow

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

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

    hashtag
    Path Parameters

    Name
    Type
    Description

    hashtag
    Query Parameters (only for the stop command)

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

    hashtag
    Get the status of the stop flow command

    GET https://api.YOUR_TENANT.appmixer.cloud/flows/{flowId}/coordinator/status

    hashtag
    Send GET request to a component

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

    Query Parameters

    Name
    Type
    Description

    hashtag
    Send POST request to a component

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

    Query Parameters

    Name
    Type
    Description

    hashtag
    Send PUT request to a component

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

    Query Parameters

    Name
    Type
    Description

    hashtag
    Send DELETE request to a component

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

    Query Parameters

    Name
    Type
    Description

    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.

    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.

    filter

    string

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

    sharedWithPermissions

    string

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

    projection

    string

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

    sort

    string

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

    id

    string

    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.

    id

    string

    Flow ID.

    forceUpdate

    boolean

    A running flow cannot be updated unless forceUpdate=true.

    object

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

    id

    string

    Flow ID.

    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

    id

    string

    Flow ID.

    background

    boolean

    Will trigger the stop command, but won't wait for it to finish. Then you can use GET /flows/{flowId}/coordinator/status to check the status of that operation.

    command

    string

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

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

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

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

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

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

    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.

    hashtag
    Installation

    • Download and install NodeJS: https://nodejs.orgarrow-up-right (version 18 is required)

    • npm install -g appmixer

    hashtag
    Help

    Display the command options with the -h option:

    Each command has its own help information:

    hashtag
    Initialization

    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 .

    Login to your Appmixer account and enter your password:

    hashtag
    Creating Custom Components

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

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

    circle-info

    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.

    hashtag
    Publish your component

    circle-info

    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.

    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.

    hashtag
    List all available components

    To see all the available components uploaded to Appmixer, use the appmixer component ls command:

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

    circle-info

    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!

    hashtag
    Updating your component

    You can re-publish (appmixer publish) your component which effectively replaces the old component with the new one.

    circle-info

    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.

    hashtag
    Testing your component

    circle-exclamation

    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

    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:

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

    hashtag
    Working With Flows

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

    hashtag
    Starting and Stopping Flows

    You can start and stop flows using the appmixer flow start and appmixer flow stop commands:

    hashtag
    Removing Flows

    To remove flows, use the appmixer flow remove command:

    circle-info

    Note that the removed flow will be automatically stopped if it was running. Also, note that this action cannot be undone.

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

    hashtag
    Downloading Modifiers

    The next command will download modifiers from Appmixer and save them into the modifiers.json file.

    hashtag
    Publishing Modifiers

    The next command will publish modifiers into Appmixer.

    hashtag
    Deleting Modifiers

    The next command will delete existing modifiers from Appmixer. The next time Appmixer starts, it will load the default set of Modifiers.

    appmixer test component HelloAppmixer -i "{\"in\": {\"text\": \"abc\"}}"
    http://localhost:2200arrow-up-right
    here
    Using Backoffice to set vendor property.
    Custom component published
    $ 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 [email protected]
    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": "[email protected]" } 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": "[email protected]" } }'
    
      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": "[email protected]" }}' -i '{ "in": { "to": "[email protected]" }}'
    
      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": "[email protected]",
                                    "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

    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.

    Component Configuration

    Configuration properties are defined using two objects schema and inspector.

    hashtag
    properties.schema

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

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

    hashtag
    properties.inspector

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

    circle-exclamation

    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.

    hashtag
    Inspector built-in types:

    hashtag
    text

    A single line input field.

    hashtag
    textarea

    A multi-line text input field.

    hashtag
    number

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

    hashtag
    key-value

    A field for entering key-value text pairs. Additional pairs can be added or removed dynamically. Produces a single string representing a stringified object from the entered key-value pairs.

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

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

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

    hashtag
    toggle

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

    hashtag
    color-palette

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

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

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

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

    This is how it is done:

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

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

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

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

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

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

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

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

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

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

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

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

    time_24hr

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

    weekNumbers

    Boolean. Enables display of week numbers in calendar.

    When opening the variables picker in myText field inside the second group, only variableB will be available, because variableA is already been used.
    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.

  • Option

    Description

    format

    String representing the format of the date/time. Please see the moment.js library documentation for all the available tokens: https://momentjs.com/docs/#/parsing/string-format/arrow-up-right.

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

    http://json-schema.orgarrow-up-right
    Rappid Inspector definition formatarrow-up-right
    CSS color formatsarrow-up-right
    source
    Invalid Inspector field
    Configuration Overview
    source with expand

    mode

    {
        "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": "key-value",
        "label": "An input field for key-value text pairs."
    }
    {
        "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"
    }

    Authentication

    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: API keyarrow-up-right, Password, OAuth 1arrow-up-right, and OAuth 2arrow-up-right.

    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.

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

    hashtag
    type

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

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

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

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

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

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

    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 .

    hashtag
    requestProfileInfo () (optional)

    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.

    See that implement this method.

    hashtag
    accountNameFromProfileInfo ()

    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.

    hashtag
    validate ()

    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:

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

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

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

    hashtag
    accountNameFromProfileInfo ()

    Works exactly the same way as described in the section.

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

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

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

    hashtag
    requestProfileInfo () (optional)

    Works exactly the same way as described in the section.

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

    hashtag
    OAuth 2

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

    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:

    hashtag
    accountNameFromProfileInfo ()

    Works exactly the same way as described in the section.

    hashtag
    authUrl ()

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

    Standard means, there is a response_type parameter set to code, 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 .

    The same logic applies to the following property requestAccessToken.

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

    hashtag
    requestProfileInfo () (optional)

    Works exactly the same way as described in the section.

    hashtag
    refreshAccessToken ()

    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.

    hashtag
    validateAccessToken ()

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

    hashtag
    scope

    String or an array of strings.

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

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

    hashtag
    refreshBeforeExpUnits

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

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

    hashtag
    Context

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

    hashtag
    async context.httpRequest

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

    hashtag
    async context.log(string severity, object)

    Just like in a component, this can be used to create a log record that is visible in the Insights. This is useful for debugging auth.js files.

    hashtag
    Custom "Connect account" button

    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 :

    hashtag
    Setting OAuth 1,2 secrets

    To set your OAuth applications secrets, follow the guideline.

    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.

    hashtag
    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 5-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 Appmixer REST API.

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

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

    hashtag
    Input/Output message(s)

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

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

    where the flow descriptor would contain something like this:

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

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

    circle-info

    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.

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

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

    hashtag
    Authentication

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

    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:

    hashtag
    Backoffice configuration

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

    hashtag
    Properties

    hashtag
    context.properties

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

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

    hashtag
    Component State

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

    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.

    circle-info

    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.

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

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

    hashtag
    async context.stateSet(key, value)

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

    hashtag
    async context.stateGet(key)

    Get a state value stored under key.

    hashtag
    async context.stateUnset(key)

    Remove a value under key.

    hashtag
    async context.stateClear()

    Clears the entire state.

    hashtag
    async context.stateAddToSet(key, value)

    Add value into set under key.

    hashtag
    async context.stateRemoveFromSet(key, value)

    Remove value from set under key.

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

    hashtag
    Flow State

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

    hashtag
    async context.flow.loadState()

    Load the state from the DB.

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

    hashtag
    async context.flow.stateGet(key)

    Get a state value stored under key.

    hashtag
    async context.flow.stateUnset(key)

    Remove a value under key.

    hashtag
    async context.flow.stateClear()

    Clears the entire state.

    hashtag
    async context.flow.stateAddToSet(key, value)

    Add value into a Set stored under key.

    hashtag
    async context.flow.stateRemoveFromSet(key, value)

    Remove value from Set stored under key.

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

    hashtag
    Service State

    This is similar to the component state, but the service state is available across all components in the connector.

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

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

    hashtag
    async context.service.stateGet(key)

    Get a state value stored under key.

    hashtag
    async context.service.stateUnset(key)

    Remove a value under key.

    hashtag
    async context.service.stateClear()

    Clears the entire state.

    hashtag
    async context.service.stateAddToSet(key, value)

    Add value into a Set stored under key.

    hashtag
    async context.service.stateRemoveFromSet(key, value)

    Remove value from Set stored under key.

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

    hashtag
    Files

    hashtag
    async context.saveFile(fileName, mimeType, buffer)

    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.

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

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

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

    hashtag
    async context.getFileInfo(fileId)

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

    Example return object:

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

    hashtag
    context.readFileStream(fileId)

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

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

    hashtag
    async context.removeFile(fileId)

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

    hashtag
    Webhook

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

    circle-info

    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:

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

    hashtag
    HTTP

    hashtag
    async context.httpRequest

    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.

    hashtag
    Store

    hashtag
    async context.store.listStores()

    Get the list of user's Data Stores.

    hashtag
    async context.store.get(storeId, key)

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

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

    Set value to the Data Store under the key.

    hashtag
    async context.store.remove(storeId, key)

    Remove the key from the Data Store.

    hashtag
    async context.store.clear(storeId)

    Clear all data from the Data Store.

    hashtag
    async context.store.find(storeId, query)

    Find items in the Data Store.

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

    Get a cursor.

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

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

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

    hashtag
    async context.store.unregisterWebhook(storeId);

    Unregister a webhook.

    hashtag
    Scheduling

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

    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.

    hashtag
    async context.clearTimeout(timeoutId)

    Clear (cancel) a scheduled timeout.

    hashtag
    Miscellaneous

    hashtag
    async context.callAppmixer(request)

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

    hashtag
    async context.stopFlow()

    Stop the running flow. Example:

    hashtag
    context.componentId

    The ID of the component.

    hashtag
    context.flowId

    The ID of the flow the component runs in.

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

    hashtag
    context.customFields

    Flow properties are available in this object.

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

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

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

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

    Example:

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

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

    hashtag
    async context.lock(lockName, options)

    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.

    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

    Example:

    hashtag
    Error Handling

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

    hashtag
    receive(context)

    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 5 times before it is saved into collection. Every unsuccessful attempt will be logged and visible in Insights.

    Sometimes you, as a developer of a component, know that there is no point in retrying a message 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.

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

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

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

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

    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.

    , number, 30 by default

    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.

    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.

    Unprocessed Messages
    context.saveFileStream
    AWS S3 GetFileObjectarrow-up-right
    context.getFileReadStream
    axiosarrow-up-right
    API section
    customFields
    quota
    unprocessedMessages
    Variables
    Authentication UI dialog for the above auth.js definition

    correlationId

    context
    NodeJS module
    function, object or string
    examples of real connectorsarrow-up-right
    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
    Asana auth.js modulearrow-up-right
    function or string
    API Key
    function, object or string
    JIRA auth.js module definitionarrow-up-right
    function, object or string
    the JIRA auth.js module definitionarrow-up-right
    function, object or string
    API Key
    function, object or string
    function, object or string
    OAuth 1
    axiosarrow-up-right
    Googlearrow-up-right
    Google auth.jsarrow-up-right
    Connector Configuration
    Authentication form for Freshdesk, defined by auth object
    Setting custom redirect URI just for appmixer:google module.
    Custom "Connect account" button in the Designer.
    Custom "Connect account" button in the Wizard.
    {
        receive(context) {
            const smsContent = context.messages.message.content;
        }
    }
    Humidity: {{{$.ec8cd99f-0ad3-4bca-9efc-ebea5be6b596.weather.main.humidity}}}
    context.messages.message.content === 'Humidity: 75'
    const Promise = require('bluebird');
    
    module.exports = {
        async receive(context) {
     
            // 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');
        }
    }
    const twilio = require('twilio');
    module.exports = {
        type: 'apiKey',
        definition() {
            return {
                tokenType: 'authentication-token',
                accountNameFromProfileInfo: 'accountSID',
                auth: {
                    accountSID: {
                        type: 'text',
                        name: 'Account SID',
                        tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
                    },
                    authenticationToken: {
                        type: 'text',
                        name: 'Authentication Token',
                        tooltip: 'Found directly next to your Account SID.'
                    }
                },
                validate: context => {
                    let client = new twilio(context.accountSID, context.authenticationToken);
                    return client.api.accounts.list();
                }
            };
        }
    };
    {
        receive(context) {
            let { accountSID, authenticationToken } = context.auth;
        }
    }
    module.exports = {
        receive(context) {
            const endpoint = context.config.baseUrl + '/weather';
            const { data } = await context.httpRequest.get(endpoint);
            // process results
        }
    };
    
    {
        "properties": {
            "schema": {
                "properties": {
                    "fromNumber": { "type": "string" }
                }
            },
            "inspector": {
                ...
            }
        }
    }
    {
        receive(context) {
            const fromNumber = context.properties.fromNumber;
        }
    }
    {
        async receive(context) {
            // Emit a message only once per day for this component instance.
            const day = (new Date).getDay();
            const state = context.state;
            if (!state[day]) {
                state[day] = true;
                await context.saveState(state);
                return context.sendJson({ tick: true }, 'out');
            }
        }
    }
    module.exports = {
    
        async start(context) {
    
            // register webhook in the slack plugin
            return context.service.stateAddToSet(
                context.properties.channelId,
                {
                    componentId: context.componentId,
                    flowId: context.flowId
                }
            );
        },
    
        async stop(context) {
    
            return context.service.stateRemoveFromSet(
                context.properties.channelId,
                {
                    componentId: context.componentId,
                    flowId: context.flowId
                }
            );
        }
    }
    {
        receive(context) {
            // getAttachment() is 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');
                });
        }
    }
    // Example
    {
      length: 7,
      chunkSize: 261120,
      uploadDate: "2023-03-22T10:34:07.751Z",
      filename: "test",
      md5: "3e47b75000b0924b6c9ba5759a7cf15d",
      metadata: {
        userId: "638f734271b34799e665c9b9",
        fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2",
        originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
      },
      fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2"
    }
    // Example
    {
      length: 7,
      chunkSize: 261120,
      uploadDate: "2023-03-22T10:34:07.765Z",
      filename: "something.txt",
      md5: "3e47b75000b0924b6c9ba5759a7cf15d",
      metadata: {
        userId: "638f734271b34799e665c9b9",
        fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030",
        originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
      },
      fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030"
    }
    'use strict';
    
    module.exports = {
    
        receive(context) {
    
            const { fileId, content } = context.messages.in.content;
            return context.replaceFileStream(
                fileId,
                content
            ).then(savedFile => {
                return context.sendJson(savedFile, 'file');
            });
        }
    };
    
    {
      "filename": "testFile",
      "contentType": "text",
      "length": 7,
      "chunkSize": 261120,
      "uploadDate": "2021-01-22T12:20:29.227Z",
      "metadata": {
        "userId": "5f804b96ea48ec47a8c444a7",
        "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
      },
      "md5": "9a0364b9e99bb480dd25e1f0284c8555",
      "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
    }
    {
        receive(context) {
            return context.loadFile(context.messages.file.content.fileId)
                .then((fileContent) => {
                    // uploadFileToAPI() is some function that uploads a file to an API
                    return uploadFileToAPI(context.auth, content.messages.file.content.fileName, fileContent);
                });
        }
    }
    module.exports = {
        async receive(context) {
            if (context.messages.webhook) {
                // Webhook URL received data.
                await context.sendJson(context.messages.webhook.content.data, 'myOutPort');
                // Send response to the webhook HTTP call.
                // Note: you can also skip sending response immediately and send it
                // in other connected components in the flow.
                // If context.response() is not called, the engine waits for the first component
                // that sends the response (in the same "session", i.e. the same "message flow").
                return context.response('<myresponse></myresponse>', 200, { 'Content-Type': 'text/xml' });
            }
            // Otherwise, normal input port received data.
            const input = context.messages.myInPort.content;
    
            // The webhook URL. Do something with it (send to your API, send to other connected,
            // components, send to your backend, ...)
            const url = context.getWebhookUrl();
        }
    };
    
    module.exports = {
      async receive(context) {
        const { data } = await context.httpRequest({
          url: "https://some-url.com/api",
          method: "POST",
          data: {
            username: "someuser",
            password: "somepass",
          },
          headers: { Authentication: "some"}        
        });
            
        // it is also possible to execute dedicated http methods
        // see here https://axios-http.com/docs/api_intro
        // for example
        // context.httpRequest.get(url[, config])
        const { data } = await context.httpRequest.get("https://url", { params: { name: "some" }, headers: {}});
        // context.httpRequest.post(url[, data[, config]])
        const { data } = await context.httpRequest.post("https://url", { username: "some" }, headers: {});
            
        return context.response(data);
      }
    }
    await context.store.find(storeId, { query: { key: { $nin: rowIds } } })
    module.exports = {
    
        async receive(context) {
    
            // whenever there is an item added/updated/removed from the data
            // store, this component will get triggered with data on the
            // context.messages.webhook.content
            const data = context.messages.webhook.content.data.currentValue;
    
            if (context.messages.webhook.content.data.type === 'insert') {
                await context.sendJson({
                    key: data.key,
                    storeId: data.storeId,
                    value: data.value,
                    updatedAt: data.updatedAt,
                    createdAt: data.createdAt
                }, 'item');
            }
            return context.response('ok');
        },
    
        async start(context) {
    
            // register without specifying 'events'.
            await context.store.registerWebhook(context.properties.storeId);
        },
    
        async stop(context) {
    
            await context.store.unregisterWebhook(context.properties.storeId);
        }
    };
    module.exports = {
    
        async receive(context) {
    
            const data = context.messages.webhook.content.data.currentValue;
            await context.sendJson({
                key: data.key,
                storeId: data.storeId,
                value: data.value,
                updatedAt: data.updatedAt,
                createdAt: data.createdAt
            }, 'item');
            return context.response('ok');
        },
    
        async start(context) {
    
            // register only the 'insert' events on the data store.
            await context.store.registerWebhook(context.properties.storeId, ['insert']);
        },
    
        async stop(context) {
    
            await context.store.unregisterWebhook(context.properties.storeId);
        }
    };
    module.exports = {
        receive(context) {
            if (context.messages.timeout) {
                // Timeout message.
                return context.sendJson(context.messages.timeout.content, 'out');
            } else {
                // Normal input message.
                return context.setTimeout(context.messages.in.content, 5 * 60 * 1000);
            }
        }
    };
    const task = await context.callAppmixer({
        endPoint: '/people-task/tasks',
        method: 'POST',
        body: {
            title: 'My Task',
            description: 'My Example Task',
            requester: '[email protected]',
            approver: '[email protected]',
            decisionBy: (function() {
                const tomorrow = new Date;
                tomorrow.setDate(tomorrow.getDate() + 1);
                return tomorrow.toISOString();
            })()
        }
    });
    module.exports = {
        async receive(context) {
            return context.stopFlow();
        }
    };
    '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 });
        }
    };
    [
      {
        "variables": {
          "dynamic": [
            {
              "label": "Column A",
              "value": "columnA",
              "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
              "port": "out"
            },
            {
              "label": "Column B",
              "value": "columnB",
              "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
              "port": "out"
            }
          ],
          "static": {}
        },
        "sourceComponentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
        "outPort": "out",
        "inPort": "in"
      },
      {
        "variables": {
          "dynamic": [
            {
              "label": "Column C",
              "value": "columnC",
              "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
              "port": "out"
            },
            {
              "label": "Column D",
              "value": "columnD",
              "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
              "port": "out"
            }
          ],
          "static": {}
        },
        "sourceComponentId": "71eb1f40-ce29-4963-8922-102739bafee4",
        "outPort": "out",
        "inPort": "in"
      }
    ]
    {
        receive(context) {
            return context.loadVariables()
                .then(data => {
                    const newSchema = data.reduce((acc, item) => {
                        return acc.concat(item.variables.dynamic.map(o => ({ label: o.label, value: o. value })));
                    }, []);
                    context.sendJson(newSchema, 'leftJoin');
                    context.sendJson(newSchema, 'innerJoin');
                    context.sendJson(newSchema, 'rightJoin');
                });
        }
    }
    {
        async start(context) {
            
            await context.log({ test: 'my test log' });
            return context.sendJson({ started: (new Date()).toISOString() }, 'out');
        }
    }
        async receive(context) {
    
            let lock = null;
            try {
                lock = await context.lock(context.flowId, {
                    ttl: 30000,
                    maxRetryCount: 1
                });
                let { callCount = 0, messages = [] } = await context.loadState();
                messages.push(context.messages.in.originalContent);
    
                if (++callCount === context.messages.in.content.callCount) {
                    await context.sendJson(messages || [], 'out');
                    await context.saveState({ callCount });
                    return;
                }
    
                await context.saveState({ callCount, messages });
            } finally {
                if (lock) {
                    await lock.unlock();
                }
            }
        }
    
    /**
     * Example of context.CancelError
     */
    module.exports = {
    
        async receive(context) {
    
            let data = context.messages.in.content;
    
            try {
                // In this component is trying to create a record in a 3rd party
                // system.
                const resp = await someAPI.createSomething(data);
                await context.sendJson(resp, 'out');
            } catch (err) {
                // And there might be a unique constraint, let's say an email
                // address. And the 3rd party API will return an error with a
                // message saying that this record cannot be created.
                if (err.message === 'duplicate record') {
                    // In this case, we can tell Appmixer to cancel the message.
                    // Because next attempt would fail again with the same result.
                    throw new context.CancelError(err.message);
                }
                // In case of any other error, rethrow the exception. Appmixer will
                // then try to process it again.
                throw err;
            }
        }
    };
    
    // 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: '[email protected]',
            name: 'Appmixer example',
            // More properties here...
        }
        // There can be more properties here as well...
    }
    'use strict';
    
    module.exports = {
    
        type: 'apiKey',
    
        definition: {
    
            tokenType: 'apiKey',
    
            accountNameFromProfileInfo: 'appId',
    
            auth: {
                appId: {
                    type: 'text',
                    name: ' APP ID',
                    tooltip: 'Log into your account and find Api key.'
                },
                apiKey: {
                    type: 'text',
                    name: 'REST API Key',
                    tooltip: 'Found directly next to your App ID.'
                },
                cert: {
                    // just to show, that a 'textarea' input is supported as well
                    type: 'textarea',
                    name: 'TLS CA',
                    tooltip: 'Paste text content of <code>.crt</code> file' 
                }
            },
    
            // In the validate request we need the appId and apiKey specified by the user.
            // All properties defined in the previous 'auth' object will be
            // available in the validate call. Just use {{whatever-key-from-auth-object}}
            // anywhere in the next object. Appmixer will replace these with the
            // correct values.
            validate: {
                method: 'GET',
                url: 'https://acme.com/get-some-records/app_id={{appId}}',
                headers: {
                    'Authorization': 'Basic {{apiKey}}'
                }
            }
        }
    };
    
    validate: {
        'method': 'GET',
        'uri': 'https://api.apify.com/v2/users/{{apiUserId}}?token={{apiToken}}'
    },
    
     validateErrCallback: err => {
    
        if (err?.someNestedObject?.message) {
            throw new Error(err.someNestedObject.message);  // message for the UI
        }
        throw err;
    }
    'use strict';
    
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            // As opposed to the 'apiKey' module, the 'auth' property inside the 
            // definition object is optional for the 'pwd' type. If omitted, it will 
            // have the following structure. If you want to change name of the field,
            // change the content of the tooltip or add another field, you can do
            // that here. 
            auth: {
                username: {
                    type: 'text',
                    name: 'Username',
                    tooltip: 'Username'
                },
                password: {
                    type: 'password',
                    name: 'Password',
                    tooltip: 'Password'
                }
            },
    
            // the only mandatory property for the 'pwd' authentication type
            validate: async context => {
            
                const { username, password } = context
                const { data } = await context.httpRequest.post(
                    // the URL for the username/password authentication
                    'https://service-server-api-url/auth',
                    { username, password }
                );
                
                // verify authentication works
                if (!data.isValid) {
                    throw new Error("Invalid username/password combination.");
                }
                
                // if the API does not return a token for exchange, then return here.
                // in such case, the components using this authentication module
                // will have to use the username/password to perform each request.
                // There will be properties context.auth.username and 
                // context.auth.password available in the context object in a component.
                
                // if the API returns a token and expiration then return them using 
                // the following way - object with 'token' and 'expires' properties.
                const { token, expiresIn } = data;
                
                // the token and expiration (if provided) will be available in a
                // component's context object under context.auth.token and
                // context.auth.expires
                return {
                    token,
                    // expires can be a Date - token expiration date (timestamp),
                    // or number/string with seconds representing a token lifetime.
                    // Appmixer will use this to request a new token (using stored
                    // username and password) when the current one is about to expire
                    expires: expiresIn
                };
                
                // if the API returns a token that does not expire, just return
                // that token 
                // return { token };
            }
        }
    };
    
    'use strict';
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            validate: async context => {
    
                await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                    auth: {
                        username: context.username,
                        password: context.password
                    }
                });
            }
        }
    }
    'use strict';
    module.exports = {
    
        async start(context) {
    
            await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                auth: {
                    username: context.auth.username,
                    password: context.auth.password
                }
            });
        }
    }
    'use strict';
    const OAuth = require('oauth').OAuth;
    const Promise = require('bluebird');
    
    module.exports = {
    
        type: 'oauth',
    
        // In this example, 'definition' property is defined as a function. 
        definition: context => {
    
            let trelloOauth = Promise.promisifyAll(new OAuth(
                'https://trello.com/1/OAuthGetRequestToken',
                'https://trello.com/1/OAuthGetAccessToken',
                context.consumerKey,
                context.consumerSecret,
                '1.0',
                context.callbackUrl,
                'HMAC-SHA1'
            ), { multiArgs: true });
    
            return {
    
                accountNameFromProfileInfo: 'id',
    
                authUrl: context => {
    
                    return 'https://trello.com/1/OAuthAuthorizeToken' +
                        '?oauth_token=' + context.requestToken +
                        '&name=AppMixer' +
                        '&scope=read,write,account' +
                        '&expiration=never';
                },
    
                requestRequestToken: () => {
    
                    return trelloOauth.getOAuthRequestTokenAsync()
                        .then(result => {
                            return {
                                requestToken: result[0],
                                requestTokenSecret: result[1]
                            };
                        });
                },
    
                requestAccessToken: context => {
    
                    return trelloOauth.getOAuthAccessTokenAsync(
                        context.requestToken,
                        context.requestTokenSecret,
                        context.oauthVerifier
                    ).then(result => {
                        return {
                            accessToken: result[0],
                            accessTokenSecret: result[1]
                        };
                    });
                },
    
                requestProfileInfo: context => {
    
                    return trelloOauth.getProtectedResourceAsync(
                        'https://api.trello.com/1/members/me',
                        'GET',
                        context.accessToken,
                        context.accessTokenSecret
                    ).then(result => {
                        if (result[1].statusCode !== 200) {
                            throw new Error(result[1].statusMessage);
                        }
                        result = JSON.parse(result[0]);
                        // get rid of limits for now
                        // may and will contain keys with dots - mongo doesn't like it
                        delete result.limits;
                        return result;
                    });
                },
    
                validateAccessToken: context => {
    
                    return trelloOauth.getProtectedResourceAsync(
                        'https://api.trello.com/1/tokens/' + context.accessToken,
                        'GET',
                        context.accessToken,
                        context.accessTokenSecret
                    ).then(result => {
                        if (result[1].statusCode === 401) {
                            throw new context.InvalidTokenError(result[1].statusMessage);
                        }
                        if (result[1].statusCode !== 200) {
                            throw new Error(result[1].statusMessage);
                        }
    
                        result = JSON.parse(result[0]);
                        if (result['dateExpires'] === null) {
                            return;
                        }
                        throw new context.InvalidTokenError('Invalid token.');
                    });
                }
            };
        }
    };
    
    'use strict';
    const request = require('request-promise');
    
    module.exports = {
    
        type: 'oauth2',
    
        // function definition is used in this case because of the 'profileInfo'
        // property 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;
                },
                
        ...
    }
    

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

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

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

    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.

    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:

    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.

    To patch the OpenAPI spec with a JSON Patch file, pass the path to the patch file to the --patch argument:

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

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

    hashtag
    OpenAPI specification to Appmixer Connector mapping

    The following table describes how the OpenAPI specification fields/features are mapped to Appmixer connectors.

    OpenAPI field/feature
    Appmixer Connector Field/Feature

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

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

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

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

    hashtag
    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

    hashtag
    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

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

    hashtag
    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

    hashtag
    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

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

    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:

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

    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:

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

    Examples

    Two API keys (one username, one api key, custom value for Authorization header)

    One API key (custom value for authorization header)

    hashtag
    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

    hashtag
    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

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

    hashtag
    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

    hashtag
    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

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

    • transform ... A expression that must transform the resulting data into an array of objects with value and label fields. The label

    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:

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

    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

    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.

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

    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

    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.

    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

    hashtag
    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.boredapi.com/documentation#endpoints-price-range">link text</a>.

    Location

    • Info Object

    Value

    A string.

    Example

    hashtag
    Examples

    hashtag
    BoredAPI

    • A minimalistic OpenAPI spec for the single-endpoint https://www.boredapi.com/ API.

    • No JSON patch, no OpenAPI extensions are used.

    • OpenAPI Spec

    hashtag
    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

    hashtag
    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

    Used extensions:

    • x-connector-icon

    • x-connector-service

    • x-connector-module

    hashtag
    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

    Used extensions:

    • x-connector-icon

    • x-connector-service

    • x-connector-module

    hashtag
    Supported OpenAPI versions

    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.

    Also note that the tool accepts both YAML and JSON files as inputs for the OpenAPI specification.

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

    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.

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

    headers ... HTTP headers object.
  • query ... HTTP query object (key-value pairs representing the query string parameters).

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

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

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

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

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

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

  • and
    unsubscribe
    objects.
  • {$request.body} ... The entire request body. See OpenAPI runtime expressionsarrow-up-right 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 OpenAPI runtime expressionsarrow-up-right 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.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.

  • {$response.header.header_name} ... The value of the specified response header. See OpenAPI runtime expressionsarrow-up-right for more details.

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

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

  • ... 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-webhook
  • x-connector-source

  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

  • OpenAPI Webhooks Spec

  • JSON Patch

  • x-connector-webhook
  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

  • x-connector-field-index

  • x-connector-transform

  • JSON Patch
    x-connector-pagination
  • x-connector-connection

  • x-connector-field-index

  • x-connector-source

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

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

    RFC 6902arrow-up-right
    a quick referencearrow-up-right
    JSON Pointerarrow-up-right
    JSON Path expressionarrow-up-right
    JMESPatharrow-up-right
    swagger2openapiarrow-up-right
    tranform functionsarrow-up-right
    $ 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
    { "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.boredapi.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